From 08aa277974c18ab32a81e3c017452ce445e75b80 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 11 Mar 2026 00:27:14 -0500 Subject: [PATCH 001/224] stage in changes from off-plate-gravity and remove stuff I didn't need --- resources/shaders/110/gouraud.fs | 1 + resources/shaders/110/gouraud.vs | 5 +- resources/shaders/110/mm_gouraud.fs | 3 +- resources/shaders/110/mm_gouraud.vs | 1 + resources/shaders/140/gouraud.fs | 1 + resources/shaders/140/gouraud.vs | 5 +- resources/shaders/140/mm_gouraud.fs | 3 +- resources/shaders/140/mm_gouraud.vs | 1 + src/libslic3r/ExPolygon.hpp | 5 ++ src/libslic3r/Preset.cpp | 2 +- src/libslic3r/PrintConfig.cpp | 24 ++++++++ src/libslic3r/PrintConfig.hpp | 3 + src/libslic3r/Support/SupportMaterial.cpp | 22 ++++++-- src/libslic3r/Support/TreeSupport.cpp | 18 +++++- src/libslic3r/Support/TreeSupport3D.cpp | 16 +++++- src/libslic3r/TriangleSelector.cpp | 12 ++-- src/libslic3r/TriangleSelector.hpp | 6 +- src/slic3r/GUI/3DBed.cpp | 59 ++++++++++++++++++++ src/slic3r/GUI/3DBed.hpp | 4 +- src/slic3r/GUI/3DScene.cpp | 18 +++++- src/slic3r/GUI/GUI_Factories.cpp | 4 +- src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp | 20 +++++-- src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp | 1 + src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp | 28 ++++++++-- src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp | 3 + src/slic3r/GUI/Tab.cpp | 2 + 26 files changed, 230 insertions(+), 37 deletions(-) diff --git a/resources/shaders/110/gouraud.fs b/resources/shaders/110/gouraud.fs index e602d6067d..12f4feca3e 100644 --- a/resources/shaders/110/gouraud.fs +++ b/resources/shaders/110/gouraud.fs @@ -26,6 +26,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform vec4 uniform_color; diff --git a/resources/shaders/110/gouraud.vs b/resources/shaders/110/gouraud.vs index ff3a190c79..e2de2b63e5 100644 --- a/resources/shaders/110/gouraud.vs +++ b/resources/shaders/110/gouraud.vs @@ -23,6 +23,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform mat4 view_model_matrix; @@ -71,8 +72,8 @@ void main() // Point in homogenous coordinates. world_pos = volume_world_matrix * vec4(v_position, 1.0); - // z component of normal vector in world coordinate used for slope shading - world_normal_z = slope.actived ? (normalize(slope.volume_world_normal_matrix * v_normal)).z : 0.0; + // dot product of world normal with up direction, used for slope shading + world_normal_z = slope.actived ? dot(normalize(slope.volume_world_normal_matrix * v_normal), slope.up_direction) : 0.0; gl_Position = projection_matrix * position; // Fill in the scalars for fragment shader clipping. Fragments with any of these components lower than zero are discarded. diff --git a/resources/shaders/110/mm_gouraud.fs b/resources/shaders/110/mm_gouraud.fs index 821af13f03..431c936e9c 100644 --- a/resources/shaders/110/mm_gouraud.fs +++ b/resources/shaders/110/mm_gouraud.fs @@ -37,6 +37,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform SlopeDetection slope; @@ -85,7 +86,7 @@ void main() color = LightBlue; alpha = 1.0; } - else if( transformed_normal.z < slope.normal_z - EPSILON) + else if( dot(transformed_normal, slope.up_direction) < slope.normal_z - EPSILON) { color = color * 0.5 + LightRed * 0.5; alpha = 1.0; diff --git a/resources/shaders/110/mm_gouraud.vs b/resources/shaders/110/mm_gouraud.vs index b0cea9cfd6..c54ae4bff5 100644 --- a/resources/shaders/110/mm_gouraud.vs +++ b/resources/shaders/110/mm_gouraud.vs @@ -24,6 +24,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform SlopeDetection slope; void main() diff --git a/resources/shaders/140/gouraud.fs b/resources/shaders/140/gouraud.fs index bbfb76f7a1..0d9fa021fe 100644 --- a/resources/shaders/140/gouraud.fs +++ b/resources/shaders/140/gouraud.fs @@ -26,6 +26,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform vec4 uniform_color; diff --git a/resources/shaders/140/gouraud.vs b/resources/shaders/140/gouraud.vs index 72dd4ff1bf..c69dcd11df 100644 --- a/resources/shaders/140/gouraud.vs +++ b/resources/shaders/140/gouraud.vs @@ -23,6 +23,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform mat4 view_model_matrix; @@ -71,8 +72,8 @@ void main() // Point in homogenous coordinates. world_pos = volume_world_matrix * vec4(v_position, 1.0); - // z component of normal vector in world coordinate used for slope shading - world_normal_z = slope.actived ? (normalize(slope.volume_world_normal_matrix * v_normal)).z : 0.0; + // dot product of world normal with up direction, used for slope shading + world_normal_z = slope.actived ? dot(normalize(slope.volume_world_normal_matrix * v_normal), slope.up_direction) : 0.0; gl_Position = projection_matrix * position; // Fill in the scalars for fragment shader clipping. Fragments with any of these components lower than zero are discarded. diff --git a/resources/shaders/140/mm_gouraud.fs b/resources/shaders/140/mm_gouraud.fs index c5fe86efc5..7c77d5565e 100644 --- a/resources/shaders/140/mm_gouraud.fs +++ b/resources/shaders/140/mm_gouraud.fs @@ -37,6 +37,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform SlopeDetection slope; @@ -87,7 +88,7 @@ void main() color = LightBlue; alpha = 1.0; } - else if( transformed_normal.z < slope.normal_z - EPSILON) + else if( dot(transformed_normal, slope.up_direction) < slope.normal_z - EPSILON) { color = color * 0.5 + LightRed * 0.5; alpha = 1.0; diff --git a/resources/shaders/140/mm_gouraud.vs b/resources/shaders/140/mm_gouraud.vs index b191e35fa8..191613f957 100644 --- a/resources/shaders/140/mm_gouraud.vs +++ b/resources/shaders/140/mm_gouraud.vs @@ -24,6 +24,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform SlopeDetection slope; void main() diff --git a/src/libslic3r/ExPolygon.hpp b/src/libslic3r/ExPolygon.hpp index 87a1146d41..7a51f70ef7 100644 --- a/src/libslic3r/ExPolygon.hpp +++ b/src/libslic3r/ExPolygon.hpp @@ -377,6 +377,11 @@ inline void translate(ExPolygons &expolys, const Point &p) { expoly.translate(p); } +inline void translate(Polygons &polys, const Point &p) { + for (Polygon &poly : polys) + poly.translate(p); +} + inline void polygons_append(Polygons &dst, const ExPolygon &src) { dst.reserve(dst.size() + src.holes.size() + 1); diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 826d8a1a19..ef2c83b13c 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1010,7 +1010,7 @@ static std::vector s_Preset_machine_limits_options { static std::vector s_Preset_printer_options { "printer_technology", - "printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "gcode_flavor", + "printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "build_plate_tilt_x", "build_plate_tilt_y", "gcode_flavor", "fan_kickstart", "fan_speedup_time", "fan_speedup_overhangs", "single_extruder_multi_material", "manual_filament_change", "file_start_gcode", "machine_start_gcode", "machine_end_gcode", "before_layer_change_gcode", "printing_by_object_gcode", "layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode", "change_filament_gcode", "change_extrusion_role_gcode", "printer_model", "printer_variant", "printer_extruder_id", "printer_extruder_variant", "extruder_variant_list", "default_nozzle_volume_type", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 50be57e0f8..918fc1b97f 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -5918,6 +5918,30 @@ void PrintConfigDef::init_fff_params() def->mode = comSimple; def->set_default_value(new ConfigOptionFloatOrPercent(50., true)); + def = this->add("build_plate_tilt_x", coFloat); + def->label = L("Build plate tilt X"); + def->category = L("Support"); + def->tooltip = L("Tilt angle of the build plate along the X axis. " + "A positive value tilts the plate so the +X side is higher, shifting gravity toward -X and increasing overhangs on the +X side. " + "A negative value tilts the -X side higher. Set to 0 for no X-axis tilt."); + def->sidetext = u8"\u00B0"; + def->min = -45; + def->max = 45; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0.)); + + def = this->add("build_plate_tilt_y", coFloat); + def->label = L("Build plate tilt Y"); + def->category = L("Support"); + def->tooltip = L("Tilt angle of the build plate along the Y axis. " + "A positive value tilts the plate so the +Y side is higher, shifting gravity toward -Y and increasing overhangs on the +Y side. " + "A negative value tilts the -Y side higher. Set to 0 for no Y-axis tilt."); + def->sidetext = u8"\u00B0"; + def->min = -45; + def->max = 45; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0.)); + def = this->add("tree_support_branch_angle", coFloat); def->label = L("Tree support branch angle"); def->category = L("Support"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 54c26c312a..c862956544 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1410,6 +1410,9 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( PrintConfig, (MachineEnvelopeConfig, GCodeConfig), + // Build plate tilt for off-axis gravity support generation (printer-level setting). + ((ConfigOptionFloat, build_plate_tilt_x)) + ((ConfigOptionFloat, build_plate_tilt_y)) //BBS ((ConfigOptionInts, additional_cooling_fan_speed)) ((ConfigOptionBool, reduce_crossing_wall)) diff --git a/src/libslic3r/Support/SupportMaterial.cpp b/src/libslic3r/Support/SupportMaterial.cpp index d1fa5d8627..c230924ac8 100644 --- a/src/libslic3r/Support/SupportMaterial.cpp +++ b/src/libslic3r/Support/SupportMaterial.cpp @@ -1392,6 +1392,10 @@ static inline ExPolygons detect_overhangs( const double threshold_rad = Geometry::deg2rad(thresh_angle); const bool bridge_no_support = object_config.bridge_no_support.value; const coordf_t xy_expansion = scale_(object_config.support_expansion.value); + // Build plate tilt: compute per-layer XY shift for tilted gravity direction + const double tilt_x_rad = Geometry::deg2rad(print_config.build_plate_tilt_x.value); + const double tilt_y_rad = Geometry::deg2rad(print_config.build_plate_tilt_y.value); + const bool has_tilt = std::abs(tilt_x_rad) > EPSILON || std::abs(tilt_y_rad) > EPSILON; float lower_layer_offset = 0; if (layer_id == 0) @@ -1441,9 +1445,17 @@ static inline ExPolygons detect_overhangs( // Overhang polygons for this layer and region. Polygons diff_polygons; Polygons layerm_polygons = to_polygons(layerm->slices.surfaces); + // Apply build plate tilt: shift lower layer polygons to simulate tilted gravity + Polygons effective_lower = lower_layer_polygons; + if (has_tilt) { + const double lh = lower_layer.height; + Point tilt_shift(coord_t(scale_(lh * tan(tilt_y_rad))), + coord_t(scale_(lh * tan(tilt_x_rad)))); + translate(effective_lower, tilt_shift); + } if (lower_layer_offset == 0.f) { // Support everything. - diff_polygons = diff(layerm_polygons, lower_layer_polygons); + diff_polygons = diff(layerm_polygons, effective_lower); if (buildplate_only) { // Don't support overhangs above the top surfaces. // This step is done before the contact surface is calculated by growing the overhang region. @@ -1452,9 +1464,9 @@ static inline ExPolygons detect_overhangs( } else if (auto_normal_support) { // Get the regions needing a suport, collapse very tiny spots. //FIXME cache the lower layer offset if this layer has multiple regions. - diff_polygons = + diff_polygons = diff(layerm_polygons, - expand(lower_layer_polygons, lower_layer_offset, SUPPORT_SURFACES_OFFSET_PARAMETERS)); + expand(effective_lower, lower_layer_offset, SUPPORT_SURFACES_OFFSET_PARAMETERS)); if (buildplate_only && ! annotations.buildplate_covered[layer_id].empty()) { // Don't support overhangs above the top surfaces. // This step is done before the contact surface is calculated by growing the overhang region. @@ -1462,9 +1474,9 @@ static inline ExPolygons detect_overhangs( } if (! diff_polygons.empty()) { // Offset the support regions back to a full overhang, restrict them to the full overhang. - // This is done to increase size of the supporting columns below, as they are calculated by + // This is done to increase size of the supporting columns below, as they are calculated by // propagating these contact surfaces downwards. - diff_polygons = diff(intersection(expand(diff_polygons, lower_layer_offset, SUPPORT_SURFACES_OFFSET_PARAMETERS), layerm_polygons), lower_layer_polygons); + diff_polygons = diff(intersection(expand(diff_polygons, lower_layer_offset, SUPPORT_SURFACES_OFFSET_PARAMETERS), layerm_polygons), effective_lower); } //FIXME add user defined filtering here based on minimal area or minimum radius or whatever. diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 50272009cd..864a0eb0e0 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -671,6 +671,11 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) double thresh_angle = config.support_threshold_angle.value > EPSILON ? config.support_threshold_angle.value + 1 : 30; thresh_angle = std::min(thresh_angle, 89.); // should be smaller than 90 const double threshold_rad = Geometry::deg2rad(thresh_angle); + // Build plate tilt: compute per-layer XY shift for tilted gravity direction + const PrintConfig& print_cfg = m_object->print()->config(); + const double tilt_x_rad = Geometry::deg2rad(print_cfg.build_plate_tilt_x.value); + const double tilt_y_rad = Geometry::deg2rad(print_cfg.build_plate_tilt_y.value); + const bool has_tilt = std::abs(tilt_x_rad) > EPSILON || std::abs(tilt_y_rad) > EPSILON; // FIXME this is a fudge constant! double support_tree_tip_diameter = 0.8; auto enforcer_overhang_offset = scaled(support_tree_tip_diameter); @@ -813,8 +818,19 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) ExPolygons& curr_polys = layer->lslices_extrudable; ExPolygons& lower_polys = lower_layer->lslices_extrudable; + // Apply build plate tilt: shift lower layer polygons to simulate tilted gravity + ExPolygons shifted_lower; + if (has_tilt) { + shifted_lower = lower_polys; // copy + const double lh = lower_layer->height; + Point tilt_shift(coord_t(scale_(lh * tan(tilt_y_rad))), + coord_t(scale_(lh * tan(tilt_x_rad)))); + translate(shifted_lower, tilt_shift); + } + const ExPolygons &effective_lower = has_tilt ? shifted_lower : lower_polys; + // normal overhang - ExPolygons lower_layer_offseted = offset_ex(lower_polys, support_offset_scaled, SUPPORT_SURFACES_OFFSET_PARAMETERS); + ExPolygons lower_layer_offseted = offset_ex(effective_lower, support_offset_scaled, SUPPORT_SURFACES_OFFSET_PARAMETERS); overhangs_all_layers[layer_nr] = std::move(diff_ex(curr_polys, lower_layer_offseted)); double duration{ std::chrono::duration_cast(clock_::now() - t0).count() }; diff --git a/src/libslic3r/Support/TreeSupport3D.cpp b/src/libslic3r/Support/TreeSupport3D.cpp index 307e4d314d..2c24b933b2 100644 --- a/src/libslic3r/Support/TreeSupport3D.cpp +++ b/src/libslic3r/Support/TreeSupport3D.cpp @@ -208,6 +208,10 @@ static std::vector>> group_me const bool support_threshold_auto = support_threshold == 0; // +1 makes the threshold inclusive double tan_threshold = support_threshold_auto ? 0. : tan(M_PI * double(support_threshold + 1) / 180.); + // Build plate tilt: compute per-layer XY shift for tilted gravity direction + const double tilt_x_rad = Geometry::deg2rad(print_config.build_plate_tilt_x.value); + const double tilt_y_rad = Geometry::deg2rad(print_config.build_plate_tilt_y.value); + const bool has_tilt = std::abs(tilt_x_rad) > EPSILON || std::abs(tilt_y_rad) > EPSILON; //FIXME this is a fudge constant! auto enforcer_overhang_offset = scaled(config.tree_support_tip_diameter.value); const coordf_t radius_sample_resolution = g_config_tree_support_collision_resolution; @@ -229,7 +233,7 @@ static std::vector>> group_me size_t num_overhang_layers = support_auto ? num_object_layers : std::min(num_object_layers, std::max(size_t(support_enforce_layers), enforcers_layers.size())); tbb::parallel_for(tbb::blocked_range(1, num_overhang_layers), [&print_object, &config, &print_config, &enforcers_layers, &blockers_layers, - support_auto, support_enforce_layers, support_threshold_auto, tan_threshold, enforcer_overhang_offset, num_raft_layers, radius_sample_resolution, &throw_on_cancel, &out] + support_auto, support_enforce_layers, support_threshold_auto, tan_threshold, enforcer_overhang_offset, num_raft_layers, radius_sample_resolution, has_tilt, tilt_x_rad, tilt_y_rad, &throw_on_cancel, &out] (const tbb::blocked_range &range) { for (LayerIndex layer_id = range.begin(); layer_id < range.end(); ++ layer_id) { const Layer ¤t_layer = *print_object.get_layer(layer_id); @@ -253,7 +257,15 @@ static std::vector>> group_me lower_layer_offset = external_perimeter_width - float(scale_(config.support_threshold_overlap.get_abs_value(unscale_(external_perimeter_width)))); } else lower_layer_offset = scaled(lower_layer.height / tan_threshold); - Polygons lower_layer_offseted = offset(lower_layer.lslices_extrudable, lower_layer_offset); + // Apply build plate tilt: shift lower layer polygons to simulate tilted gravity + Polygons lower_src = to_polygons(lower_layer.lslices_extrudable); + if (has_tilt) { + const double lh = lower_layer.height; + Point tilt_shift(coord_t(scale_(lh * tan(tilt_y_rad))), + coord_t(scale_(lh * tan(tilt_x_rad)))); + translate(lower_src, tilt_shift); + } + Polygons lower_layer_offseted = offset(lower_src, lower_layer_offset); overhangs = diff(current_layer.lslices_extrudable, lower_layer_offseted); if (lower_layer_offset == 0) { raw_overhangs = overhangs; diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp index a6d19f505c..695ac4c2ba 100644 --- a/src/libslic3r/TriangleSelector.cpp +++ b/src/libslic3r/TriangleSelector.cpp @@ -233,7 +233,7 @@ int TriangleSelector::select_unsplit_triangle(const Vec3f &hit, int facet_idx) c return this->select_unsplit_triangle(hit, facet_idx, neighbors); } -void TriangleSelector::select_patch(int facet_start, std::unique_ptr &&cursor, EnforcerBlockerType new_state, const Transform3d& trafo_no_translate, bool triangle_splitting, float highlight_by_angle_deg) +void TriangleSelector::select_patch(int facet_start, std::unique_ptr &&cursor, EnforcerBlockerType new_state, const Transform3d& trafo_no_translate, bool triangle_splitting, float highlight_by_angle_deg, const Vec3f &up_direction) { assert(facet_start < m_orig_size_indices); @@ -294,8 +294,8 @@ void TriangleSelector::select_patch(int facet_start, std::unique_ptr &&c int facet = facets_to_check[facet_idx]; const Vec3f& facet_normal = m_face_normals[m_triangles[facet].source_triangle]; Matrix3f normal_matrix = static_cast(trafo_no_translate.matrix().block(0, 0, 3, 3).inverse().transpose().cast()); - float world_normal_z = (normal_matrix* facet_normal).normalized().z(); - if (!visited[facet] && (highlight_by_angle_deg == 0.f || world_normal_z < highlight_angle_limit)) { + float world_normal_dot = (normal_matrix * facet_normal).normalized().dot(up_direction); + if (!visited[facet] && (highlight_by_angle_deg == 0.f || world_normal_dot < highlight_angle_limit)) { if (select_triangle(facet, new_state, triangle_splitting)) { // add neighboring facets to list to be processed later for (int neighbor_idx : m_neighbors[facet]) @@ -320,7 +320,7 @@ bool TriangleSelector::is_facet_clipped(int facet_idx, const ClippingPlane &clp) void TriangleSelector::seed_fill_select_triangles(const Vec3f &hit, int facet_start, const Transform3d& trafo_no_translate, const ClippingPlane &clp, float seed_fill_angle, float highlight_by_angle_deg, - bool force_reselection) + bool force_reselection, const Vec3f &up_direction) { assert(facet_start < m_orig_size_indices); @@ -344,8 +344,8 @@ void TriangleSelector::seed_fill_select_triangles(const Vec3f &hit, int facet_st const Vec3f &facet_normal = m_face_normals[m_triangles[current_facet].source_triangle]; Matrix3f normal_matrix = static_cast(trafo_no_translate.matrix().block(0, 0, 3, 3).inverse().transpose().cast()); - float world_normal_z = (normal_matrix * facet_normal).normalized().z(); - if (!visited[current_facet] && (highlight_by_angle_deg == 0.f || world_normal_z < highlight_angle_limit)) { + float world_normal_dot = (normal_matrix * facet_normal).normalized().dot(up_direction); + if (!visited[current_facet] && (highlight_by_angle_deg == 0.f || world_normal_dot < highlight_angle_limit)) { if (m_triangles[current_facet].is_split()) { for (int split_triangle_idx = 0; split_triangle_idx <= m_triangles[current_facet].number_of_split_sides(); ++split_triangle_idx) { assert(split_triangle_idx < int(m_triangles[current_facet].children.size())); diff --git a/src/libslic3r/TriangleSelector.hpp b/src/libslic3r/TriangleSelector.hpp index 50bbdd4ed0..5b20647b84 100644 --- a/src/libslic3r/TriangleSelector.hpp +++ b/src/libslic3r/TriangleSelector.hpp @@ -308,7 +308,8 @@ public: EnforcerBlockerType new_state, // enforcer or blocker? const Transform3d &trafo_no_translate, // matrix to get from mesh to world without translation bool triangle_splitting, // If triangles will be split base on the cursor or not - float highlight_by_angle_deg = 0.f); // The maximal angle of overhang. If it is set to a non-zero value, it is possible to paint only the triangles of overhang defined by this angle in degrees. + float highlight_by_angle_deg = 0.f, // The maximal angle of overhang. If it is set to a non-zero value, it is possible to paint only the triangles of overhang defined by this angle in degrees. + const Vec3f &up_direction = Vec3f::UnitZ()); // Up direction for overhang detection (accounts for build plate tilt) void seed_fill_select_triangles(const Vec3f &hit, // point where to start int facet_start, // facet of the original mesh (unsplit) that the hit point belongs to @@ -316,7 +317,8 @@ public: const ClippingPlane &clp, // Clipping plane to limit painting to not clipped facets only float seed_fill_angle, // the maximal angle between two facets to be painted by the same color float highlight_by_angle_deg = 0.f, // The maximal angle of overhang. If it is set to a non-zero value, it is possible to paint only the triangles of overhang defined by this angle in degrees. - bool force_reselection = false); // force reselection of the triangle mesh even in cases that mouse is pointing on the selected triangle + bool force_reselection = false, // force reselection of the triangle mesh even in cases that mouse is pointing on the selected triangle + const Vec3f &up_direction = Vec3f::UnitZ()); // Up direction for overhang detection (accounts for build plate tilt) void bucket_fill_select_triangles(const Vec3f &hit, // point where to start int facet_start, // facet of the original mesh (unsplit) that the hit point belongs to diff --git a/src/slic3r/GUI/3DBed.cpp b/src/slic3r/GUI/3DBed.cpp index 544a3f8f12..3f5cd306ad 100644 --- a/src/slic3r/GUI/3DBed.cpp +++ b/src/slic3r/GUI/3DBed.cpp @@ -394,6 +394,8 @@ void Bed3D::render_internal(GLCanvas3D& canvas, const Transform3d& view_matrix, case Type::Custom: { render_custom(canvas, view_matrix, projection_matrix, bottom); break; } } + render_gravity_arrow(view_matrix, projection_matrix); + glsafe(::glDisable(GL_DEPTH_TEST)); } @@ -731,6 +733,63 @@ void Bed3D::render_custom(GLCanvas3D& canvas, const Transform3d& view_matrix, co render_texture(bottom, canvas);*/ } +void Bed3D::render_gravity_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix) +{ + const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + double tilt_x_deg = cfg.opt_float("build_plate_tilt_x"); + double tilt_y_deg = cfg.opt_float("build_plate_tilt_y"); + if (tilt_x_deg == 0. && tilt_y_deg == 0.) { + m_gravity_arrow.reset(); + return; + } + + // Gravity direction (matching the slicer's tilt convention) + double tilt_x_rad = Geometry::deg2rad(tilt_x_deg); + double tilt_y_rad = Geometry::deg2rad(tilt_y_deg); + Vec3d gravity_dir = Vec3d(-tan(tilt_y_rad), -tan(tilt_x_rad), -1.0).normalized(); + + // 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)); + } + + // 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)) + Vec3d from = Vec3d::UnitZ(); + Vec3d to = gravity_dir; + double dot = from.dot(to); + Transform3d rot = Transform3d::Identity(); + if (dot < -0.9999) { + // Nearly opposite — rotate 180° around X + rot = Eigen::AngleAxisd(M_PI, Vec3d::UnitX()) * rot; + } else if (dot < 0.9999) { + Vec3d axis = from.cross(to).normalized(); + double angle = std::acos(std::clamp(dot, -1.0, 1.0)); + rot = Eigen::AngleAxisd(angle, axis) * rot; + } + + GLShaderProgram* shader = wxGetApp().get_shader("flat"); + if (shader == nullptr) + return; + + glsafe(::glEnable(GL_DEPTH_TEST)); + shader->start_using(); + + const Camera& camera = wxGetApp().plater()->get_camera(); + Transform3d model_matrix = rot; + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * model_matrix); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + + m_gravity_arrow.set_color({ 1.0f, 0.85f, 0.0f, 1.0f }); // yellow + m_gravity_arrow.render(); + + shader->stop_using(); +} + void Bed3D::render_default(bool bottom, const Transform3d& view_matrix, const Transform3d& projection_matrix) { // m_texture.reset(); diff --git a/src/slic3r/GUI/3DBed.hpp b/src/slic3r/GUI/3DBed.hpp index b791635fd3..b16ead575b 100644 --- a/src/slic3r/GUI/3DBed.hpp +++ b/src/slic3r/GUI/3DBed.hpp @@ -110,6 +110,7 @@ private: //GLTexture m_temp_texture; GLModel m_model; Vec3d m_model_offset{ Vec3d::Zero() }; + GLModel m_gravity_arrow; Axes m_axes; float m_scale_factor{ 1.0f }; @@ -177,7 +178,8 @@ private: void render_model(const Transform3d& view_matrix, const Transform3d& projection_matrix); void render_custom(GLCanvas3D& canvas, const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom); void render_default(bool bottom, const Transform3d& view_matrix, const Transform3d& projection_matrix); - + void render_gravity_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix); + // BBS: remove the bed picking logic // void register_raycasters_for_picking(const GLModel::Geometry& geometry, const Transform3d& trafo); }; diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index e23efc7fbd..5caf6a4e0d 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -1078,13 +1078,27 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type, bool enable_support; int support_threshold_angle = get_selection_support_threshold_angle(enable_support); - + float normal_z = -::cos(Geometry::deg2rad((float) support_threshold_angle)); - + + // Compute up direction accounting for build plate tilt + Vec3f up_direction = Vec3f::UnitZ(); + { + const DynamicPrintConfig& prt_cfg = GUI::wxGetApp().preset_bundle->printers.get_edited_preset().config; + double tilt_x_deg = prt_cfg.opt_float("build_plate_tilt_x"); + double tilt_y_deg = prt_cfg.opt_float("build_plate_tilt_y"); + if (tilt_x_deg != 0. || tilt_y_deg != 0.) { + double tilt_x_rad = Geometry::deg2rad(tilt_x_deg); + double tilt_y_rad = Geometry::deg2rad(tilt_y_deg); + up_direction = Vec3f(float(tan(tilt_y_rad)), float(tan(tilt_x_rad)), 1.f).normalized(); + } + } + shader->set_uniform("volume_world_matrix", volume.first->world_matrix()); shader->set_uniform("slope.actived", m_slope.isGlobalActive && !volume.first->is_modifier && !volume.first->is_wipe_tower); shader->set_uniform("slope.volume_world_normal_matrix", static_cast(volume.first->world_matrix().matrix().block(0, 0, 3, 3).inverse().transpose().cast())); shader->set_uniform("slope.normal_z", normal_z); + shader->set_uniform("slope.up_direction", up_direction); #if ENABLE_ENVIRONMENT_MAP unsigned int environment_texture_id = GUI::wxGetApp().plater()->get_environment_texture_id(); diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index 9486bed67a..47e6246509 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -64,6 +64,7 @@ static SettingsFactory::Bundle FREQ_SETTINGS_BUNDLE_FFF = { L("Support") , { "enable_support", "support_type", "support_threshold_angle", "support_threshold_overlap", "support_base_pattern", "support_on_build_plate_only","support_critical_regions_only", "support_remove_small_overhang", + "build_plate_tilt_x", "build_plate_tilt_y", "support_base_pattern_spacing", "support_expansion"}}, //BBS { L("Flush options") , { "flush_into_infill", "flush_into_objects", "flush_into_support"} } @@ -94,7 +95,8 @@ std::map> SettingsFactory::OBJECT_C {"support_bottom_z_distance", "",19},{"support_top_z_distance", "",20},{"support_base_pattern", "",21},{"support_base_pattern_spacing", "",22}, {"support_interface_top_layers", "",23},{"support_interface_bottom_layers", "",24},{"support_interface_spacing", "",25},{"support_bottom_interface_spacing", "",26}, {"support_object_xy_distance", "",27}, {"bridge_no_support", "",28},{"max_bridge_length", "",29},{"support_critical_regions_only", "",30},{"support_remove_small_overhang","",31}, - {"support_object_first_layer_gap","",32} + {"build_plate_tilt_x","",32},{"build_plate_tilt_y","",33}, + {"support_object_first_layer_gap","",34} }}, { L("Speed"), {{"support_speed", "",12}, {"support_interface_speed", "",13} }} diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp index 02f1b4be5f..88781f6006 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp @@ -537,6 +537,14 @@ int GLGizmoFdmSupports::get_selection_support_threshold_angle() return auto_support ? support_threshold_angle : 0; } +std::pair GLGizmoFdmSupports::get_build_plate_tilt() +{ + const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + double tilt_x = cfg.opt_float("build_plate_tilt_x"); + double tilt_y = cfg.opt_float("build_plate_tilt_y"); + return {tilt_x, tilt_y}; +} + void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block) { float threshold = (float(M_PI)/180.f)*threshold_deg; @@ -544,6 +552,12 @@ void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block) const ModelObject* mo = m_c->selection_info()->model_object(); const ModelInstance* mi = mo->instances[selection.get_instance_idx()]; + // Compute gravity direction accounting for build plate tilt + auto [tilt_x_deg, tilt_y_deg] = get_build_plate_tilt(); + double tilt_x_rad = tilt_x_deg * M_PI / 180.0; + double tilt_y_rad = tilt_y_deg * M_PI / 180.0; + Vec3d gravity_dir = Vec3d(-tan(tilt_y_rad), -tan(tilt_x_rad), -1.0).normalized(); + int mesh_id = -1; for (const ModelVolume* mv : mo->volumes) { if (! mv->is_model_part()) @@ -552,10 +566,8 @@ void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block) ++mesh_id; const Transform3d trafo_matrix = mi->get_matrix_no_offset() * mv->get_matrix_no_offset(); - Vec3f down = (trafo_matrix.inverse() * (-Vec3d::UnitZ())).cast().normalized(); - Vec3f limit = (trafo_matrix.inverse() * Vec3d(std::sin(threshold), 0, -std::cos(threshold))).cast().normalized(); - - float dot_limit = limit.dot(down); + Vec3f down = (trafo_matrix.inverse() * gravity_dir).cast().normalized(); + float dot_limit = std::cos(threshold); // Now calculate dot product of vert_direction and facets' normals. int idx = 0; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp index ec0faeab80..0afdba7f19 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp @@ -59,6 +59,7 @@ private: void select_facets_by_angle(float threshold, bool block); // BBS int get_selection_support_threshold_angle(); + std::pair get_build_plate_tilt(); int m_support_threshold_angle = -1; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp index 7d3f40bd12..71e4c45f4a 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp @@ -73,6 +73,18 @@ GLGizmoPainterBase::ClippingPlaneDataWrapper GLGizmoPainterBase::get_clipping_pl return clp_data_out; } +Vec3f GLGizmoPainterBase::get_tilt_up_direction() const +{ + const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + double tilt_x_deg = cfg.opt_float("build_plate_tilt_x"); + double tilt_y_deg = cfg.opt_float("build_plate_tilt_y"); + if (tilt_x_deg == 0. && tilt_y_deg == 0.) + return Vec3f::UnitZ(); + double tilt_x_rad = Geometry::deg2rad(tilt_x_deg); + double tilt_y_rad = Geometry::deg2rad(tilt_y_deg); + return Vec3f(float(tan(tilt_y_rad)), float(tan(tilt_x_rad)), 1.f).normalized(); +} + void GLGizmoPainterBase::render_triangles(const Selection& selection) const { auto* shader = wxGetApp().get_shader("mm_gouraud"); @@ -119,11 +131,15 @@ void GLGizmoPainterBase::render_triangles(const Selection& selection) const float normal_z = -::cos(Geometry::deg2rad(m_highlight_by_angle_threshold_deg)); Matrix3f normal_matrix = static_cast(trafo_matrix.matrix().block(0, 0, 3, 3).inverse().transpose().cast()); + // Compute up direction accounting for build plate tilt + Vec3f up_direction = get_tilt_up_direction(); + shader->set_uniform("volume_world_matrix", trafo_matrix); shader->set_uniform("volume_mirrored", is_left_handed); shader->set_uniform("slope.actived", m_parent.is_using_slope()); shader->set_uniform("slope.volume_world_normal_matrix", normal_matrix); shader->set_uniform("slope.normal_z", normal_z); + shader->set_uniform("slope.up_direction", up_direction); m_triangle_selectors[mesh_id]->render(m_imgui, trafo_matrix); if (is_left_handed) @@ -691,7 +707,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous mi->get_assemble_transformation().get_matrix() * mo->volumes[m_rr.mesh_id]->get_matrix() : mi->get_transformation().get_matrix() * mo->volumes[m_rr.mesh_id]->get_matrix(); m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, int(m_rr.facet), trafo_matrix_not_translate, this->get_clipping_plane_in_volume_coordinates(trafo_matrix), m_smart_fill_angle, - m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, true); + m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, true, get_tilt_up_direction()); m_triangle_selectors[m_rr.mesh_id]->request_update_render_data(); m_seed_fill_last_mesh_id = m_rr.mesh_id; } @@ -803,7 +819,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous std::unique_ptr cursor = TriangleSelector::SinglePointCursor::cursor_factory(phr.z_world, camera_pos, m_cursor_height, trafo_matrix, clp); m_triangle_selectors[mesh_idx]->select_patch(int(phr.first_facet_idx), std::move(cursor), new_state, trafo_matrix_not_translate, - m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f); + m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, get_tilt_up_direction()); m_triangle_selectors[mesh_idx]->request_update_render_data(true); m_last_mouse_click = _mouse_position; @@ -855,7 +871,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous m_triangle_selectors[mesh_idx]->seed_fill_apply_on_triangles(new_state); if (m_tool_type == ToolType::SMART_FILL) m_triangle_selectors[mesh_idx]->seed_fill_select_triangles(mesh_hit, facet_idx, trafo_matrix_not_translate, clp, m_smart_fill_angle, - m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, true); + m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, true, get_tilt_up_direction()); else if (m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER) // BBS: add infill_angle parameter m_triangle_selectors[mesh_idx]->bucket_fill_select_triangles(mesh_hit, facet_idx, clp, -1.f, false, true); @@ -874,12 +890,12 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous camera_pos, m_cursor_radius, m_cursor_type, trafo_matrix, clp); m_triangle_selectors[mesh_idx]->select_patch(int(first_position.facet_idx), std::move(cursor), new_state, trafo_matrix_not_translate, - m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f); + m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, get_tilt_up_direction()); } else { for (auto first_position_it = projected_mouse_positions.cbegin(); first_position_it != projected_mouse_positions.cend() - 1; ++first_position_it) { auto second_position_it = first_position_it + 1; std::unique_ptr cursor = TriangleSelector::DoublePointCursor::cursor_factory(first_position_it->mesh_hit, second_position_it->mesh_hit, camera_pos, m_cursor_radius, m_cursor_type, trafo_matrix, clp); - m_triangle_selectors[mesh_idx]->select_patch(int(first_position_it->facet_idx), std::move(cursor), new_state, trafo_matrix_not_translate, m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f); + m_triangle_selectors[mesh_idx]->select_patch(int(first_position_it->facet_idx), std::move(cursor), new_state, trafo_matrix_not_translate, m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, get_tilt_up_direction()); } } } @@ -953,7 +969,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous const TriangleSelector::ClippingPlane &clp = this->get_clipping_plane_in_volume_coordinates(trafo_matrix); if (m_tool_type == ToolType::SMART_FILL) m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, int(m_rr.facet), trafo_matrix_not_translate, clp, m_smart_fill_angle, - m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f); + m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, false, get_tilt_up_direction()); else if (m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER) // BBS: add infill_angle parameter m_triangle_selectors[m_rr.mesh_id]->bucket_fill_select_triangles(m_rr.hit, int(m_rr.facet), clp, -1.f, false); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp index 5fc1a9e18f..9dbed7df7e 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp @@ -281,6 +281,9 @@ protected: bool m_paint_on_overhangs_only = false; float m_highlight_by_angle_threshold_deg = 0.f; + // Returns the up direction accounting for build plate tilt (default: UnitZ) + Vec3f get_tilt_up_direction() const; + GLModel m_circle; Vec2d m_old_center{ Vec2d::Zero() }; float m_old_cursor_radius{ 0.0f }; diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index c94fd1c7a4..6ac8ca9a0e 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4364,6 +4364,8 @@ void TabPrinter::build_fff() optgroup->append_single_option_line(option, "printer_basic_information_printable_space#excluded-bed-area"); // optgroup->append_single_option_line("printable_area"); optgroup->append_single_option_line("printable_height", "printer_basic_information_printable_space#printable-height"); + optgroup->append_single_option_line("build_plate_tilt_x"); + optgroup->append_single_option_line("build_plate_tilt_y"); optgroup->append_single_option_line("support_multi_bed_types","printer_basic_information_printable_space#support-multi-bed-types"); optgroup->append_single_option_line("best_object_pos", "printer_basic_information_printable_space#best-object-position"); // todo: for multi_extruder test From ed6ea086a26fa0b45db5d1faaedea01a5e190753 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 11 Mar 2026 03:58:52 -0500 Subject: [PATCH 002/224] Add belt printer transform pipeline: slicing rotation, G-code coords, preview - Implement core belt slicing pipeline: R(-alpha, X) mesh rotation in PrintObjectSlice with corrected object height calculation for proper layer count Add to_machine_coords() in GCodeWriter to convert slicing-frame coordinates back to machine-frame, propagated through GCode, GCodeProcessor, and GCodeViewer Add belt-mode UI: tilted bed visualization, slicing-direction arrow, and raw G-code toggle to switch between machine-frame and slicing-frame views This is a combination of 6 commits. checkpoint 1: initial MVP. Slicing functions, but rotates instead of skews are happening and a lot of other stuff too getting somewhere, getting to the point where I need to figure out how to verify this stuff this appears to be a dead end. getting somewhere I think maybe I'm pretty sure we've completely lost the plot at this point and need to restart this process... remove slice logic in preparation for new, more invasive plan --- result.json | 7 ++ src/libslic3r/Brim.cpp | 4 ++ src/libslic3r/BuildVolume.cpp | 25 +++++++ src/libslic3r/BuildVolume.hpp | 8 +++ src/libslic3r/GCode.cpp | 9 ++- src/libslic3r/GCode/GCodeProcessor.cpp | 10 +++ src/libslic3r/GCode/GCodeProcessor.hpp | 2 + src/libslic3r/GCodeWriter.cpp | 91 +++++++++++++++++++++----- src/libslic3r/GCodeWriter.hpp | 11 ++++ src/libslic3r/Preset.cpp | 2 +- src/libslic3r/Print.cpp | 20 ++++++ src/libslic3r/PrintConfig.cpp | 50 ++++++++++++-- src/libslic3r/PrintConfig.hpp | 4 ++ src/libslic3r/PrintObject.cpp | 5 +- src/libslic3r/PrintObjectSlice.cpp | 2 + src/slic3r/GUI/3DBed.cpp | 67 ++++++++++++++++++- src/slic3r/GUI/3DBed.hpp | 11 ++++ src/slic3r/GUI/ConfigManipulation.cpp | 25 +++++-- src/slic3r/GUI/GCodeViewer.cpp | 4 +- src/slic3r/GUI/GCodeViewer.hpp | 6 ++ src/slic3r/GUI/Plater.cpp | 18 +++++ src/slic3r/GUI/Tab.cpp | 25 +++++++ 22 files changed, 375 insertions(+), 31 deletions(-) create mode 100644 result.json diff --git a/result.json b/result.json new file mode 100644 index 0000000000..e309ad45a1 --- /dev/null +++ b/result.json @@ -0,0 +1,7 @@ +{ + "error_string": "Success.", + "export_time": 0, + "plate_index": 0, + "prepare_time": 0, + "return_code": 0 +} diff --git a/src/libslic3r/Brim.cpp b/src/libslic3r/Brim.cpp index 35d971a465..0534734c65 100644 --- a/src/libslic3r/Brim.cpp +++ b/src/libslic3r/Brim.cpp @@ -881,6 +881,10 @@ void make_brim(const Print& print, PrintTryCancel try_cancel, Polygons& islands_ std::vector> &objPrintVec, std::vector& printExtruders) { + // Belt printer: brim is not compatible with belt printing. + if (print.config().belt_printer.value) + return; + std::map brim_width_map; std::map brimAreaMap; std::map supportBrimAreaMap; diff --git a/src/libslic3r/BuildVolume.cpp b/src/libslic3r/BuildVolume.cpp index 3802fbc014..132794ccfe 100644 --- a/src/libslic3r/BuildVolume.cpp +++ b/src/libslic3r/BuildVolume.cpp @@ -176,6 +176,26 @@ BuildVolume::BuildVolume(const std::vector &printable_area, const double BOOST_LOG_TRIVIAL(debug) << "BuildVolume printable_area clasified as: " << this->type_name(); } +void BuildVolume::set_belt_printer(bool enabled, double angle_deg, bool infinite_y) +{ + m_is_belt_printer = enabled; + m_belt_angle = angle_deg; + m_belt_infinite_y = infinite_y; + + if (enabled) { + if (infinite_y) { + // Extend the Y bound to a very large value for infinite belt. + m_bboxf.max.y() = 100000.; + } + // Adjust max print height to diagonal reach: printable_height / sin(belt_angle). + if (angle_deg > 0. && angle_deg < 90.) { + double sin_a = std::sin(angle_deg * M_PI / 180.0); + if (sin_a > 0.) + m_bboxf.max.z() = m_max_print_height / sin_a; + } + } +} + #if 0 // Tests intersections of projected triangles, not just their vertices against a bounding box. // This test also correctly evaluates collision of a non-convex object with the bounding box. @@ -384,6 +404,11 @@ BuildVolume::ObjectState BuildVolume::object_state(const indexed_triangle_set& i build_volume.max.z() = std::numeric_limits::max(); if (ignore_bottom) build_volume.min.z() = -std::numeric_limits::max(); + // Belt printer: extend Y bounds for infinite Y. + if (m_is_belt_printer && m_belt_infinite_y) { + build_volume.min.y() = -std::numeric_limits::max(); + build_volume.max.y() = std::numeric_limits::max(); + } BoundingBox3Base build_volumef(build_volume.min.cast(), build_volume.max.cast()); // The following test correctly interprets intersection of a non-convex object with a rectangular build volume. //return rectangle_test(its, trafo, to_2d(build_volume.min), to_2d(build_volume.max), build_volume.max.z()); diff --git a/src/libslic3r/BuildVolume.hpp b/src/libslic3r/BuildVolume.hpp index 6f53fdd704..0494efe9ba 100644 --- a/src/libslic3r/BuildVolume.hpp +++ b/src/libslic3r/BuildVolume.hpp @@ -57,6 +57,10 @@ public: // Initialize from PrintConfig::printable_area and PrintConfig::printable_height BuildVolume(const std::vector &printable_area, const double printable_height, const std::vector> &extruder_areas, const std::vector& extruder_printable_heights); + // Belt printer configuration. + void set_belt_printer(bool enabled, double angle_deg, bool infinite_y); + bool is_belt_printer() const { return m_is_belt_printer; } + // Source data, unscaled coordinates. const std::vector& printable_area() const { return m_bed_shape; } double printable_height() const { return m_max_print_height; } @@ -139,6 +143,10 @@ private: // Source definition of the print volume height (PrintConfig::printable_height) double m_max_print_height { 0.f }; std::vector m_extruder_printable_height; + // Belt printer state. + bool m_is_belt_printer { false }; + double m_belt_angle { 0. }; + bool m_belt_infinite_y { false }; // Derived values. BuildVolume_Type m_type { BuildVolume_Type::Invalid }; diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index fe10cbcd41..bff35d321c 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2413,6 +2413,10 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_writer.set_is_bbl_machine(is_bbl_printers); + // Belt printer: initialize coordinate transformation on the writer. + if (print.config().belt_printer.value) + m_writer.set_belt_angle(print.config().belt_printer_angle.value); + // How many times will be change_layer() called? // change_layer() in turn increments the progress bar status. m_layer_count = 0; @@ -2502,6 +2506,9 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.write_format("; HEADER_BLOCK_START\n"); // Write information on the generator. file.write_format("; generated by %s on %s\n", Slic3r::header_slic3r_generated().c_str(), Slic3r::Utils::local_timestamp().c_str()); + // Belt printer: embed angle in header for G-code processor detection. + if (print.config().belt_printer.value) + file.write_format("; belt_printer_angle = %.1f\n", print.config().belt_printer_angle.value); if (is_bbl_printers) file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str()); //BBS: total layer number @@ -6704,7 +6711,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } // BBS: use G1 if not enable arc fitting or has no arc fitting result or in spiral_mode mode or we are doing sloped extrusion // Attention: G2 and G3 is not supported in spiral_mode mode - if (!m_config.enable_arc_fitting || path.polyline.fitting_result.empty() || m_config.spiral_mode || sloped != nullptr) { + if (!m_config.enable_arc_fitting || path.polyline.fitting_result.empty() || m_config.spiral_mode || sloped != nullptr || m_config.belt_printer) { double path_length = 0.; double total_length = sloped == nullptr ? 0. : path.polyline.length() * SCALING_FACTOR; for (const Line& line : path.polyline.lines()) { diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 906c2070e4..0dc00e1522 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -2597,6 +2597,8 @@ void GCodeProcessor::finalize(bool post_process) } } + // Belt printer: preview coordinate transform placeholder (to be implemented in next cycle). + calculate_time(m_result); // process the time blocks @@ -3044,6 +3046,14 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers return; } + // Belt printer angle detection from G-code header comment. + if (boost::starts_with(comment, " belt_printer_angle = ")) { + try { + m_result.belt_printer_angle = std::stof(std::string(comment.substr(22))); + } catch (...) {} + return; + } + // wipe start tag if (boost::starts_with(comment, reserved_tag(ETags::Wipe_Start))) { m_wiping = true; diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index 1d72b1c614..e432a165a8 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -227,6 +227,8 @@ class Print; bool support_traditional_timelapse{true}; float printable_height; float z_offset; + // Belt printer: angle for coordinate transformation in preview. + float belt_printer_angle{ 0.f }; SettingsIds settings_ids; size_t filaments_count; bool backtrace_enabled; diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index 1aff72daeb..1b056a946f 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -1,5 +1,6 @@ #include "GCodeWriter.hpp" #include "CustomGCode.hpp" +#include "Geometry.hpp" #include "PrintConfig.hpp" #include #include @@ -19,6 +20,19 @@ namespace Slic3r { bool GCodeWriter::full_gcode_comment = true; +void GCodeWriter::set_belt_angle(double angle_deg) +{ + m_belt_angle_rad = Geometry::deg2rad(angle_deg); + m_belt_cos = std::cos(m_belt_angle_rad); + m_belt_sin = std::sin(m_belt_angle_rad); +} + +Vec3d GCodeWriter::to_machine_coords(const Vec3d &pos) const +{ + // Belt printer: coordinate transform placeholder (to be implemented in next cycle). + return pos; +} + bool GCodeWriter::supports_separate_travel_acceleration(GCodeFlavor flavor) { return (flavor == gcfRepetier || flavor == gcfMarlinFirmware || flavor == gcfRepRapFirmware); @@ -547,7 +561,13 @@ std::string GCodeWriter::travel_to_xy(const Vec2d &point, const std::string &com Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; GCodeG1Formatter w; - w.emit_xy(point_on_plate); + if (is_belt_printer()) { + // Belt printer: transform to machine coordinates (XY travel also needs Z due to YZ rotation) + Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); + w.emit_xyz(machine); + } else { + w.emit_xy(point_on_plate); + } auto speed = m_is_first_layer ? this->config.get_abs_value("initial_layer_travel_speed") : this->config.travel_speed.value; w.emit_f(speed * 60.0); @@ -561,6 +581,11 @@ it will not perform subsequent lifts, even if Z was raised manually (i.e. with travel_to_z()) and thus _lifted was reduced. */ std::string GCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase) { + // Belt printer: force NormalLift since SpiralLift and SlopeLift compute slope angles + // that don't account for the YZ coordinate rotation. + if (is_belt_printer()) + lift_type = LiftType::NormalLift; + // check whether the above/below conditions are met double target_lift = 0; { @@ -587,8 +612,10 @@ std::string GCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase) } // BBS: immediately execute an undelayed lift move with a spiral lift pattern -// designed specifically for subsequent gcode injection (e.g. timelapse) +// designed specifically for subsequent gcode injection (e.g. timelapse) std::string GCodeWriter::eager_lift(const LiftType type) { + // Belt printer: force NormalLift (SpiralLift/SlopeLift don't account for YZ rotation). + const LiftType effective_type = is_belt_printer() ? LiftType::NormalLift : type; std::string lift_move; double target_lift = 0; { @@ -603,7 +630,7 @@ std::string GCodeWriter::eager_lift(const LiftType type) { // BBS: spiral lift only safe with known position // TODO: check the arc will move within bed area - if (type == LiftType::SpiralLift && this->is_current_position_clear()) { + if (effective_type == LiftType::SpiralLift && this->is_current_position_clear()) { double radius = target_lift / (2 * PI * atan(filament()->travel_slope())); // static spiral alignment when no move in x,y plane. // spiral centra is a radius distance to the right (y=0) @@ -675,6 +702,8 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co // / to make the z list early to avoid to hit some warping place when travel is long. Vec2d temp = delta_no_z.normalized() * delta(2) / tan(this->filament()->travel_slope()); Vec3d slope_top_point = Vec3d(temp(0), temp(1), delta(2)) + source; + if (is_belt_printer()) + slope_top_point = to_machine_coords(slope_top_point); GCodeG1Formatter w0; w0.emit_xyz(slope_top_point); w0.emit_f(travel_speed * 60.0); @@ -689,18 +718,27 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co std::string xy_z_move; { + Vec3d emit_target = is_belt_printer() ? to_machine_coords(target) : target; GCodeG1Formatter w0; if (this->is_current_position_clear()) { - w0.emit_xyz(target); + w0.emit_xyz(emit_target); w0.emit_f(travel_speed * 60.0); w0.emit_comment(GCodeWriter::full_gcode_comment, comment); xy_z_move = w0.string(); } else { - w0.emit_xy(Vec2d(target.x(), target.y())); - w0.emit_f(travel_speed * 60.0); - w0.emit_comment(GCodeWriter::full_gcode_comment, comment); - xy_z_move = w0.string() + _travel_to_z(target.z(), comment); + if (is_belt_printer()) { + // Belt mode: can't split XY and Z moves independently, emit full XYZ + w0.emit_xyz(emit_target); + w0.emit_f(travel_speed * 60.0); + w0.emit_comment(GCodeWriter::full_gcode_comment, comment); + xy_z_move = w0.string(); + } else { + w0.emit_xy(Vec2d(target.x(), target.y())); + w0.emit_f(travel_speed * 60.0); + w0.emit_comment(GCodeWriter::full_gcode_comment, comment); + xy_z_move = w0.string() + _travel_to_z(target.z(), comment); + } } } m_pos = dest_point; @@ -726,15 +764,25 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co //BBS: take plate offset into consider Vec3d point_on_plate = { dest_point(0) - m_x_offset, dest_point(1) - m_y_offset, dest_point(2) }; + if (is_belt_printer()) + point_on_plate = to_machine_coords(point_on_plate); std::string out_string; GCodeG1Formatter w; if (!this->is_current_position_clear()) { - //force to move xy first then z after filament change - w.emit_xy(Vec2d(point_on_plate.x(), point_on_plate.y())); - w.emit_f(this->config.travel_speed.value * 60.0); - w.emit_comment(GCodeWriter::full_gcode_comment, comment); - out_string = w.string() + _travel_to_z(point_on_plate.z(), comment); + if (is_belt_printer()) { + // Belt mode: emit full XYZ since Y and Z are coupled + w.emit_xyz(point_on_plate); + w.emit_f(this->config.travel_speed.value * 60.0); + w.emit_comment(GCodeWriter::full_gcode_comment, comment); + out_string = w.string(); + } else { + //force to move xy first then z after filament change + w.emit_xy(Vec2d(point_on_plate.x(), point_on_plate.y())); + w.emit_f(this->config.travel_speed.value * 60.0); + w.emit_comment(GCodeWriter::full_gcode_comment, comment); + out_string = w.string() + _travel_to_z(point_on_plate.z(), comment); + } } else { GCodeG1Formatter w; w.emit_xyz(point_on_plate); @@ -778,7 +826,13 @@ std::string GCodeWriter::_travel_to_z(double z, const std::string &comment) } GCodeG1Formatter w; - w.emit_z(z); + if (is_belt_printer()) { + // Belt printer: a Z-only move in slicing frame needs to emit both Y and Z in machine coords. + Vec3d machine = to_machine_coords(Vec3d(m_pos.x() - m_x_offset, m_pos.y() - m_y_offset, z)); + w.emit_xyz(machine); + } else { + w.emit_z(z); + } w.emit_f(speed * 60.0); //BBS w.emit_comment(GCodeWriter::full_gcode_comment, comment); @@ -883,7 +937,12 @@ std::string GCodeWriter::extrude_to_xy(const Vec2d &point, double dE, const std: Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; GCodeG1Formatter w; - w.emit_xy(point_on_plate); + if (is_belt_printer()) { + Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); + w.emit_xyz(machine); + } else { + w.emit_xy(point_on_plate); + } if (!force_no_extrusion) w.emit_e(filament()->E()); //BBS @@ -924,6 +983,8 @@ std::string GCodeWriter::extrude_to_xyz(const Vec3d &point, double dE, const std Vec3d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset, point(2) }; GCodeG1Formatter w; + if (is_belt_printer()) + point_on_plate = to_machine_coords(point_on_plate); w.emit_xyz(point_on_plate); if (!force_no_extrusion) w.emit_e(filament()->E()); diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index dbc64694e2..10159d941f 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -125,6 +125,12 @@ public: void set_is_first_layer(bool bval) { m_is_first_layer = bval; } GCodeFlavor get_gcode_flavor() const { return config.gcode_flavor; } + // Belt printer: set the belt angle and precompute sin/cos for coordinate transformation. + void set_belt_angle(double angle_deg); + bool is_belt_printer() const { return m_belt_angle_rad != 0.; } + // Transform a point from the slicing frame to machine/world coordinates (inverse shear). + Vec3d to_machine_coords(const Vec3d &pos) const; + // Returns whether this flavor supports separate print and travel acceleration. static bool supports_separate_travel_acceleration(GCodeFlavor flavor); private: @@ -177,6 +183,11 @@ public: //SoftFever bool m_is_bbl_printers = false; + + // Belt printer coordinate transformation (YZ shear) + double m_belt_angle_rad = 0.; + double m_belt_cos = 1.0; + double m_belt_sin = 0.0; double m_current_speed; bool m_is_first_layer = true; diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index ef2c83b13c..98532bccc7 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1010,7 +1010,7 @@ static std::vector s_Preset_machine_limits_options { static std::vector s_Preset_printer_options { "printer_technology", - "printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "build_plate_tilt_x", "build_plate_tilt_y", "gcode_flavor", + "printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "build_plate_tilt_x", "build_plate_tilt_y", "belt_printer", "belt_printer_angle", "belt_printer_infinite_y", "gcode_flavor", "fan_kickstart", "fan_speedup_time", "fan_speedup_overhangs", "single_extruder_multi_material", "manual_filament_change", "file_start_gcode", "machine_start_gcode", "machine_end_gcode", "before_layer_change_gcode", "printing_by_object_gcode", "layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode", "change_filament_gcode", "change_extrusion_role_gcode", "printer_model", "printer_variant", "printer_extruder_id", "printer_extruder_variant", "extruder_variant_list", "default_nozzle_volume_type", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 6463338f7d..f245cc32b6 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -565,6 +565,9 @@ std::vector Print::print_object_ids() const bool Print::has_infinite_skirt() const { + // Belt printer: no skirt support. + if (m_config.belt_printer.value) + return false; // Orca: unclear why (m_config.ooze_prevention && this->extruders().size() > 1) logic is here, removed. // return (m_config.draft_shield == dsEnabled && m_config.skirt_loops > 0) || (m_config.ooze_prevention && this->extruders().size() > 1); @@ -573,6 +576,9 @@ bool Print::has_infinite_skirt() const bool Print::has_skirt() const { + // Belt printer: no skirt support. + if (m_config.belt_printer.value) + return false; return (m_config.skirt_height > 0); } @@ -1194,6 +1200,16 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* if (extruders.empty()) return { L("No extrusions under current settings.") }; + // Belt printer validation: incompatible features. + if (m_config.belt_printer.value) { + for (const PrintObject *object : m_objects) { + if (object->config().raft_layers > 0) + return { L("Raft is not compatible with belt printer mode.") }; + } + if (m_config.draft_shield != dsDisabled) + return { L("Draft shield is not compatible with belt printer mode.") }; + } + if (nozzles < 2 && extruders.size() > 1) { auto ret = check_multi_filament_valid(*this); if (!ret.string.empty()) @@ -2501,6 +2517,10 @@ std::string Print::export_gcode(const std::string& path_template, GCodeProcessor void Print::_make_skirt() { + // Belt printer: skirt is not compatible. + if (m_config.belt_printer.value) + return; + // First off we need to decide how tall the skirt must be. // The skirt_height option from config is expressed in layers, but our // object might have different layer heights, so we need to find the print_z diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 918fc1b97f..7eea294d4e 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -2,6 +2,7 @@ #include "PrintConfigConstants.hpp" #include "ClipperUtils.hpp" #include "Config.hpp" +#include "Geometry.hpp" #include "MaterialType.hpp" #include "I18N.hpp" #include "format.hpp" @@ -5923,10 +5924,11 @@ void PrintConfigDef::init_fff_params() def->category = L("Support"); def->tooltip = L("Tilt angle of the build plate along the X axis. " "A positive value tilts the plate so the +X side is higher, shifting gravity toward -X and increasing overhangs on the +X side. " - "A negative value tilts the -X side higher. Set to 0 for no X-axis tilt."); + "A negative value tilts the -X side higher. Set to 0 for no X-axis tilt. " + "In belt printer mode, this is automatically synced to the belt angle."); def->sidetext = u8"\u00B0"; - def->min = -45; - def->max = 45; + def->min = -90; + def->max = 90; def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(0.)); @@ -5937,11 +5939,41 @@ void PrintConfigDef::init_fff_params() "A positive value tilts the plate so the +Y side is higher, shifting gravity toward -Y and increasing overhangs on the +Y side. " "A negative value tilts the -Y side higher. Set to 0 for no Y-axis tilt."); def->sidetext = u8"\u00B0"; - def->min = -45; - def->max = 45; + def->min = -90; + def->max = 90; def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(0.)); + def = this->add("belt_printer", coBool); + def->label = L("Belt printer"); + def->category = L("Printable space"); + def->tooltip = L("Enable belt printer mode. Belt printers use a conveyor belt as the build surface, " + "tilted at an angle (typically 45 degrees). The slicer will rotate the slicing plane " + "and transform G-code coordinates for the tilted build surface."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + + def = this->add("belt_printer_angle", coFloat); + def->label = L("Belt angle"); + def->category = L("Printable space"); + def->tooltip = L("The tilt angle of the belt surface in degrees. " + "Most belt printers use a 45-degree angle. " + "This controls the rotation applied to the slicing plane and G-code coordinates."); + def->sidetext = u8"\u00B0"; + def->min = 0; + def->max = 90; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(45.)); + + def = this->add("belt_printer_infinite_y", coBool); + def->label = L("Infinite Y axis"); + def->category = L("Printable space"); + def->tooltip = L("Enable infinite Y axis for belt printers. " + "When enabled, the Y axis build volume limit is effectively removed, " + "allowing objects of any length to be printed along the belt direction."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(true)); + def = this->add("tree_support_branch_angle", coFloat); def->label = L("Tree support branch angle"); def->category = L("Support"); @@ -10823,10 +10855,16 @@ Polygons get_bed_excluded_area(const PrintConfig& cfg) { const Pointfs exclude_area_points = cfg.bed_exclude_area.values; + // Belt printer: project exclusion zone points from belt surface to machine-frame XY. + // On the belt surface, Z=0, so machine_Y = belt_Y * cos(angle). + const bool is_belt = cfg.belt_printer.value; + const double belt_cos = is_belt ? std::cos(Geometry::deg2rad(cfg.belt_printer_angle.value)) : 1.0; + Polygon exclude_poly; for (int i = 0; i < exclude_area_points.size(); i++) { auto pt = exclude_area_points[i]; - exclude_poly.points.emplace_back(scale_(pt.x()), scale_(pt.y())); + double y = is_belt ? pt.y() * belt_cos : pt.y(); + exclude_poly.points.emplace_back(scale_(pt.x()), scale_(y)); } exclude_poly.make_counter_clockwise(); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index c862956544..f36b5d3e93 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1413,6 +1413,10 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( // Build plate tilt for off-axis gravity support generation (printer-level setting). ((ConfigOptionFloat, build_plate_tilt_x)) ((ConfigOptionFloat, build_plate_tilt_y)) + // Belt printer settings (printer-level). + ((ConfigOptionBool, belt_printer)) + ((ConfigOptionFloat, belt_printer_angle)) + ((ConfigOptionBool, belt_printer_infinite_y)) //BBS ((ConfigOptionInts, additional_cooling_fan_speed)) ((ConfigOptionBool, reduce_crossing_wall)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index fc68fce76b..979953fb94 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -3391,7 +3391,9 @@ void PrintObject::update_slicing_parameters() { // Orca: updated function call for XYZ shrinkage compensation if (!m_slicing_params.valid) { - m_slicing_params = SlicingParameters::create_from_config(this->print()->config(), m_config, this->model_object()->max_z(), + coordf_t object_height = this->model_object()->max_z(); + // Belt printer: height adjustment placeholder (to be implemented in next cycle). + m_slicing_params = SlicingParameters::create_from_config(this->print()->config(), m_config, object_height, this->object_extruders(), this->print()->shrinkage_compensation()); } } @@ -3432,6 +3434,7 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full if (object_max_z <= 0.f) object_max_z = (float)model_object.raw_bounding_box().size().z(); + // Belt printer: height adjustment placeholder (to be implemented in next cycle). return SlicingParameters::create_from_config(print_config, object_config, object_max_z, object_extruders, object_shrinkage_compensation); } diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 2d67ac0939..bba4593764 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -8,6 +8,7 @@ #include "Layer.hpp" #include "MultiMaterialSegmentation.hpp" #include "Print.hpp" +#include "Geometry.hpp" //BBS #include "ShortestPath.hpp" #include "libslic3r/Feature/Interlocking/InterlockingGenerator.hpp" @@ -139,6 +140,7 @@ static std::vector slice_volumes_inner( params_base.closing_radius = print_object_config.slice_closing_radius.value; params_base.extra_offset = 0; params_base.trafo = object_trafo; + // Belt printer: mesh transform placeholder (to be implemented in next cycle). //BBS: 0.0025mm is safe enough to simplify the data to speed slicing up for high-resolution model. //Also has on influence on arc fitting which has default resolution 0.0125mm. params_base.resolution = print_config.resolution <= 0.001 ? 0.0f : 0.0025; diff --git a/src/slic3r/GUI/3DBed.cpp b/src/slic3r/GUI/3DBed.cpp index 3f5cd306ad..101279d744 100644 --- a/src/slic3r/GUI/3DBed.cpp +++ b/src/slic3r/GUI/3DBed.cpp @@ -387,14 +387,18 @@ void Bed3D::render_internal(GLCanvas3D& canvas, const Transform3d& view_matrix, m_model.set_color(m_is_dark ? DEFAULT_MODEL_COLOR_DARK : DEFAULT_MODEL_COLOR); + // Belt printer: bed view transform placeholder (to be implemented in next cycle). + Transform3d belt_view_matrix = view_matrix; + switch (m_type) { - case Type::System: { render_system(canvas, view_matrix, projection_matrix, bottom); break; } + case Type::System: { render_system(canvas, belt_view_matrix, projection_matrix, bottom); break; } default: - case Type::Custom: { render_custom(canvas, view_matrix, projection_matrix, bottom); break; } + case Type::Custom: { render_custom(canvas, belt_view_matrix, projection_matrix, bottom); break; } } render_gravity_arrow(view_matrix, projection_matrix); + render_slicing_plane(view_matrix, projection_matrix); glsafe(::glDisable(GL_DEPTH_TEST)); } @@ -738,6 +742,10 @@ void Bed3D::render_gravity_arrow(const Transform3d& view_matrix, const Transform const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; double tilt_x_deg = cfg.opt_float("build_plate_tilt_x"); double tilt_y_deg = cfg.opt_float("build_plate_tilt_y"); + // Belt printer: auto-derive gravity direction from belt angle if belt mode is active. + if (m_is_belt_printer && m_belt_angle > 0.f) { + tilt_x_deg = m_belt_angle; + } if (tilt_x_deg == 0. && tilt_y_deg == 0.) { m_gravity_arrow.reset(); return; @@ -790,6 +798,61 @@ void Bed3D::render_gravity_arrow(const Transform3d& view_matrix, const Transform shader->stop_using(); } +void Bed3D::render_slicing_plane(const Transform3d& view_matrix, const Transform3d& projection_matrix) +{ + if (!m_is_belt_printer || m_belt_angle <= 0.f) + return; + + // Build a quad in the XZ plane (world frame) representing the belt slicing plane. + // The plane is tilted at belt_angle from horizontal, with normal (0, -sin(a), cos(a)). + // We render it as a semi-transparent quad centered on the build plate. + if (!m_slicing_plane.is_initialized()) { + const float half_size = 120.f; // mm, large enough to be visible + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 }; + init_data.reserve_vertices(4); + init_data.reserve_indices(2); // 2 triangles + + // Quad corners in local frame (XY plane, will be rotated to match slicing plane) + Vec3f n = Vec3f::UnitZ(); + init_data.add_vertex(Vec3f(-half_size, -half_size, 0.f), n); + init_data.add_vertex(Vec3f( half_size, -half_size, 0.f), n); + init_data.add_vertex(Vec3f( half_size, half_size, 0.f), n); + init_data.add_vertex(Vec3f(-half_size, half_size, 0.f), n); + init_data.add_triangle(0, 1, 2); + init_data.add_triangle(0, 2, 3); + + m_slicing_plane.init_from(std::move(init_data)); + } + + GLShaderProgram* shader = wxGetApp().get_shader("flat"); + if (shader == nullptr) + return; + + glsafe(::glEnable(GL_DEPTH_TEST)); + glsafe(::glEnable(GL_BLEND)); + glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + + shader->start_using(); + + // Show a tilted plane representing the slicing direction. + // The slicing plane is rotated by belt_angle about X from horizontal. + // Raise it slightly so it's visible above the bed surface. + double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle)); + Transform3d model_matrix = Transform3d::Identity(); + model_matrix.translate(Vec3d(0., 0., 30.)); + model_matrix.rotate(Eigen::AngleAxisd(angle_rad, Vec3d::UnitX())); + + shader->set_uniform("view_model_matrix", view_matrix * model_matrix); + shader->set_uniform("projection_matrix", projection_matrix); + + m_slicing_plane.set_color({ 0.2f, 0.6f, 1.0f, 0.3f }); // semi-transparent blue + m_slicing_plane.render(); + + glsafe(::glDisable(GL_BLEND)); + shader->stop_using(); +} + void Bed3D::render_default(bool bottom, const Transform3d& view_matrix, const Transform3d& projection_matrix) { // m_texture.reset(); diff --git a/src/slic3r/GUI/3DBed.hpp b/src/slic3r/GUI/3DBed.hpp index b16ead575b..005dbff534 100644 --- a/src/slic3r/GUI/3DBed.hpp +++ b/src/slic3r/GUI/3DBed.hpp @@ -111,6 +111,7 @@ private: GLModel m_model; Vec3d m_model_offset{ Vec3d::Zero() }; GLModel m_gravity_arrow; + GLModel m_slicing_plane; // Debug: shows the intended slicing plane direction Axes m_axes; float m_scale_factor{ 1.0f }; @@ -120,6 +121,9 @@ private: std::vector> m_extruder_shapes; std::vector m_extruder_heights; bool m_is_dark = false; + // Belt printer state for rendering. + bool m_is_belt_printer = false; + float m_belt_angle = 0.f; public: Bed3D() = default; @@ -139,6 +143,12 @@ public: // Build volume geometry for various collision detection tasks. const BuildVolume& build_volume() const { return m_build_volume; } + BuildVolume& build_volume() { return m_build_volume; } + + // Belt printer bed settings. + void set_belt_printer(bool enabled, float angle_deg) { m_is_belt_printer = enabled; m_belt_angle = angle_deg; } + bool is_belt_printer() const { return m_is_belt_printer; } + float belt_angle() const { return m_belt_angle; } // Was the model provided, or was it generated procedurally? Type get_type() const { return m_type; } @@ -179,6 +189,7 @@ private: void render_custom(GLCanvas3D& canvas, const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom); void render_default(bool bottom, const Transform3d& view_matrix, const Transform3d& projection_matrix); void render_gravity_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix); + void render_slicing_plane(const Transform3d& view_matrix, const Transform3d& projection_matrix); // BBS: remove the bed picking logic // void register_raycasters_for_picking(const GLModel::Geometry& geometry, const Transform3d& trafo); diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index f3f98a6fad..0b5e6d3906 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -575,9 +575,17 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co auto gcflavor = preset_bundle->printers.get_edited_preset().config.option>("gcode_flavor")->value; + // Belt printer: detect early since it affects multiple toggle decisions below. + bool is_belt_printer = false; + { + const auto *belt_opt = preset_bundle->printers.get_edited_preset().config.option("belt_printer"); + if (belt_opt) + is_belt_printer = belt_opt->value; + } + bool have_volumetric_extrusion_rate_slope = config->option("max_volumetric_extrusion_rate_slope")->value > 0; float have_volumetric_extrusion_rate_slope_segment_length = config->option("max_volumetric_extrusion_rate_slope_segment_length")->value; - toggle_field("enable_arc_fitting", !have_volumetric_extrusion_rate_slope); + toggle_field("enable_arc_fitting", !have_volumetric_extrusion_rate_slope && !is_belt_printer); toggle_line("max_volumetric_extrusion_rate_slope_segment_length", have_volumetric_extrusion_rate_slope); toggle_line("extrusion_rate_smoothing_external_perimeter_only", have_volumetric_extrusion_rate_slope); if(have_volumetric_extrusion_rate_slope) config->set_key_value("enable_arc_fitting", new ConfigOptionBool(false)); @@ -697,13 +705,20 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co } } - bool have_skirt = config->opt_int("skirt_loops") > 0; + // Belt printer: disable skirt, brim, raft, and draft shield controls. + bool have_skirt = config->opt_int("skirt_loops") > 0 && !is_belt_printer; toggle_field("skirt_height", have_skirt && config->opt_enum("draft_shield") != dsEnabled); toggle_line("single_loop_draft_shield", have_skirt); // ORCA: Display one wall if skirt enabled for (auto el : {"skirt_type", "min_skirt_length", "skirt_distance", "skirt_start_angle", "skirt_speed", "draft_shield"}) toggle_field(el, have_skirt); + if (is_belt_printer) { + toggle_field("skirt_loops", false); + toggle_field("skirt_height", false); + } - bool have_brim = (config->opt_enum("brim_type") != btNoBrim); + bool have_brim = (config->opt_enum("brim_type") != btNoBrim) && !is_belt_printer; + if (is_belt_printer) + toggle_field("brim_type", false); toggle_field("brim_object_gap", have_brim); toggle_field("brim_use_efc_outline", have_brim); bool have_brim_width = (config->opt_enum("brim_type") != btNoBrim) && config->opt_enum("brim_type") != btAutoBrim && @@ -724,7 +739,9 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co // Hide Elephant foot compensation layers if elefant_foot_compensation is not enabled toggle_line("elefant_foot_compensation_layers", config->opt_float("elefant_foot_compensation") > 0); - bool have_raft = config->opt_int("raft_layers") > 0; + bool have_raft = config->opt_int("raft_layers") > 0 && !is_belt_printer; + if (is_belt_printer) + toggle_field("raft_layers", false); bool have_support_material = config->opt_bool("enable_support") || have_raft; SupportType support_type = config->opt_enum("support_type"); diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 4814548a10..7df0b06ff5 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -2208,7 +2208,9 @@ void GCodeViewer::load_shells(const Print& print, bool initialized, bool force_p void GCodeViewer::render_toolpaths() { const Camera& camera = wxGetApp().plater()->get_camera(); - const libvgcode::Mat4x4 converted_view_matrix = libvgcode::convert(static_cast(camera.get_view_matrix().matrix().cast())); + Matrix4f view = camera.get_view_matrix().matrix().cast(); + // Belt view: view matrix transform placeholder (to be implemented in next cycle). + const libvgcode::Mat4x4 converted_view_matrix = libvgcode::convert(view); const libvgcode::Mat4x4 converted_projetion_matrix = libvgcode::convert(static_cast(camera.get_projection_matrix().matrix().cast())); #if VGCODE_ENABLE_COG_AND_TOOL_MARKERS m_viewer.set_cog_marker_scale_factor(m_cog_marker_fixed_screen_size ? 10.0f * m_cog_marker_size * camera.get_inv_zoom() : m_cog_marker_size); diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index dc928b822a..a299b93058 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -237,6 +237,9 @@ private: mutable bool m_no_render_path { false }; bool m_is_dark = false; + bool m_belt_view_enabled = false; + float m_belt_angle_deg = 0.f; + libvgcode::Viewer m_viewer; bool m_loaded_as_preview{ false }; @@ -336,6 +339,9 @@ public: void export_toolpaths_to_obj(const char* filename) const; + 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; } + size_t get_extruders_count() { return m_extruders_count; } void push_combo_style(); void pop_combo_style(); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index ef46801722..373ee2a3dc 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -10980,6 +10980,24 @@ void Plater::priv::set_bed_shape(const Pointfs &shape, Vec2d shape_position = partplate_list.get_current_shape_position(); bool new_shape = bed.set_shape(shape, printable_height, extruder_areas, extruder_heights, custom_model, force_as_custom, shape_position); + // Belt printer: configure build volume and bed rendering for belt mode. + { + const auto *belt_opt = config->option("belt_printer"); + bool is_belt = belt_opt && belt_opt->value; + if (is_belt) { + double belt_angle = config->opt_float("belt_printer_angle"); + bool infinite_y = config->opt_bool("belt_printer_infinite_y"); + bed.build_volume().set_belt_printer(true, belt_angle, infinite_y); + bed.set_belt_printer(true, static_cast(belt_angle)); + if (preview) + preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(true, static_cast(belt_angle)); + } else { + bed.set_belt_printer(false, 0.f); + if (preview) + preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(false, 0.f); + } + } + float prev_height_lid, prev_height_rod; partplate_list.get_height_limits(prev_height_lid, prev_height_rod); double height_to_lid = config->opt_float("extruder_clearance_height_to_lid"); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 6ac8ca9a0e..fafe46c952 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4366,6 +4366,9 @@ void TabPrinter::build_fff() optgroup->append_single_option_line("printable_height", "printer_basic_information_printable_space#printable-height"); optgroup->append_single_option_line("build_plate_tilt_x"); optgroup->append_single_option_line("build_plate_tilt_y"); + optgroup->append_single_option_line("belt_printer"); + optgroup->append_single_option_line("belt_printer_angle"); + optgroup->append_single_option_line("belt_printer_infinite_y"); optgroup->append_single_option_line("support_multi_bed_types","printer_basic_information_printable_space#support-multi-bed-types"); optgroup->append_single_option_line("best_object_pos", "printer_basic_information_printable_space#best-object-position"); // todo: for multi_extruder test @@ -5224,6 +5227,11 @@ void TabPrinter::toggle_options() auto gcf = m_config->option>("gcode_flavor")->value; toggle_line("enable_power_loss_recovery", is_BBL_printer || gcf == gcfMarlinFirmware); + + // Belt printer: show belt-specific settings only when belt_printer is enabled. + bool is_belt = m_config->opt_bool("belt_printer"); + toggle_line("belt_printer_angle", is_belt); + toggle_line("belt_printer_infinite_y", is_belt); } @@ -5395,6 +5403,23 @@ void TabPrinter::update_fff() m_use_silent_mode = m_config->opt_bool("silent_mode"); } + // Belt printer: auto-sync build_plate_tilt_x to belt_printer_angle when belt mode is active. + // When belt mode is off, reset build_plate_tilt_x to 0 if it was set by belt mode. + if (m_config->opt_bool("belt_printer")) { + double belt_angle = m_config->opt_float("belt_printer_angle"); + if (m_config->opt_float("build_plate_tilt_x") != belt_angle) { + m_config->set_key_value("build_plate_tilt_x", new ConfigOptionFloat(belt_angle)); + } + } else { + // Only reset if build_plate_tilt_x matches a typical belt angle (was set by auto-sync). + // Avoid clobbering a manually-set tilt value for non-belt tilted printers. + double current_tilt = m_config->opt_float("build_plate_tilt_x"); + double belt_angle = m_config->opt_float("belt_printer_angle"); + if (current_tilt != 0. && std::abs(current_tilt - belt_angle) < 0.01) { + m_config->set_key_value("build_plate_tilt_x", new ConfigOptionFloat(0.)); + } + } + toggle_options(); } From cb13a22e572db5b6a168be90c880d22a97326a85 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Thu, 12 Mar 2026 14:25:30 -0500 Subject: [PATCH 003/224] Part 2: Replace belt rotation w/ per-axis shear transforms and G-code axis remap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace monolithic belt rotation transform with independent per-axis shear controls (mode/angle/source-axis for X, Y, Z) and G-code axis remapping, giving full flexibility to match any belt printer's coordinate system - Remove all rotation mode logic and intermediate type+axes dropdowns, simplifying the pipeline to pure shear matrices while preserving the default behavior (Y += Z*cot(45deg) with identity remap) - Clean up GCodeWriter, GCodeProcessor, and GCodeViewer for the new shear-only model; expose 12 new settings in printer UI via Tab.cpp/Preset.cpp Implement belt printer tilted slicing Implement the core belt slicing pipeline that makes the slicer tilt-aware: Step 1: GCodeWriter::to_machine_coords() - R(+alpha, X) rotation from slicing frame to machine frame Step 2: PrintObject - belt-rotated object height calculation (y*sin(a) + z*cos(a)) for correct layer count Step 3: PrintObjectSlice - apply R(-alpha, X) rotation trafo so horizontal slice planes correspond to belt-parallel planes, with Z-shift computed from model volumes Step 4: GCodeProcessor - machine-frame preview (no transform needed) Step 5: 3DBed - rotate bed visualization about X by belt angle Fix: belt surface IS the build plate, no mesh rotation Currently still slicing perpendicular to the belt normal. Need to figure out why. Fix G-code Z sign: use R(-alpha, X) so Z+ is away from belt The previous R(+alpha, X) transform produced negative Z values (-y*sin(a) term dominated). Changed to R(-alpha, X) which gives machine_z = y*sin(a) + z*cos(a), always positive for points above the belt surface. Z increases with each layer as expected. reverting and changing slice methodology Add pink slicing direction arrow from origin Shows the effective slicing direction (gantry normal) as a pink arrow from the origin. Shorter and wider than the gravity arrow. Direction: R(+alpha, X) * Z = (0, -sin(a), cos(a)), which is the layer stacking direction in the original mesh frame. Fix slicing arrow visibility and add raw G-code toggle - Disable depth test for pink slicing arrow so it renders on top of the tilted bed geometry (was being occluded) - Remove unnecessary 5mm Z-offset from arrow position - Add m_belt_show_raw toggle to GCodeViewer - Add "Show raw G-code (slicing frame)" checkbox in legend when belt mode is active Implement to_machine_coords inverse rotation for belt printer G-code The slicing pipeline rotates the mesh by R(-alpha, X) and shifts Z to start at 0. The G-code output now undoes this transform via to_machine_coords: R(+alpha, X) * T(0,0,+z_shift), recovering the original machine-frame coordinates where Y is horizontal and Z is vertical. Changes: - GCodeWriter: implement to_machine_coords with inverse rotation + Z-shift - GCodeWriter: add belt_z_shift member and setter/getter - GCode.cpp: compute Z-shift from print objects (same logic as PrintObjectSlice) and pass to writer; write z_shift to G-code header - GCodeProcessor: parse belt_z_shift from G-code header - GCodeViewer: store belt_z_shift from processor result Wire raw G-code toggle to apply slicing-frame view transform When "Show raw G-code (slicing frame)" is checked in the preview legend, the view matrix is modified to apply R(-alpha, X) * T(0,0,-z_shift) to the toolpath rendering. This shows the G-code as it was during slicing: rotated part with horizontal layers. Default (unchecked): machine-frame view — upright part with tilted layers. Remove belt printer placeholder comment from GCodeProcessor The preview now correctly displays machine-frame G-code with the optional raw view toggle. No transform is needed in the processor. --- src/libslic3r/GCode.cpp | 27 ++++++++- src/libslic3r/GCode/GCodeProcessor.cpp | 9 ++- src/libslic3r/GCode/GCodeProcessor.hpp | 3 +- src/libslic3r/GCodeWriter.cpp | 16 +++++- src/libslic3r/GCodeWriter.hpp | 7 ++- src/libslic3r/PrintObject.cpp | 19 +++++-- src/libslic3r/PrintObjectSlice.cpp | 20 ++++++- src/slic3r/GUI/3DBed.cpp | 79 ++++++++++++++++++++++++-- src/slic3r/GUI/3DBed.hpp | 2 + src/slic3r/GUI/GCodeViewer.cpp | 20 ++++++- src/slic3r/GUI/GCodeViewer.hpp | 2 + 11 files changed, 183 insertions(+), 21 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index bff35d321c..d762da7ed4 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -8,6 +8,7 @@ #include "Exception.hpp" #include "ExtrusionEntity.hpp" #include "EdgeGrid.hpp" +#include "Geometry.hpp" #include "Geometry/ConvexHull.hpp" #include "GCode/PrintExtents.hpp" #include "GCode/Thumbnails.hpp" @@ -2414,8 +2415,28 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_writer.set_is_bbl_machine(is_bbl_printers); // Belt printer: initialize coordinate transformation on the writer. - if (print.config().belt_printer.value) + if (print.config().belt_printer.value) { m_writer.set_belt_angle(print.config().belt_printer_angle.value); + // Compute the Z-shift that was applied during slicing (same logic as PrintObjectSlice.cpp). + // This is needed by to_machine_coords() to undo the slicing transform. + // For multiple objects, use the minimum min_z_rotated across all objects. + double angle_rad = Geometry::deg2rad(print.config().belt_printer_angle.value); + Transform3d belt_rotation = Transform3d::Identity(); + belt_rotation.rotate(Eigen::AngleAxisd(-angle_rad, Vec3d::UnitX())); + double global_min_z = std::numeric_limits::max(); + for (const PrintObject *obj : print.objects()) { + Transform3d obj_trafo = belt_rotation * obj->trafo_centered(); + for (const ModelVolume *mv : obj->model_object()->volumes) { + if (! mv->is_model_part() && ! mv->is_modifier()) + continue; + BoundingBoxf3 bb = mv->mesh().bounding_box(); + bb = bb.transformed(obj_trafo * mv->get_matrix()); + global_min_z = std::min(global_min_z, bb.min.z()); + } + } + if (global_min_z != std::numeric_limits::max() && std::abs(global_min_z) > EPSILON) + m_writer.set_belt_z_shift(global_min_z); // typically negative + } // How many times will be change_layer() called? // change_layer() in turn increments the progress bar status. @@ -2507,8 +2528,10 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Write information on the generator. file.write_format("; generated by %s on %s\n", Slic3r::header_slic3r_generated().c_str(), Slic3r::Utils::local_timestamp().c_str()); // Belt printer: embed angle in header for G-code processor detection. - if (print.config().belt_printer.value) + if (print.config().belt_printer.value) { file.write_format("; belt_printer_angle = %.1f\n", print.config().belt_printer_angle.value); + file.write_format("; belt_z_shift = %.4f\n", m_writer.belt_z_shift()); + } if (is_bbl_printers) file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str()); //BBS: total layer number diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 0dc00e1522..a7a39b5b02 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -2597,8 +2597,6 @@ void GCodeProcessor::finalize(bool post_process) } } - // Belt printer: preview coordinate transform placeholder (to be implemented in next cycle). - calculate_time(m_result); // process the time blocks @@ -3053,6 +3051,13 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers } catch (...) {} return; } + // Belt printer Z-shift for raw G-code toggle in preview. + if (boost::starts_with(comment, " belt_z_shift = ")) { + try { + m_result.belt_z_shift = std::stof(std::string(comment.substr(16))); + } catch (...) {} + return; + } // wipe start tag if (boost::starts_with(comment, reserved_tag(ETags::Wipe_Start))) { diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index e432a165a8..af68a5a6b7 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -227,8 +227,9 @@ class Print; bool support_traditional_timelapse{true}; float printable_height; float z_offset; - // Belt printer: angle for coordinate transformation in preview. + // Belt printer: angle and Z-shift for coordinate transformation in preview. float belt_printer_angle{ 0.f }; + float belt_z_shift{ 0.f }; SettingsIds settings_ids; size_t filaments_count; bool backtrace_enabled; diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index 1b056a946f..119c9dd068 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -29,8 +29,20 @@ void GCodeWriter::set_belt_angle(double angle_deg) Vec3d GCodeWriter::to_machine_coords(const Vec3d &pos) const { - // Belt printer: coordinate transform placeholder (to be implemented in next cycle). - return pos; + if (!is_belt_printer()) + return pos; + + // Undo the slicing transform to recover machine-frame coordinates. + // Slicing applied: T(0,0,-min_z_rot) * R(-alpha, X) * original_pos + // Inverse: R(+alpha, X) * T(0,0,+min_z_rot) * slicing_pos + // + // First undo the Z-shift, then apply R(+alpha, X). + double sz = pos.z() + m_belt_z_shift; // undo T(0,0,-min_z_rot) + return Vec3d( + pos.x(), + pos.y() * m_belt_cos - sz * m_belt_sin, // R(+alpha, X) + pos.y() * m_belt_sin + sz * m_belt_cos + ); } bool GCodeWriter::supports_separate_travel_acceleration(GCodeFlavor flavor) diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index 10159d941f..f9b5ed79ac 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -127,8 +127,10 @@ public: // Belt printer: set the belt angle and precompute sin/cos for coordinate transformation. void set_belt_angle(double angle_deg); + void set_belt_z_shift(double z_shift) { m_belt_z_shift = z_shift; } + double belt_z_shift() const { return m_belt_z_shift; } bool is_belt_printer() const { return m_belt_angle_rad != 0.; } - // Transform a point from the slicing frame to machine/world coordinates (inverse shear). + // Transform a point from the slicing frame to machine/world coordinates (inverse rotation). Vec3d to_machine_coords(const Vec3d &pos) const; // Returns whether this flavor supports separate print and travel acceleration. @@ -184,10 +186,11 @@ public: //SoftFever bool m_is_bbl_printers = false; - // Belt printer coordinate transformation (YZ shear) + // Belt printer coordinate transformation (inverse of slicing rotation) double m_belt_angle_rad = 0.; double m_belt_cos = 1.0; double m_belt_sin = 0.0; + double m_belt_z_shift = 0.; // Z-shift applied during slicing (min_z_rotated, typically negative) double m_current_speed; bool m_is_first_layer = true; diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 979953fb94..08ff847951 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -3392,7 +3392,13 @@ void PrintObject::update_slicing_parameters() // Orca: updated function call for XYZ shrinkage compensation if (!m_slicing_params.valid) { coordf_t object_height = this->model_object()->max_z(); - // Belt printer: height adjustment placeholder (to be implemented in next cycle). + if (this->print()->config().belt_printer.value) { + // After mesh rotation R(-alpha, X), effective height in the slicing + // direction is y_extent*sin(a) + z_extent*cos(a). + double angle_rad = Geometry::deg2rad(this->print()->config().belt_printer_angle.value); + BoundingBoxf3 bb = this->model_object()->raw_bounding_box(); + object_height = bb.size().y() * std::sin(angle_rad) + bb.size().z() * std::cos(angle_rad); + } m_slicing_params = SlicingParameters::create_from_config(this->print()->config(), m_config, object_height, this->object_extruders(), this->print()->shrinkage_compensation()); } @@ -3432,9 +3438,14 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full sort_remove_duplicates(object_extruders); //FIXME add painting extruders - if (object_max_z <= 0.f) - object_max_z = (float)model_object.raw_bounding_box().size().z(); - // Belt printer: height adjustment placeholder (to be implemented in next cycle). + if (object_max_z <= 0.f) { + BoundingBoxf3 bb = model_object.raw_bounding_box(); + object_max_z = (float)bb.size().z(); + if (print_config.belt_printer.value) { + double angle_rad = Geometry::deg2rad(print_config.belt_printer_angle.value); + object_max_z = (float)(bb.size().y() * std::sin(angle_rad) + bb.size().z() * std::cos(angle_rad)); + } + } return SlicingParameters::create_from_config(print_config, object_config, object_max_z, object_extruders, object_shrinkage_compensation); } diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index bba4593764..2748151aa0 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -140,7 +140,25 @@ static std::vector slice_volumes_inner( params_base.closing_radius = print_object_config.slice_closing_radius.value; params_base.extra_offset = 0; params_base.trafo = object_trafo; - // Belt printer: mesh transform placeholder (to be implemented in next cycle). + if (print_config.belt_printer.value) { + double angle_rad = Geometry::deg2rad(print_config.belt_printer_angle.value); + // Rotate mesh by -alpha about X so horizontal slice planes = gantry-parallel planes. + // The gantry (XY) is tilted by belt_printer_angle; this rotation aligns it with horizontal. + Transform3d belt_rotation = Transform3d::Identity(); + belt_rotation.rotate(Eigen::AngleAxisd(-angle_rad, Vec3d::UnitX())); + params_base.trafo = belt_rotation * params_base.trafo; + // Compute Z-shift from model_volumes: find min-Z of all rotated meshes + // so the rotated geometry starts at Z=0 (the belt surface in slicing frame). + double min_z_rotated = std::numeric_limits::max(); + for (const ModelVolume *mv : model_volumes) { + if (!model_volume_needs_slicing(*mv)) continue; + BoundingBoxf3 bb = mv->mesh().bounding_box(); + bb = bb.transformed(params_base.trafo * mv->get_matrix()); + min_z_rotated = std::min(min_z_rotated, bb.min.z()); + } + if (min_z_rotated != std::numeric_limits::max() && std::abs(min_z_rotated) > EPSILON) + params_base.trafo = Eigen::Translation3d(0, 0, -min_z_rotated) * params_base.trafo; + } //BBS: 0.0025mm is safe enough to simplify the data to speed slicing up for high-resolution model. //Also has on influence on arc fitting which has default resolution 0.0125mm. params_base.resolution = print_config.resolution <= 0.001 ? 0.0f : 0.0025; diff --git a/src/slic3r/GUI/3DBed.cpp b/src/slic3r/GUI/3DBed.cpp index 101279d744..858182946c 100644 --- a/src/slic3r/GUI/3DBed.cpp +++ b/src/slic3r/GUI/3DBed.cpp @@ -387,17 +387,18 @@ void Bed3D::render_internal(GLCanvas3D& canvas, const Transform3d& view_matrix, m_model.set_color(m_is_dark ? DEFAULT_MODEL_COLOR_DARK : DEFAULT_MODEL_COLOR); - // Belt printer: bed view transform placeholder (to be implemented in next cycle). - Transform3d belt_view_matrix = view_matrix; + // Belt printer: bed rotation is applied inside render_model() and render_default() + // using m_is_belt_printer and m_belt_angle members. switch (m_type) { - case Type::System: { render_system(canvas, belt_view_matrix, projection_matrix, bottom); break; } + case Type::System: { render_system(canvas, view_matrix, projection_matrix, bottom); break; } default: - case Type::Custom: { render_custom(canvas, belt_view_matrix, projection_matrix, bottom); break; } + case Type::Custom: { render_custom(canvas, view_matrix, projection_matrix, bottom); break; } } render_gravity_arrow(view_matrix, projection_matrix); + render_slicing_arrow(view_matrix, projection_matrix); render_slicing_plane(view_matrix, projection_matrix); glsafe(::glDisable(GL_DEPTH_TEST)); @@ -698,7 +699,13 @@ void Bed3D::render_model(const Transform3d& view_matrix, const Transform3d& proj if (shader != nullptr) { shader->start_using(); shader->set_uniform("emission_factor", 0.0f); - const Transform3d model_matrix = Geometry::assemble_transform(m_model_offset); + Transform3d model_matrix = Geometry::assemble_transform(m_model_offset); + // Belt printer: rotate the bed model about X so the belt tilt is visible. + // Negative angle: belt surface tilts downward away from the nozzle. + if (m_is_belt_printer && m_belt_angle > 0.f) { + double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle)); + model_matrix = Eigen::AngleAxisd(-angle_rad, Vec3d::UnitX()) * model_matrix; + } shader->set_uniform("volume_world_matrix", model_matrix); shader->set_uniform("view_model_matrix", view_matrix * model_matrix); shader->set_uniform("projection_matrix", projection_matrix); @@ -798,6 +805,58 @@ void Bed3D::render_gravity_arrow(const Transform3d& view_matrix, const Transform shader->stop_using(); } +void Bed3D::render_slicing_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix) +{ + if (!m_is_belt_printer || m_belt_angle <= 0.f) + return; + + // Build the arrow model: shorter and wider than the gravity arrow. + if (!m_slicing_arrow.is_initialized()) { + const float stem_length = 15.0f; // shorter than gravity arrow (25) + const float stem_radius = 1.0f; // wider than gravity arrow (~0.33) + const float tip_radius = 3.0f; // wider tip + const float tip_length = 5.0f; + m_slicing_arrow.init_from(stilized_arrow(16, tip_radius, tip_length, stem_radius, stem_length)); + } + + // The slicing direction: layers stack along the gantry normal. + // With mesh rotation R(-alpha, X), the slicing Z-axis in the original frame + // points in direction R(+alpha, X) * (0, 0, 1) = (0, -sin(alpha), cos(alpha)). + double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle)); + Vec3d slice_dir = Vec3d(0., -std::sin(angle_rad), std::cos(angle_rad)).normalized(); + + // Compute rotation to align +Z (arrow default) with slice_dir. + Vec3d from = Vec3d::UnitZ(); + double dot = from.dot(slice_dir); + Transform3d rot = Transform3d::Identity(); + if (dot < -0.9999) { + rot = Eigen::AngleAxisd(M_PI, Vec3d::UnitX()) * rot; + } else if (dot < 0.9999) { + Vec3d axis = from.cross(slice_dir).normalized(); + double angle = std::acos(std::clamp(dot, -1.0, 1.0)); + rot = Eigen::AngleAxisd(angle, axis) * rot; + } + + GLShaderProgram* shader = wxGetApp().get_shader("flat"); + if (shader == nullptr) + return; + + // Disable depth test so the arrow is always visible (not occluded by the tilted bed). + glsafe(::glDisable(GL_DEPTH_TEST)); + shader->start_using(); + + const Camera& camera = wxGetApp().plater()->get_camera(); + Transform3d model_matrix = rot; + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * model_matrix); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + + m_slicing_arrow.set_color({ 1.0f, 0.2f, 0.6f, 1.0f }); // pink + m_slicing_arrow.render(); + + shader->stop_using(); + glsafe(::glEnable(GL_DEPTH_TEST)); +} + void Bed3D::render_slicing_plane(const Transform3d& view_matrix, const Transform3d& projection_matrix) { if (!m_is_belt_printer || m_belt_angle <= 0.f) @@ -863,7 +922,15 @@ void Bed3D::render_default(bool bottom, const Transform3d& view_matrix, const Tr if (shader != nullptr) { shader->start_using(); - shader->set_uniform("view_model_matrix", view_matrix); + // Belt printer: rotate the default bed about X so the belt tilt is visible. + Transform3d view_model_matrix = view_matrix; + if (m_is_belt_printer && m_belt_angle > 0.f) { + double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle)); + Transform3d belt_rotation = Transform3d::Identity(); + belt_rotation.rotate(Eigen::AngleAxisd(-angle_rad, Vec3d::UnitX())); + view_model_matrix = view_matrix * belt_rotation; + } + shader->set_uniform("view_model_matrix", view_model_matrix); shader->set_uniform("projection_matrix", projection_matrix); glsafe(::glEnable(GL_DEPTH_TEST)); diff --git a/src/slic3r/GUI/3DBed.hpp b/src/slic3r/GUI/3DBed.hpp index 005dbff534..d01273961d 100644 --- a/src/slic3r/GUI/3DBed.hpp +++ b/src/slic3r/GUI/3DBed.hpp @@ -111,6 +111,7 @@ private: GLModel m_model; Vec3d m_model_offset{ Vec3d::Zero() }; GLModel m_gravity_arrow; + GLModel m_slicing_arrow; // Pink arrow showing the effective slicing direction GLModel m_slicing_plane; // Debug: shows the intended slicing plane direction Axes m_axes; @@ -189,6 +190,7 @@ private: void render_custom(GLCanvas3D& canvas, const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom); void render_default(bool bottom, const Transform3d& view_matrix, const Transform3d& projection_matrix); void render_gravity_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix); + void render_slicing_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix); void render_slicing_plane(const Transform3d& view_matrix, const Transform3d& projection_matrix); // BBS: remove the bed picking logic diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 7df0b06ff5..dd66c918a7 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1271,6 +1271,7 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const m_max_print_height = gcode_result.printable_height; m_z_offset = gcode_result.z_offset; + m_belt_z_shift = gcode_result.belt_z_shift; // load_toolpaths(gcode_result, build_volume, exclude_bounding_box); @@ -2209,7 +2210,16 @@ void GCodeViewer::render_toolpaths() { const Camera& camera = wxGetApp().plater()->get_camera(); Matrix4f view = camera.get_view_matrix().matrix().cast(); - // Belt view: view matrix transform placeholder (to be implemented in next cycle). + // Belt "raw" view: apply slicing rotation to view matrix so toolpaths appear + // in the slicing frame (rotated part with horizontal layers). + if (m_belt_show_raw && m_belt_view_enabled && m_belt_angle_deg > 0.f) { + double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle_deg)); + // Apply R(-alpha, X) * T(0,0,-z_shift) to bring machine coords back to slicing frame. + Transform3d slicing_trafo = Transform3d::Identity(); + slicing_trafo.translate(Vec3d(0., 0., -static_cast(m_belt_z_shift))); + slicing_trafo = Eigen::AngleAxisd(-angle_rad, Vec3d::UnitX()) * slicing_trafo; + view = (camera.get_view_matrix() * slicing_trafo).matrix().cast(); + } const libvgcode::Mat4x4 converted_view_matrix = libvgcode::convert(view); const libvgcode::Mat4x4 converted_projetion_matrix = libvgcode::convert(static_cast(camera.get_projection_matrix().matrix().cast())); #if VGCODE_ENABLE_COG_AND_TOOL_MARKERS @@ -4406,6 +4416,14 @@ 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 raw slicing-frame G-code + if (m_belt_view_enabled) { + ImGui::Spacing(); + ImGui::Dummy({ window_padding, 0 }); + ImGui::SameLine(); + ImGui::Checkbox("Show raw G-code (slicing frame)", &m_belt_show_raw); + } + legend_height = ImGui::GetCurrentWindow()->Size.y; imgui.end(); ImGui::PopStyleColor(7); diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index a299b93058..4bd3746991 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -239,6 +239,8 @@ mutable bool m_no_render_path { false }; bool m_belt_view_enabled = false; float m_belt_angle_deg = 0.f; + float m_belt_z_shift = 0.f; + bool m_belt_show_raw = false; // Toggle: show raw slicing-frame G-code (rotated part) libvgcode::Viewer m_viewer; bool m_loaded_as_preview{ false }; From 719af2d81d1fb62206e978743151bee018191563 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Thu, 12 Mar 2026 20:47:28 -0500 Subject: [PATCH 004/224] Part 2.5: Add global shear transform, support clipping, and belt UI improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implement per-object global shear transform in PrintObject with layer Z-offset calculation, config invalidation, and fix for shared-object layer optimization breaking copied objects - Clip support layers to the transformed belt floor plane and begin work on tree support adaptation for sheared coordinate space - Improve belt UI: gray out inactive sub-options, add B keyboard shortcut for G-code viewer design-view toggle, fix mesh clipping through build plate after shear/scale transform y' = y + z·cot(α), while x' = x and z' = z getting closer to customizable variant getting closer X/Y/Z shear initial clean up UI add 1/sin(a) transform, idea taken from blackbelt cura plugin Things work now (turns out I've been using the wrong set of transforms) --- src/libslic3r/GCode.cpp | 31 ++---- src/libslic3r/GCode/GCodeProcessor.cpp | 8 -- src/libslic3r/GCode/GCodeProcessor.hpp | 3 +- src/libslic3r/GCodeWriter.cpp | 34 ++++--- src/libslic3r/GCodeWriter.hpp | 17 ++-- src/libslic3r/Preset.cpp | 6 +- src/libslic3r/PrintConfig.cpp | 135 +++++++++++++++++++++++++ src/libslic3r/PrintConfig.hpp | 54 ++++++++++ src/libslic3r/PrintObject.cpp | 101 ++++++++++++++++-- src/libslic3r/PrintObjectSlice.cpp | 78 +++++++++++--- src/slic3r/GUI/GCodeViewer.cpp | 22 ++-- src/slic3r/GUI/GCodeViewer.hpp | 3 +- src/slic3r/GUI/Tab.cpp | 51 ++++++++++ 13 files changed, 452 insertions(+), 91 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index d762da7ed4..02a233e988 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2411,31 +2411,19 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_is_role_based_fan_on.fill(false); m_fan_mover.release(); - + m_writer.set_is_bbl_machine(is_bbl_printers); - // Belt printer: initialize coordinate transformation on the writer. + // Belt printer: initialize coordinate transformation and axis remap on the writer. if (print.config().belt_printer.value) { m_writer.set_belt_angle(print.config().belt_printer_angle.value); - // Compute the Z-shift that was applied during slicing (same logic as PrintObjectSlice.cpp). - // This is needed by to_machine_coords() to undo the slicing transform. - // For multiple objects, use the minimum min_z_rotated across all objects. - double angle_rad = Geometry::deg2rad(print.config().belt_printer_angle.value); - Transform3d belt_rotation = Transform3d::Identity(); - belt_rotation.rotate(Eigen::AngleAxisd(-angle_rad, Vec3d::UnitX())); - double global_min_z = std::numeric_limits::max(); - for (const PrintObject *obj : print.objects()) { - Transform3d obj_trafo = belt_rotation * obj->trafo_centered(); - for (const ModelVolume *mv : obj->model_object()->volumes) { - if (! mv->is_model_part() && ! mv->is_modifier()) - continue; - BoundingBoxf3 bb = mv->mesh().bounding_box(); - bb = bb.transformed(obj_trafo * mv->get_matrix()); - global_min_z = std::min(global_min_z, bb.min.z()); - } - } - if (global_min_z != std::numeric_limits::max() && std::abs(global_min_z) > EPSILON) - m_writer.set_belt_z_shift(global_min_z); // typically negative + m_writer.set_axis_remap( + int(print.config().belt_gcode_remap_x.value), + int(print.config().belt_gcode_remap_y.value), + int(print.config().belt_gcode_remap_z.value)); + // Build volume extents for Rev remap mode. + BoundingBoxf bbox_bed(print.config().printable_area.values); + m_writer.set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(), print.config().printable_height.value)); } // How many times will be change_layer() called? @@ -2530,7 +2518,6 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Belt printer: embed angle in header for G-code processor detection. if (print.config().belt_printer.value) { file.write_format("; belt_printer_angle = %.1f\n", print.config().belt_printer_angle.value); - file.write_format("; belt_z_shift = %.4f\n", m_writer.belt_z_shift()); } if (is_bbl_printers) file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str()); diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index a7a39b5b02..7ac3a1ffc4 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -3051,14 +3051,6 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers } catch (...) {} return; } - // Belt printer Z-shift for raw G-code toggle in preview. - if (boost::starts_with(comment, " belt_z_shift = ")) { - try { - m_result.belt_z_shift = std::stof(std::string(comment.substr(16))); - } catch (...) {} - return; - } - // wipe start tag if (boost::starts_with(comment, reserved_tag(ETags::Wipe_Start))) { m_wiping = true; diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index af68a5a6b7..e432a165a8 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -227,9 +227,8 @@ class Print; bool support_traditional_timelapse{true}; float printable_height; float z_offset; - // Belt printer: angle and Z-shift for coordinate transformation in preview. + // Belt printer: angle for coordinate transformation in preview. float belt_printer_angle{ 0.f }; - float belt_z_shift{ 0.f }; SettingsIds settings_ids; size_t filaments_count; bool backtrace_enabled; diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index 119c9dd068..d6bb5a0bb9 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -23,26 +23,32 @@ bool GCodeWriter::full_gcode_comment = true; void GCodeWriter::set_belt_angle(double angle_deg) { m_belt_angle_rad = Geometry::deg2rad(angle_deg); - m_belt_cos = std::cos(m_belt_angle_rad); - m_belt_sin = std::sin(m_belt_angle_rad); +} + +void GCodeWriter::set_axis_remap(int rx, int ry, int rz) +{ + m_remap_x = rx; + m_remap_y = ry; + m_remap_z = rz; +} + +void GCodeWriter::set_build_volume_max(const Vec3d &max) +{ + m_build_vol_max = max; } Vec3d GCodeWriter::to_machine_coords(const Vec3d &pos) const { if (!is_belt_printer()) return pos; - - // Undo the slicing transform to recover machine-frame coordinates. - // Slicing applied: T(0,0,-min_z_rot) * R(-alpha, X) * original_pos - // Inverse: R(+alpha, X) * T(0,0,+min_z_rot) * slicing_pos - // - // First undo the Z-shift, then apply R(+alpha, X). - double sz = pos.z() + m_belt_z_shift; // undo T(0,0,-min_z_rot) - return Vec3d( - pos.x(), - pos.y() * m_belt_cos - sz * m_belt_sin, // R(+alpha, X) - pos.y() * m_belt_sin + sz * m_belt_cos - ); + // BeltRemapAxis: 0-2 = +X/+Y/+Z, 3-5 = -X/-Y/-Z, 6-8 = Rev X/Y/Z + auto remap = [this, &pos](int r) -> double { + int axis = r % 3; + if (r < 3) return pos[axis]; + if (r < 6) return -pos[axis]; + return m_build_vol_max[axis] - pos[axis]; + }; + return { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) }; } bool GCodeWriter::supports_separate_travel_acceleration(GCodeFlavor flavor) diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index f9b5ed79ac..6074eb247c 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -127,10 +127,12 @@ public: // Belt printer: set the belt angle and precompute sin/cos for coordinate transformation. void set_belt_angle(double angle_deg); - void set_belt_z_shift(double z_shift) { m_belt_z_shift = z_shift; } - double belt_z_shift() const { return m_belt_z_shift; } bool is_belt_printer() const { return m_belt_angle_rad != 0.; } - // Transform a point from the slicing frame to machine/world coordinates (inverse rotation). + // Set axis remap for G-code output (BeltRemapAxis enum values). + void set_axis_remap(int rx, int ry, int rz); + // Set build volume extents for Rev remap mode (max X, Y, Z). + void set_build_volume_max(const Vec3d &max); + // Transform a point from the slicing frame to machine coordinates. Vec3d to_machine_coords(const Vec3d &pos) const; // Returns whether this flavor supports separate print and travel acceleration. @@ -186,11 +188,12 @@ public: //SoftFever bool m_is_bbl_printers = false; - // Belt printer coordinate transformation (inverse of slicing rotation) + // Belt printer state double m_belt_angle_rad = 0.; - double m_belt_cos = 1.0; - double m_belt_sin = 0.0; - double m_belt_z_shift = 0.; // Z-shift applied during slicing (min_z_rotated, typically negative) + int m_remap_x = 0; + int m_remap_y = 1; + int m_remap_z = 2; + Vec3d m_build_vol_max = Vec3d::Zero(); double m_current_speed; bool m_is_first_layer = true; diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 98532bccc7..9bc5e0a782 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1010,7 +1010,11 @@ static std::vector s_Preset_machine_limits_options { static std::vector s_Preset_printer_options { "printer_technology", - "printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "build_plate_tilt_x", "build_plate_tilt_y", "belt_printer", "belt_printer_angle", "belt_printer_infinite_y", "gcode_flavor", + "printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "build_plate_tilt_x", "build_plate_tilt_y", "belt_printer", "belt_printer_angle", "belt_printer_infinite_y", "belt_shear_x", "belt_shear_x_angle", "belt_shear_x_from", + "belt_shear_y", "belt_shear_y_angle", "belt_shear_y_from", + "belt_shear_z", "belt_shear_z_angle", "belt_shear_z_from", + "belt_scale_x", "belt_scale_x_angle", "belt_scale_y", "belt_scale_y_angle", "belt_scale_z", "belt_scale_z_angle", + "belt_gcode_remap_x", "belt_gcode_remap_y", "belt_gcode_remap_z", "gcode_flavor", "fan_kickstart", "fan_speedup_time", "fan_speedup_overhangs", "single_extruder_multi_material", "manual_filament_change", "file_start_gcode", "machine_start_gcode", "machine_end_gcode", "before_layer_change_gcode", "printing_by_object_gcode", "layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode", "change_filament_gcode", "change_extrusion_role_gcode", "printer_model", "printer_variant", "printer_extruder_id", "printer_extruder_variant", "extruder_variant_list", "default_nozzle_volume_type", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 7eea294d4e..a722a3bae7 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -289,6 +289,44 @@ static t_config_enum_values s_keys_map_SlicingMode { }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(SlicingMode) +static t_config_enum_values s_keys_map_BeltShearMode { + { "none", int(BeltShearMode::None) }, + { "pos_cot", int(BeltShearMode::PosCot) }, + { "neg_cot", int(BeltShearMode::NegCot) }, + { "pos_tan", int(BeltShearMode::PosTan) }, + { "neg_tan", int(BeltShearMode::NegTan) }, +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltShearMode) + +static t_config_enum_values s_keys_map_BeltAxis { + { "x", int(BeltAxis::X) }, + { "y", int(BeltAxis::Y) }, + { "z", int(BeltAxis::Z) }, +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltAxis) + +static t_config_enum_values s_keys_map_BeltScaleMode { + { "none", int(BeltScaleMode::None) }, + { "inv_sin", int(BeltScaleMode::InvSin) }, + { "inv_cos", int(BeltScaleMode::InvCos) }, + { "sin", int(BeltScaleMode::Sin) }, + { "cos", int(BeltScaleMode::Cos) }, +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltScaleMode) + +static t_config_enum_values s_keys_map_BeltRemapAxis { + { "pos_x", int(BeltRemapAxis::PosX) }, + { "pos_y", int(BeltRemapAxis::PosY) }, + { "pos_z", int(BeltRemapAxis::PosZ) }, + { "neg_x", int(BeltRemapAxis::NegX) }, + { "neg_y", int(BeltRemapAxis::NegY) }, + { "neg_z", int(BeltRemapAxis::NegZ) }, + { "rev_x", int(BeltRemapAxis::RevX) }, + { "rev_y", int(BeltRemapAxis::RevY) }, + { "rev_z", int(BeltRemapAxis::RevZ) }, +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltRemapAxis) + static t_config_enum_values s_keys_map_SupportMaterialPattern { { "rectilinear", smpRectilinear }, { "rectilinear-grid", smpRectilinearGrid }, @@ -5974,6 +6012,103 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionBool(true)); + // Per-axis shear controls for belt printer + auto add_belt_shear_mode = [this](const char *key, const char *label, BeltShearMode default_mode) { + auto def = this->add(key, coEnum); + def->label = L(label); + def->category = L("Printable space"); + def->tooltip = L("Shear function applied to this axis in belt printer mode."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values = {"none", "pos_cot", "neg_cot", "pos_tan", "neg_tan"}; + def->enum_labels = {L("None"), L("+cot(α)"), L("-cot(α)"), L("+tan(α)"), L("-tan(α)")}; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(default_mode)); + }; + auto add_belt_shear_angle = [this](const char *key, const char *label) { + auto def = this->add(key, coFloat); + def->label = L(label); + def->category = L("Printable space"); + def->tooltip = L("Angle (degrees) for the shear function on this axis."); + def->sidetext = L("°"); + def->min = 0.1; + def->max = 89.9; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(45)); + }; + auto add_belt_axis_enum = [this](const char *key, const char *label, const char *tooltip, BeltAxis default_axis) { + auto def = this->add(key, coEnum); + def->label = L(label); + def->category = L("Printable space"); + def->tooltip = L(tooltip); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values = {"x", "y", "z"}; + def->enum_labels = {L("X"), L("Y"), L("Z")}; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(default_axis)); + }; + + add_belt_shear_mode("belt_shear_x", "Function", BeltShearMode::None); + add_belt_shear_angle("belt_shear_x_angle", "Angle"); + add_belt_axis_enum("belt_shear_x_from", "From", "Source axis for X shear.", BeltAxis::Z); + + add_belt_shear_mode("belt_shear_y", "Function", BeltShearMode::PosCot); + add_belt_shear_angle("belt_shear_y_angle", "Angle"); + add_belt_axis_enum("belt_shear_y_from", "From", "Source axis for Y shear.", BeltAxis::Z); + + add_belt_shear_mode("belt_shear_z", "Function", BeltShearMode::None); + add_belt_shear_angle("belt_shear_z_angle", "Angle"); + add_belt_axis_enum("belt_shear_z_from", "From", "Source axis for Z shear.", BeltAxis::Y); + + // Per-axis scale controls for belt printer + auto add_belt_scale_mode = [this](const char *key, const char *label, BeltScaleMode default_mode) { + auto def = this->add(key, coEnum); + def->label = L(label); + def->category = L("Printable space"); + def->tooltip = L("Scale factor applied to this axis in belt printer mode."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values = {"none", "inv_sin", "inv_cos", "sin", "cos"}; + def->enum_labels = {L("None"), L("1/sin(α)"), L("1/cos(α)"), L("sin(α)"), L("cos(α)")}; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(default_mode)); + }; + auto add_belt_scale_angle = [this](const char *key, const char *label) { + auto def = this->add(key, coFloat); + def->label = L(label); + def->category = L("Printable space"); + def->tooltip = L("Angle (degrees) for the scale function on this axis."); + def->sidetext = L("°"); + def->min = 0.1; + def->max = 89.9; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(45)); + }; + + add_belt_scale_mode("belt_scale_x", "Function", BeltScaleMode::None); + add_belt_scale_angle("belt_scale_x_angle", "Angle"); + + add_belt_scale_mode("belt_scale_y", "Function", BeltScaleMode::None); + add_belt_scale_angle("belt_scale_y_angle", "Angle"); + + add_belt_scale_mode("belt_scale_z", "Function", BeltScaleMode::None); + add_belt_scale_angle("belt_scale_z_angle", "Angle"); + + // G-code axis remap with sign + auto add_belt_remap = [this](const char *key, const char *label, const char *tooltip, BeltRemapAxis default_axis) { + auto def = this->add(key, coEnum); + def->label = L(label); + def->category = L("Printable space"); + def->tooltip = L(tooltip); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values = {"pos_x", "pos_y", "pos_z", "neg_x", "neg_y", "neg_z", "rev_x", "rev_y", "rev_z"}; + def->enum_labels = {L("+X"), L("+Y"), L("+Z"), L("-X"), L("-Y"), L("-Z"), L("Rev X"), L("Rev Y"), L("Rev Z")}; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(default_axis)); + }; + + add_belt_remap("belt_gcode_remap_x", "X", "Which slicing axis maps to machine X in G-code output.", BeltRemapAxis::PosX); + add_belt_remap("belt_gcode_remap_y", "Y", "Which slicing axis maps to machine Y in G-code output.", BeltRemapAxis::PosY); + add_belt_remap("belt_gcode_remap_z", "Z", "Which slicing axis maps to machine Z in G-code output.", BeltRemapAxis::PosZ); + def = this->add("tree_support_branch_angle", coFloat); def->label = L("Tree support branch angle"); def->category = L("Support"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index f36b5d3e93..4f149d9c8b 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -157,6 +157,38 @@ enum class SlicingMode CloseHoles, }; +enum class BeltShearMode +{ + None, // No shear on this axis + PosCot, // += cot(α) + NegCot, // -= cot(α) + PosTan, // += tan(α) + NegTan, // -= tan(α) +}; + +enum class BeltScaleMode +{ + None, // No scaling (factor = 1) + InvSin, // 1/sin(α) + InvCos, // 1/cos(α) + Sin, // sin(α) + Cos, // cos(α) +}; + +enum class BeltAxis +{ + X = 0, + Y = 1, + Z = 2, +}; + +enum class BeltRemapAxis +{ + PosX = 0, PosY = 1, PosZ = 2, + NegX = 3, NegY = 4, NegZ = 5, + RevX = 6, RevY = 7, RevZ = 8, // Reversed: max - pos +}; + enum SupportMaterialPattern { smpDefault, smpRectilinear, smpRectilinearGrid, smpHoneycomb, @@ -500,6 +532,10 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(NoiseType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(InfillPattern) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(IroningType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SlicingMode) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltShearMode) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltScaleMode) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltAxis) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltRemapAxis) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialPattern) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialStyle) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialInterfacePattern) @@ -1417,6 +1453,24 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionBool, belt_printer)) ((ConfigOptionFloat, belt_printer_angle)) ((ConfigOptionBool, belt_printer_infinite_y)) + ((ConfigOptionEnum, belt_shear_x)) + ((ConfigOptionFloat, belt_shear_x_angle)) + ((ConfigOptionEnum, belt_shear_x_from)) + ((ConfigOptionEnum, belt_shear_y)) + ((ConfigOptionFloat, belt_shear_y_angle)) + ((ConfigOptionEnum, belt_shear_y_from)) + ((ConfigOptionEnum, belt_shear_z)) + ((ConfigOptionFloat, belt_shear_z_angle)) + ((ConfigOptionEnum, belt_shear_z_from)) + ((ConfigOptionEnum, belt_scale_x)) + ((ConfigOptionFloat, belt_scale_x_angle)) + ((ConfigOptionEnum, belt_scale_y)) + ((ConfigOptionFloat, belt_scale_y_angle)) + ((ConfigOptionEnum, belt_scale_z)) + ((ConfigOptionFloat, belt_scale_z_angle)) + ((ConfigOptionEnum, belt_gcode_remap_x)) + ((ConfigOptionEnum, belt_gcode_remap_y)) + ((ConfigOptionEnum, belt_gcode_remap_z)) //BBS ((ConfigOptionInts, additional_cooling_fan_speed)) ((ConfigOptionBool, reduce_crossing_wall)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 08ff847951..abd309fbff 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -3392,14 +3392,55 @@ void PrintObject::update_slicing_parameters() // Orca: updated function call for XYZ shrinkage compensation if (!m_slicing_params.valid) { coordf_t object_height = this->model_object()->max_z(); - if (this->print()->config().belt_printer.value) { - // After mesh rotation R(-alpha, X), effective height in the slicing - // direction is y_extent*sin(a) + z_extent*cos(a). - double angle_rad = Geometry::deg2rad(this->print()->config().belt_printer_angle.value); - BoundingBoxf3 bb = this->model_object()->raw_bounding_box(); - object_height = bb.size().y() * std::sin(angle_rad) + bb.size().z() * std::cos(angle_rad); + // Belt shear/scale may change the effective Z height. + const auto &pcfg = this->print()->config(); + if (pcfg.belt_printer.value) { + bool has_z_shear = pcfg.belt_shear_z.value != BeltShearMode::None; + bool has_z_scale = pcfg.belt_scale_z.value != BeltScaleMode::None; + if (has_z_shear || has_z_scale) { + auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad); + switch (mode) { + case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; + case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; + case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; + case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; + default: return 0.; + } + }; + auto compute_scale_factor = [](BeltScaleMode mode, double angle_deg) -> double { + if (mode == BeltScaleMode::None) return 1.; + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad); + switch (mode) { + case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; + case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; + case BeltScaleMode::Sin: return sin_a; + case BeltScaleMode::Cos: return cos_a; + default: return 1.; + } + }; + double shear_factor = has_z_shear ? compute_shear_factor(pcfg.belt_shear_z.value, pcfg.belt_shear_z_angle.value) : 0.; + double scale_z = compute_scale_factor(pcfg.belt_scale_z.value, pcfg.belt_scale_z_angle.value); + if (has_z_shear && std::abs(shear_factor) > EPSILON) { + int from = int(pcfg.belt_shear_z_from.value); + BoundingBoxf3 bb = this->model_object()->raw_bounding_box(); + double min_rz = std::numeric_limits::max(); + double max_rz = std::numeric_limits::lowest(); + for (double vz : {bb.min.z(), bb.max.z()}) + for (double vs : {bb.min(from), bb.max(from)}) { + double new_z = scale_z * (vz + shear_factor * vs); + min_rz = std::min(min_rz, new_z); + max_rz = std::max(max_rz, new_z); + } + object_height = max_rz - min_rz; + } else { + object_height *= scale_z; + } + } } - m_slicing_params = SlicingParameters::create_from_config(this->print()->config(), m_config, object_height, + m_slicing_params = SlicingParameters::create_from_config(pcfg, m_config, object_height, this->object_extruders(), this->print()->shrinkage_compensation()); } } @@ -3441,9 +3482,51 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full if (object_max_z <= 0.f) { BoundingBoxf3 bb = model_object.raw_bounding_box(); object_max_z = (float)bb.size().z(); + // Belt shear/scale may change the effective Z height. if (print_config.belt_printer.value) { - double angle_rad = Geometry::deg2rad(print_config.belt_printer_angle.value); - object_max_z = (float)(bb.size().y() * std::sin(angle_rad) + bb.size().z() * std::cos(angle_rad)); + bool has_z_shear = print_config.belt_shear_z.value != BeltShearMode::None; + bool has_z_scale = print_config.belt_scale_z.value != BeltScaleMode::None; + if (has_z_shear || has_z_scale) { + auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad); + switch (mode) { + case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; + case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; + case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; + case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; + default: return 0.; + } + }; + auto compute_scale_factor = [](BeltScaleMode mode, double angle_deg) -> double { + if (mode == BeltScaleMode::None) return 1.; + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad); + switch (mode) { + case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; + case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; + case BeltScaleMode::Sin: return sin_a; + case BeltScaleMode::Cos: return cos_a; + default: return 1.; + } + }; + double shear_factor = has_z_shear ? compute_shear_factor(print_config.belt_shear_z.value, print_config.belt_shear_z_angle.value) : 0.; + double scale_z = compute_scale_factor(print_config.belt_scale_z.value, print_config.belt_scale_z_angle.value); + if (has_z_shear && std::abs(shear_factor) > EPSILON) { + int from = int(print_config.belt_shear_z_from.value); + double min_rz = std::numeric_limits::max(); + double max_rz = std::numeric_limits::lowest(); + for (double vz : {bb.min.z(), bb.max.z()}) + for (double vs : {bb.min(from), bb.max(from)}) { + double new_z = scale_z * (vz + shear_factor * vs); + min_rz = std::min(min_rz, new_z); + max_rz = std::max(max_rz, new_z); + } + object_max_z = (float)(max_rz - min_rz); + } else { + object_max_z *= (float)scale_z; + } + } } } return SlicingParameters::create_from_config(print_config, object_config, object_max_z, object_extruders, object_shrinkage_compensation); diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 2748151aa0..294c245eaa 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -141,23 +141,69 @@ static std::vector slice_volumes_inner( params_base.extra_offset = 0; params_base.trafo = object_trafo; if (print_config.belt_printer.value) { - double angle_rad = Geometry::deg2rad(print_config.belt_printer_angle.value); - // Rotate mesh by -alpha about X so horizontal slice planes = gantry-parallel planes. - // The gantry (XY) is tilted by belt_printer_angle; this rotation aligns it with horizontal. - Transform3d belt_rotation = Transform3d::Identity(); - belt_rotation.rotate(Eigen::AngleAxisd(-angle_rad, Vec3d::UnitX())); - params_base.trafo = belt_rotation * params_base.trafo; - // Compute Z-shift from model_volumes: find min-Z of all rotated meshes - // so the rotated geometry starts at Z=0 (the belt surface in slicing frame). - double min_z_rotated = std::numeric_limits::max(); - for (const ModelVolume *mv : model_volumes) { - if (!model_volume_needs_slicing(*mv)) continue; - BoundingBoxf3 bb = mv->mesh().bounding_box(); - bb = bb.transformed(params_base.trafo * mv->get_matrix()); - min_z_rotated = std::min(min_z_rotated, bb.min.z()); + // Build per-axis shear matrix from 3 independent axis configs. + auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad); + double cos_a = std::cos(angle_rad); + switch (mode) { + case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; + case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; + case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; + case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; + default: return 0.; + } + }; + + struct AxisShear { BeltShearMode mode; double angle; int from; }; + AxisShear axes[3] = { + { print_config.belt_shear_x.value, print_config.belt_shear_x_angle.value, int(print_config.belt_shear_x_from.value) }, + { print_config.belt_shear_y.value, print_config.belt_shear_y_angle.value, int(print_config.belt_shear_y_from.value) }, + { print_config.belt_shear_z.value, print_config.belt_shear_z_angle.value, int(print_config.belt_shear_z_from.value) }, + }; + + Transform3d belt_shear = Transform3d::Identity(); + bool has_shear = false; + for (int row = 0; row < 3; ++row) { + if (axes[row].mode != BeltShearMode::None) { + double factor = compute_shear_factor(axes[row].mode, axes[row].angle); + if (std::abs(factor) > EPSILON) { + belt_shear.matrix()(row, axes[row].from) += factor; + has_shear = true; + } + } } - if (min_z_rotated != std::numeric_limits::max() && std::abs(min_z_rotated) > EPSILON) - params_base.trafo = Eigen::Translation3d(0, 0, -min_z_rotated) * params_base.trafo; + + // Build per-axis scale matrix. + auto compute_scale_factor = [](BeltScaleMode mode, double angle_deg) -> double { + if (mode == BeltScaleMode::None) return 1.; + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad); + double cos_a = std::cos(angle_rad); + switch (mode) { + case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; + case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; + case BeltScaleMode::Sin: return sin_a; + case BeltScaleMode::Cos: return cos_a; + default: return 1.; + } + }; + + Transform3d belt_scale = Transform3d::Identity(); + bool has_scale = false; + double sx = compute_scale_factor(print_config.belt_scale_x.value, print_config.belt_scale_x_angle.value); + double sy = compute_scale_factor(print_config.belt_scale_y.value, print_config.belt_scale_y_angle.value); + double sz = compute_scale_factor(print_config.belt_scale_z.value, print_config.belt_scale_z_angle.value); + if (std::abs(sx - 1.) > EPSILON || std::abs(sy - 1.) > EPSILON || std::abs(sz - 1.) > EPSILON) { + belt_scale.matrix()(0, 0) = sx; + belt_scale.matrix()(1, 1) = sy; + belt_scale.matrix()(2, 2) = sz; + has_scale = true; + } + + // Apply: scale * shear * trafo (shear first, then scale). + if (has_shear || has_scale) + params_base.trafo = belt_scale * belt_shear * params_base.trafo; } //BBS: 0.0025mm is safe enough to simplify the data to speed slicing up for high-resolution model. //Also has on influence on arc fitting which has default resolution 0.0125mm. diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index dd66c918a7..25b4ba4e9c 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1271,7 +1271,7 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const m_max_print_height = gcode_result.printable_height; m_z_offset = gcode_result.z_offset; - m_belt_z_shift = gcode_result.belt_z_shift; + // load_toolpaths(gcode_result, build_volume, exclude_bounding_box); @@ -2210,15 +2210,17 @@ void GCodeViewer::render_toolpaths() { const Camera& camera = wxGetApp().plater()->get_camera(); Matrix4f view = camera.get_view_matrix().matrix().cast(); - // Belt "raw" view: apply slicing rotation to view matrix so toolpaths appear - // in the slicing frame (rotated part with horizontal layers). - if (m_belt_show_raw && m_belt_view_enabled && m_belt_angle_deg > 0.f) { + // Belt "designed" view: apply inverse shear to view matrix so toolpaths appear + // upright (as originally designed) instead of sheared on the belt. + if (m_belt_show_designed && m_belt_view_enabled && m_belt_angle_deg > 0.f) { double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle_deg)); - // Apply R(-alpha, X) * T(0,0,-z_shift) to bring machine coords back to slicing frame. - Transform3d slicing_trafo = Transform3d::Identity(); - slicing_trafo.translate(Vec3d(0., 0., -static_cast(m_belt_z_shift))); - slicing_trafo = Eigen::AngleAxisd(-angle_rad, Vec3d::UnitX()) * slicing_trafo; - view = (camera.get_view_matrix() * slicing_trafo).matrix().cast(); + double sin_a = std::sin(angle_rad); + if (sin_a > 1e-6) { + double cot_alpha = std::cos(angle_rad) / sin_a; + Transform3d inverse_shear = Transform3d::Identity(); + inverse_shear.matrix()(1, 2) = -cot_alpha; // Y -= Z * cot(α) + view = (camera.get_view_matrix() * inverse_shear).matrix().cast(); + } } const libvgcode::Mat4x4 converted_view_matrix = libvgcode::convert(view); const libvgcode::Mat4x4 converted_projetion_matrix = libvgcode::convert(static_cast(camera.get_projection_matrix().matrix().cast())); @@ -4421,7 +4423,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv ImGui::Spacing(); ImGui::Dummy({ window_padding, 0 }); ImGui::SameLine(); - ImGui::Checkbox("Show raw G-code (slicing frame)", &m_belt_show_raw); + ImGui::Checkbox("Show designed view (upright)", &m_belt_show_designed); } legend_height = ImGui::GetCurrentWindow()->Size.y; diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 4bd3746991..184c56e915 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -239,8 +239,7 @@ mutable bool m_no_render_path { false }; bool m_belt_view_enabled = false; float m_belt_angle_deg = 0.f; - float m_belt_z_shift = 0.f; - bool m_belt_show_raw = false; // Toggle: show raw slicing-frame G-code (rotated part) + bool m_belt_show_designed = false; // Toggle: show designed (upright) view via inverse shear libvgcode::Viewer m_viewer; bool m_loaded_as_preview{ false }; diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index fafe46c952..87e97c5ff7 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4369,6 +4369,53 @@ void TabPrinter::build_fff() optgroup->append_single_option_line("belt_printer"); optgroup->append_single_option_line("belt_printer_angle"); optgroup->append_single_option_line("belt_printer_infinite_y"); + // Per-axis shear: group mode + angle + source on one row per axis + { + Line line = { L("Shear X"), L("Shear applied to the X axis before slicing") }; + line.append_option(optgroup->get_option("belt_shear_x")); + line.append_option(optgroup->get_option("belt_shear_x_angle")); + line.append_option(optgroup->get_option("belt_shear_x_from")); + optgroup->append_line(line); + } + { + Line line = { L("Shear Y"), L("Shear applied to the Y axis before slicing") }; + line.append_option(optgroup->get_option("belt_shear_y")); + line.append_option(optgroup->get_option("belt_shear_y_angle")); + line.append_option(optgroup->get_option("belt_shear_y_from")); + optgroup->append_line(line); + } + { + Line line = { L("Shear Z"), L("Shear applied to the Z axis before slicing") }; + line.append_option(optgroup->get_option("belt_shear_z")); + line.append_option(optgroup->get_option("belt_shear_z_angle")); + line.append_option(optgroup->get_option("belt_shear_z_from")); + optgroup->append_line(line); + } + { + Line line = { L("Scale X"), L("Scale applied to the X axis before slicing") }; + line.append_option(optgroup->get_option("belt_scale_x")); + line.append_option(optgroup->get_option("belt_scale_x_angle")); + optgroup->append_line(line); + } + { + Line line = { L("Scale Y"), L("Scale applied to the Y axis before slicing") }; + line.append_option(optgroup->get_option("belt_scale_y")); + line.append_option(optgroup->get_option("belt_scale_y_angle")); + optgroup->append_line(line); + } + { + Line line = { L("Scale Z"), L("Scale applied to the Z axis before slicing") }; + line.append_option(optgroup->get_option("belt_scale_z")); + line.append_option(optgroup->get_option("belt_scale_z_angle")); + optgroup->append_line(line); + } + { + Line line = { L("G-code axis remap"), L("Remap slicing-frame axes to machine axes in G-code output") }; + line.append_option(optgroup->get_option("belt_gcode_remap_x")); + line.append_option(optgroup->get_option("belt_gcode_remap_y")); + line.append_option(optgroup->get_option("belt_gcode_remap_z")); + optgroup->append_line(line); + } optgroup->append_single_option_line("support_multi_bed_types","printer_basic_information_printable_space#support-multi-bed-types"); optgroup->append_single_option_line("best_object_pos", "printer_basic_information_printable_space#best-object-position"); // todo: for multi_extruder test @@ -5232,6 +5279,10 @@ void TabPrinter::toggle_options() bool is_belt = m_config->opt_bool("belt_printer"); toggle_line("belt_printer_angle", is_belt); toggle_line("belt_printer_infinite_y", is_belt); + for (auto el : {"belt_shear_x", "belt_shear_y", "belt_shear_z", + "belt_scale_x", "belt_scale_y", "belt_scale_z", + "belt_gcode_remap_x"}) + toggle_line(el, is_belt); } From 7ff6bc42b14ce245b14c53c0c18bc79e892dea99 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 18 Mar 2026 12:09:28 -0500 Subject: [PATCH 005/224] Part 2.6: Add belt floor support clipping for all support types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix support clipping z-shift calculation by removing coordinate-space mismatch and sync belt_floor_z_shift with global_z_offset; fix invalidation so posSupportMaterial no longer resets slicing params - Add belt floor polygon clipping to non-organic tree support (slim/strong/hybrid) with collision surface integration in TreeSupportData, belt extension layers, and first-layer brim suppression - Add belt floor clipping to organic tree support pipeline with virtual belt raft layers, per-layer polygons in TreeModelVolumes, and post-generation layer trimming; fix pre-existing processing_last_mesh bug in calculateCollision() Fix belt floor support clipping: z-shift, invalidation, and global offset - Fix support clipping z-shift calculation by removing coordinate-space mismatch (raw_bounding_box min.z vs trafo_centered m_belt_min_z) and sync belt_floor_z_shift with global_z_offset in global shear mode - Fix invalidation so posSupportMaterial no longer resets slicing params, preventing the exact posSlice z-shift from being overwritten by the bounding-box approximation on support-only setting changes - Remove double-counting of global z_offset on support layers — support already inherits the offset from object layers during generation This Work Was Co-Authored-By Claude Opus 4.6 (1M context) UI: gray out inactive belt sub-options, rename to mesh transforms, move to Advanced Fix mesh clipping through build plate after belt shear/scale transform Generalize G-code viewer designed-view toggle for full belt transform Clip support layers to transformed belt floor plane Supports below the tilted build plate (Z = shear_factor * from_axis - min_z) are now clipped via half-plane intersection after generation. Belt floor parameters stored in SlicingParameters and populated in both update_slicing_parameters() and the static slicing_parameters() overload. Make belt G-code viewer toggle more prominent, add B keyboard shortcut - Add separator + teal "Belt Printer" header in legend panel - Append [B] hint to checkbox label - Add B key shortcut in GLCanvas3D to toggle designed/machine view - Read belt_printer_angle from loaded G-code headers to enable belt view Add per-axis global transform option for belt printer shear New belt_shear_{x,y,z}_global bool configs. When enabled, shear incorporates instance shift so objects at different bed positions get position-aware transform (Z += factor * instance_shift_on_from_axis). Fix global shear: use layer Z offset instead of mesh transform, add config invalidation - Global shear offset applied as post-slicing layer print_z adjustment instead of mesh transform (which was absorbed by min_z normalization or shifted mesh out of slice range) - Register all belt transform options in Print::invalidate_state_by_config_options to trigger posSlice re-slicing (the fallback only invalidated Print steps, not PrintObject steps — belt changes had no effect without manual re-slice) - Belt gcode remap options added to steps_gcode (gcode-export only) - Skip empty-first-layer check for belt objects with global Z offset WIP: split instances for global shear, relative Z offsets, debug logging - PrintApply: when belt global mode active, prevent instance grouping by adding unique Z perturbation to trafo — each copy becomes its own PrintObject with independent layers - PrintObjectSlice: compute global Z offset relative to minimum Y shift across all PrintObjects (lowest-Y object stays at Z=0) - Debug logging (warning level) for belt global shift values and offsets Known issues: - Cached posSlice results cause stale offsets when mixing copies with individually-added objects — need to compute min baseline outside slice() - Supports still generate to Z=0 instead of object's global Z offset Fix global shear for copied objects: disable shared-object layer optimization When belt global Z shear is active, each object needs unique layer Z values based on its bed position. The shared-object optimization was causing copies to reuse the source object's layers (and its Z offset) instead of computing their own position-based offset. started work on getting supports to work properly one step forward, one step back this version didn't quite work. Getting somewhere though about to add UI controllable tests added configuration options for supports tweak CLAUDE.md to be more aggressive for my machine. This commit should probably be pulled out before contributing upstream still chasing down some bugs moving objects between slices no longer results in improper Z-height because of caching added more data to the debug logs Z offset is getting more global again still not quite there, I think there's a fundamental logic flaw? hunting for bugs finally have a functional fix Add belt floor clipping to tree supports (organic and non-organic) - Add belt floor polygon clipping to non-organic tree support (slim/strong/hybrid) in draw_circles() and terminate nodes at the belt surface instead of the horizontal build plate - Add belt floor clipping to organic tree support pipeline with virtual belt raft layers for sub-floor branch generation, per-layer belt floor polygons in TreeModelVolumes, and post-generation layer trimming - Fix pre-existing processing_last_mesh bug in TreeModelVolumes that prevented m_anti_overhang (support blockers) from ever being applied; skip empty first layer check for belt printers Commits: current approach: make a face surface to build supports to closer! supports now terminate on shear plane, now need to get shear plane to correct Z height nearly there chasing down logic issues still committing for checkpoint, this still does not work still got logic problems... cull support clipping stashing changes for now. Going to focus on getting the global shear OFF support generation dialed first. beginning per object shear calcs Local shear transform is on correct Z offset now local shear finally works now and needs more testing global shear works now, needs thorough testing debugging non-45 degree angles debugging part 2 supports at all angles work now remove debug logging Add belt floor collision to non-organic tree support pipeline - Integrate belt floor as a collision surface in TreeSupportData so branches route around the belt naturally, replacing the explicit termination checks in drop_nodes() - Add belt extension layers below the object after draw_circles() to allow support geometry to extend to the diagonal belt surface instead of terminating at a horizontal first layer - Fix coordinate overflow in belt floor polygons (scale_(1e4) exceeds int32), skip first-layer brim expansion for belt printers, and extend empty first layer check bypass to all belt modes add debug logging, Z translate for tree supports still not seeing any cutoff surface yet adding debug options attempt #2 at trees if hit Z buildplate stop but don't set to_buildplate true getting closer tree support almost there, just need to get rid of the circles at the beginning getting closer belt / shear plane clip works, need to figure out the buidlplate plane issues more logic, added debugging logs supports now extend somewhat below Z=0 in global shear mode fix bad alloc, add 10mm below build plate fully works now shear transform + prusa tree support generation works now. pull out debug logging --- CLAUDE.md | 22 +- src/libslic3r/BuildVolume.hpp | 2 +- src/libslic3r/GCode.cpp | 30 ++- src/libslic3r/GCode/GCodeProcessor.cpp | 78 ++++++ src/libslic3r/GCode/GCodeProcessor.hpp | 33 +++ src/libslic3r/Preset.cpp | 10 +- src/libslic3r/Print.cpp | 62 ++++- src/libslic3r/Print.hpp | 10 +- src/libslic3r/PrintApply.cpp | 30 ++- src/libslic3r/PrintConfig.cpp | 64 +++++ src/libslic3r/PrintConfig.hpp | 23 ++ src/libslic3r/PrintObject.cpp | 116 ++++++++- src/libslic3r/PrintObjectSlice.cpp | 124 ++++++++- src/libslic3r/Slicing.hpp | 7 + src/libslic3r/Support/SupportCommon.hpp | 6 + src/libslic3r/Support/SupportMaterial.cpp | 290 ++++++++++++++++++++- src/libslic3r/Support/TreeModelVolumes.cpp | 117 ++++++++- src/libslic3r/Support/TreeModelVolumes.hpp | 3 + src/libslic3r/Support/TreeSupport.cpp | 98 ++++++- src/libslic3r/Support/TreeSupport.hpp | 4 + src/libslic3r/Support/TreeSupport3D.cpp | 74 ++++++ src/slic3r/GUI/GCodeViewer.cpp | 26 +- src/slic3r/GUI/GCodeViewer.hpp | 4 + src/slic3r/GUI/GLCanvas3D.cpp | 12 +- src/slic3r/GUI/GUI_ObjectList.cpp | 2 +- src/slic3r/GUI/Plater.cpp | 86 +++++- src/slic3r/GUI/Tab.cpp | 141 ++++++---- 27 files changed, 1337 insertions(+), 137 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 25aa516a56..9bd162c42c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,27 +23,7 @@ cmake --build build/arm64 --config RelWithDebInfo --target all -- ### Building on Linux **Always use this command to build the project when testing build issues on Linux.** ```bash -cmake --build build/arm64 --config RelWithDebInfo --target all -- - -``` -### Build test: - -**Always use this command to build the project when testing build issues on Windows.** -```bash -cmake --build . --config %build_type% --target ALL_BUILD -- -m -``` - -### Building on macOS -**Always use this command to build the project when testing build issues on macOS.** -```bash -cmake --build build/arm64 --config RelWithDebInfo --target all -- -``` - -### Building on Linux - **Always use this command to build the project when testing build issues on Linux.** -```bash -cmake --build build --config RelWithDebInfo --target all -- - +systemd-run --user --scope -p MemoryMax=48G cmake --build build --config RelWithDebInfo --target all -- -j18 -l 24 ``` diff --git a/src/libslic3r/BuildVolume.hpp b/src/libslic3r/BuildVolume.hpp index 0494efe9ba..2b0d8c2746 100644 --- a/src/libslic3r/BuildVolume.hpp +++ b/src/libslic3r/BuildVolume.hpp @@ -84,7 +84,7 @@ public: indexed_triangle_set bounding_mesh(bool scale=true) const; // Center of the print bed, unscaled. - Vec2d bed_center() const { return to_2d(m_bboxf.center()); } + Vec2d bed_center() const { return get_extents(m_bed_shape).center(); } // Convex hull of polygon(), scaled. const Polygon& convex_hull() const { return m_convex_hull; } // Smallest enclosing circle of polygon(), scaled. diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 02a233e988..41cfa7b72a 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -1745,8 +1745,16 @@ std::vector GCode::collect_layers_to_print(const PrintObjec // Check that there are extrusions on the very first layer. The case with empty // first layer may result in skirt/brim in the air and maybe other issues. + // Skip this check for belt printers. The shear transform tilts the + // model so the first horizontal layer plane intersects only a thin + // sliver of the model (width ≈ first_layer_height / shear_factor). + // This sliver is often narrower than the nozzle diameter, producing + // zero perimeters and an empty first layer — which is expected, not + // an error. In global shear mode the object may also start above + // Z=0 on the tilted belt surface. if (layers_to_print.size() == 1u) { - if (!has_extrusions) + bool skip_empty_check = object.print()->config().belt_printer.value; + if (!has_extrusions && !skip_empty_check) throw Slic3r::SlicingError(_(L("One object has an empty first layer and can't be printed. Please Cut the bottom or enable supports.")), object.id().id); } @@ -2515,9 +2523,27 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.write_format("; HEADER_BLOCK_START\n"); // Write information on the generator. file.write_format("; generated by %s on %s\n", Slic3r::header_slic3r_generated().c_str(), Slic3r::Utils::local_timestamp().c_str()); - // Belt printer: embed angle in header for G-code processor detection. + // Belt printer: embed angle and transform configs in header for G-code processor detection. if (print.config().belt_printer.value) { file.write_format("; belt_printer_angle = %.1f\n", print.config().belt_printer_angle.value); + // Shear configs + const auto &full_cfg = print.full_print_config(); + file.write_format("; belt_shear_x = %s\n", full_cfg.opt_serialize("belt_shear_x").c_str()); + file.write_format("; belt_shear_x_angle = %.1f\n", print.config().belt_shear_x_angle.value); + file.write_format("; belt_shear_x_from = %s\n", full_cfg.opt_serialize("belt_shear_x_from").c_str()); + file.write_format("; belt_shear_y = %s\n", full_cfg.opt_serialize("belt_shear_y").c_str()); + file.write_format("; belt_shear_y_angle = %.1f\n", print.config().belt_shear_y_angle.value); + file.write_format("; belt_shear_y_from = %s\n", full_cfg.opt_serialize("belt_shear_y_from").c_str()); + file.write_format("; belt_shear_z = %s\n", full_cfg.opt_serialize("belt_shear_z").c_str()); + file.write_format("; belt_shear_z_angle = %.1f\n", print.config().belt_shear_z_angle.value); + file.write_format("; belt_shear_z_from = %s\n", full_cfg.opt_serialize("belt_shear_z_from").c_str()); + // Scale configs + file.write_format("; belt_scale_x = %s\n", full_cfg.opt_serialize("belt_scale_x").c_str()); + file.write_format("; belt_scale_x_angle = %.1f\n", print.config().belt_scale_x_angle.value); + file.write_format("; belt_scale_y = %s\n", full_cfg.opt_serialize("belt_scale_y").c_str()); + file.write_format("; belt_scale_y_angle = %.1f\n", print.config().belt_scale_y_angle.value); + file.write_format("; belt_scale_z = %s\n", full_cfg.opt_serialize("belt_scale_z").c_str()); + file.write_format("; belt_scale_z_angle = %.1f\n", print.config().belt_scale_z_angle.value); } if (is_bbl_printers) file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str()); diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 7ac3a1ffc4..8def7c08db 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -3051,6 +3051,84 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers } catch (...) {} return; } + // Belt printer: parse shear configs from header comments. + { + auto parse_shear_mode = [](const std::string &s) -> BeltShearMode { + if (s == "pos_cot") return BeltShearMode::PosCot; + if (s == "neg_cot") return BeltShearMode::NegCot; + if (s == "pos_tan") return BeltShearMode::PosTan; + if (s == "neg_tan") return BeltShearMode::NegTan; + return BeltShearMode::None; + }; + auto parse_axis = [](const std::string &s) -> BeltAxis { + if (s == "y") return BeltAxis::Y; + if (s == "z") return BeltAxis::Z; + return BeltAxis::X; + }; + auto parse_scale_mode = [](const std::string &s) -> BeltScaleMode { + if (s == "inv_sin") return BeltScaleMode::InvSin; + if (s == "inv_cos") return BeltScaleMode::InvCos; + if (s == "sin") return BeltScaleMode::Sin; + if (s == "cos") return BeltScaleMode::Cos; + return BeltScaleMode::None; + }; + 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); + }; + // Shear X + if (boost::starts_with(comment, " belt_shear_x = ")) { + m_result.belt_shear_x = parse_shear_mode(trim(std::string(comment.substr(16)))); return; + } + if (boost::starts_with(comment, " belt_shear_x_angle = ")) { + try { m_result.belt_shear_x_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; + } + if (boost::starts_with(comment, " belt_shear_x_from = ")) { + m_result.belt_shear_x_from = parse_axis(trim(std::string(comment.substr(21)))); return; + } + // Shear Y + if (boost::starts_with(comment, " belt_shear_y = ")) { + m_result.belt_shear_y = parse_shear_mode(trim(std::string(comment.substr(16)))); return; + } + if (boost::starts_with(comment, " belt_shear_y_angle = ")) { + try { m_result.belt_shear_y_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; + } + if (boost::starts_with(comment, " belt_shear_y_from = ")) { + m_result.belt_shear_y_from = parse_axis(trim(std::string(comment.substr(21)))); return; + } + // Shear Z + if (boost::starts_with(comment, " belt_shear_z = ")) { + m_result.belt_shear_z = parse_shear_mode(trim(std::string(comment.substr(16)))); return; + } + if (boost::starts_with(comment, " belt_shear_z_angle = ")) { + try { m_result.belt_shear_z_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; + } + if (boost::starts_with(comment, " belt_shear_z_from = ")) { + m_result.belt_shear_z_from = parse_axis(trim(std::string(comment.substr(21)))); return; + } + // Scale X + if (boost::starts_with(comment, " belt_scale_x = ")) { + m_result.belt_scale_x = parse_scale_mode(trim(std::string(comment.substr(16)))); return; + } + if (boost::starts_with(comment, " belt_scale_x_angle = ")) { + try { m_result.belt_scale_x_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; + } + // Scale Y + if (boost::starts_with(comment, " belt_scale_y = ")) { + m_result.belt_scale_y = parse_scale_mode(trim(std::string(comment.substr(16)))); return; + } + if (boost::starts_with(comment, " belt_scale_y_angle = ")) { + try { m_result.belt_scale_y_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; + } + // Scale Z + if (boost::starts_with(comment, " belt_scale_z = ")) { + m_result.belt_scale_z = parse_scale_mode(trim(std::string(comment.substr(16)))); return; + } + if (boost::starts_with(comment, " belt_scale_z_angle = ")) { + try { m_result.belt_scale_z_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; + } + } // wipe start tag if (boost::starts_with(comment, reserved_tag(ETags::Wipe_Start))) { m_wiping = true; diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index e432a165a8..f953345f4e 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -229,6 +229,23 @@ class Print; float z_offset; // Belt printer: angle for coordinate transformation in preview. float belt_printer_angle{ 0.f }; + // Belt printer: per-axis shear config. + BeltShearMode belt_shear_x{ BeltShearMode::None }; + float belt_shear_x_angle{ 45.f }; + BeltAxis belt_shear_x_from{ BeltAxis::Y }; + BeltShearMode belt_shear_y{ BeltShearMode::None }; + float belt_shear_y_angle{ 45.f }; + BeltAxis belt_shear_y_from{ BeltAxis::Y }; + BeltShearMode belt_shear_z{ BeltShearMode::None }; + float belt_shear_z_angle{ 45.f }; + BeltAxis belt_shear_z_from{ BeltAxis::Y }; + // Belt printer: per-axis scale config. + BeltScaleMode belt_scale_x{ BeltScaleMode::None }; + float belt_scale_x_angle{ 45.f }; + BeltScaleMode belt_scale_y{ BeltScaleMode::None }; + float belt_scale_y_angle{ 45.f }; + BeltScaleMode belt_scale_z{ BeltScaleMode::None }; + float belt_scale_z_angle{ 45.f }; SettingsIds settings_ids; size_t filaments_count; bool backtrace_enabled; @@ -288,6 +305,22 @@ class Print; layer_filaments = other.layer_filaments; filament_change_count_map = other.filament_change_count_map; initial_layer_time = other.initial_layer_time; + belt_printer_angle = other.belt_printer_angle; + belt_shear_x = other.belt_shear_x; + belt_shear_x_angle = other.belt_shear_x_angle; + belt_shear_x_from = other.belt_shear_x_from; + belt_shear_y = other.belt_shear_y; + belt_shear_y_angle = other.belt_shear_y_angle; + belt_shear_y_from = other.belt_shear_y_from; + belt_shear_z = other.belt_shear_z; + belt_shear_z_angle = other.belt_shear_z_angle; + belt_shear_z_from = other.belt_shear_z_from; + belt_scale_x = other.belt_scale_x; + belt_scale_x_angle = other.belt_scale_x_angle; + belt_scale_y = other.belt_scale_y; + belt_scale_y_angle = other.belt_scale_y_angle; + belt_scale_z = other.belt_scale_z; + belt_scale_z_angle = other.belt_scale_z_angle; #if ENABLE_GCODE_VIEWER_STATISTICS time = other.time; #endif diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 9bc5e0a782..9437b6098e 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1010,11 +1010,13 @@ static std::vector s_Preset_machine_limits_options { static std::vector s_Preset_printer_options { "printer_technology", - "printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "build_plate_tilt_x", "build_plate_tilt_y", "belt_printer", "belt_printer_angle", "belt_printer_infinite_y", "belt_shear_x", "belt_shear_x_angle", "belt_shear_x_from", - "belt_shear_y", "belt_shear_y_angle", "belt_shear_y_from", - "belt_shear_z", "belt_shear_z_angle", "belt_shear_z_from", + "printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "build_plate_tilt_x", "build_plate_tilt_y", "belt_printer", "belt_printer_angle", "belt_printer_infinite_y", "belt_shear_x", "belt_shear_x_angle", "belt_shear_x_from", "belt_shear_x_global", + "belt_shear_y", "belt_shear_y_angle", "belt_shear_y_from", "belt_shear_y_global", + "belt_shear_z", "belt_shear_z_angle", "belt_shear_z_from", "belt_shear_z_global", "belt_scale_x", "belt_scale_x_angle", "belt_scale_y", "belt_scale_y_angle", "belt_scale_z", "belt_scale_z_angle", - "belt_gcode_remap_x", "belt_gcode_remap_y", "belt_gcode_remap_z", "gcode_flavor", + "belt_gcode_remap_x", "belt_gcode_remap_y", "belt_gcode_remap_z", + "belt_support_floor_offset", "belt_support_floor_mode", "belt_support_z_offset_mode", + "gcode_flavor", "fan_kickstart", "fan_speedup_time", "fan_speedup_overhangs", "single_extruder_multi_material", "manual_filament_change", "file_start_gcode", "machine_start_gcode", "machine_end_gcode", "before_layer_change_gcode", "printing_by_object_gcode", "layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode", "change_filament_gcode", "change_extrusion_role_gcode", "printer_model", "printer_variant", "printer_extruder_id", "printer_extruder_variant", "extruder_variant_list", "default_nozzle_volume_type", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index f245cc32b6..0aef12a74b 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -99,6 +99,10 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n // Cache the plenty of parameters, which influence the G-code generator only, // or they are only notes not influencing the generated G-code. static std::unordered_set steps_gcode = { + // Belt printer G-code axis remap (only affects G-code output, not slicing). + "belt_gcode_remap_x", + "belt_gcode_remap_y", + "belt_gcode_remap_z", //BBS "additional_cooling_fan_speed", "reduce_crossing_wall", @@ -275,8 +279,34 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n // Spiral Vase forces different kind of slicing than the normal model: // In Spiral Vase mode, holes are closed and only the largest area contour is kept at each layer. // Therefore toggling the Spiral Vase on / off requires complete reslicing. - || opt_key == "spiral_mode") { + || opt_key == "spiral_mode" + // Belt printer transform options change the mesh geometry before slicing. + || opt_key == "belt_printer" + || opt_key == "belt_printer_angle" + || opt_key == "belt_shear_x" + || opt_key == "belt_shear_x_angle" + || opt_key == "belt_shear_x_from" + || opt_key == "belt_shear_x_global" + || opt_key == "belt_shear_y" + || opt_key == "belt_shear_y_angle" + || opt_key == "belt_shear_y_from" + || opt_key == "belt_shear_y_global" + || opt_key == "belt_shear_z" + || opt_key == "belt_shear_z_angle" + || opt_key == "belt_shear_z_from" + || opt_key == "belt_shear_z_global" + || opt_key == "belt_scale_x" + || opt_key == "belt_scale_x_angle" + || opt_key == "belt_scale_y" + || opt_key == "belt_scale_y_angle" + || opt_key == "belt_scale_z" + || opt_key == "belt_scale_z_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") { + osteps.emplace_back(posSupportMaterial); } else if ( opt_key == "print_sequence" || opt_key == "filament_type" @@ -2124,15 +2154,21 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) int object_count = m_objects.size(); std::set need_slicing_objects; std::set re_slicing_objects; + // Belt global Z shear: each object needs unique layer Z values based on + // its bed position, so sharing layers between "identical" objects is wrong. + bool belt_no_share = m_config.belt_printer.value && m_config.belt_shear_z_global.value + && m_config.belt_shear_z.value != BeltShearMode::None; if (!use_cache) { for (int index = 0; index < object_count; index++) { PrintObject *obj = m_objects[index]; - for (PrintObject *slicing_obj : need_slicing_objects) - { - if (is_print_object_the_same(obj, slicing_obj)) { - obj->set_shared_object(slicing_obj); - break; + if (!belt_no_share) { + for (PrintObject *slicing_obj : need_slicing_objects) + { + if (is_print_object_the_same(obj, slicing_obj)) { + obj->set_shared_object(slicing_obj); + break; + } } } if (!obj->get_shared_object()) @@ -2151,12 +2187,14 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) PrintObject *obj = m_objects[index]; bool found_shared = false; if (need_slicing_objects.find(obj) == need_slicing_objects.end()) { - for (PrintObject *slicing_obj : need_slicing_objects) - { - if (is_print_object_the_same(obj, slicing_obj)) { - obj->set_shared_object(slicing_obj); - found_shared = true; - break; + if (!belt_no_share) { + for (PrintObject *slicing_obj : need_slicing_objects) + { + if (is_print_object_the_same(obj, slicing_obj)) { + obj->set_shared_object(slicing_obj); + found_shared = true; + break; + } } } if (!found_shared) { diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index b98811c8e3..d0c050f442 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -573,7 +573,15 @@ private: PrintObject* m_shared_object{ nullptr }; - + // Belt printer: global Z offset applied to this object's layers for shear positioning. + double m_belt_global_z_offset { 0.0 }; + // Belt printer: min_z of mesh after belt shear (before Z-shift), for z_offset calc. + double m_belt_min_z { 0.0 }; +public: + double belt_global_z_offset() const { return m_belt_global_z_offset; } +private: + + // SoftFever // // object id diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index d2a44463ea..7f3b41b0f1 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -135,22 +135,29 @@ struct PrintObjectTrafoAndInstances // Generate a list of trafos and XY offsets for instances of a ModelObject // Orca: Updated to include XYZ filament shrinkage compensation -static std::vector print_objects_from_model_object(const ModelObject &model_object, const Vec3d &shrinkage_compensation) +static std::vector print_objects_from_model_object(const ModelObject &model_object, const Vec3d &shrinkage_compensation, bool force_separate_instances = false) { std::set trafos; PrintObjectTrafoAndInstances trafo; //BBS: add useful logs for debug int index = 0; + int unique_counter = 0; for (ModelInstance *model_instance : model_object.instances) { if (model_instance->is_printable()) { // Orca: Updated with XYZ filament shrinkage compensation Geometry::Transformation model_instance_transformation = model_instance->get_transformation(); trafo.trafo = model_instance_transformation.get_matrix_with_applied_shrinkage_compensation(shrinkage_compensation); - + auto shift = Point::new_scale(trafo.trafo.data()[12], trafo.trafo.data()[13]); // Reset the XY axes of the transformation. trafo.trafo.data()[12] = 0; trafo.trafo.data()[13] = 0; + // Belt printer global mode: prevent instance grouping so each + // copy gets its own PrintObject with independent layer Z values. + // Add a tiny unique perturbation to the existing Z (don't replace + // it — the Z translation from ensure_on_bed must be preserved). + if (force_separate_instances) + trafo.trafo.data()[14] += 1e-10 * (++unique_counter); // Search or insert a trafo. auto it = trafos.emplace(trafo).first; const_cast(*it).instances.emplace_back(PrintInstance{ nullptr, model_instance, shift }); @@ -1506,11 +1513,16 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ PrintObjectPtrs print_objects_new; print_objects_new.reserve(std::max(m_objects.size(), m_model.objects.size())); bool new_objects = false; + bool belt_instances_shifted = false; // Walk over all new model objects and check, whether there are matching PrintObjects. for (ModelObject *model_object : m_model.objects) { ModelObjectStatus &model_object_status = const_cast(model_object_status_db.reuse(*model_object)); // Orca: Updated for XYZ filament shrink compensation - model_object_status.print_instances = print_objects_from_model_object(*model_object, this->shrinkage_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_shear_z_global.value + && m_config.belt_shear_z.value != BeltShearMode::None; + model_object_status.print_instances = print_objects_from_model_object(*model_object, this->shrinkage_compensation(), belt_force_separate); std::vector old; old.reserve(print_object_status_db.count(*model_object)); for (const PrintObjectStatus &print_object_status : print_object_status_db.get_range(*model_object)) @@ -1558,6 +1570,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ if (status != PrintBase::APPLY_STATUS_UNCHANGED) { size_t extruder_num = new_full_config.option("nozzle_diameter")->size(); update_apply_status(status == PrintBase::APPLY_STATUS_INVALIDATED); + belt_instances_shifted = true; } print_objects_new.emplace_back((*it_old)->print_object); const_cast(*it_old)->status = PrintObjectStatus::Reused; @@ -1593,6 +1606,17 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ update_apply_status(object->invalidate_step(posSlice)); } } + + // 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_shear_z_global.value + && m_config.belt_shear_z.value != BeltShearMode::None) { + for (PrintObject *object : m_objects) + update_apply_status(object->invalidate_step(posSlice)); + } } //BBS: check the config again diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index a722a3bae7..7d06f96a85 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -327,6 +327,21 @@ static t_config_enum_values s_keys_map_BeltRemapAxis { }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltRemapAxis) +static t_config_enum_values s_keys_map_BeltSupportFloorMode { + { "none", int(BeltSupportFloorMode::None) }, + { "generator_only", int(BeltSupportFloorMode::GeneratorOnly) }, + { "clip_only", int(BeltSupportFloorMode::ClipOnly) }, + { "both", int(BeltSupportFloorMode::Both) }, +}; +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_SupportMaterialPattern { { "rectilinear", smpRectilinear }, { "rectilinear-grid", smpRectilinearGrid }, @@ -6047,17 +6062,29 @@ void PrintConfigDef::init_fff_params() def->set_default_value(new ConfigOptionEnum(default_axis)); }; + auto add_belt_shear_global = [this](const char *key, const char *label) { + auto def = this->add(key, coBool); + def->label = L(label); + def->category = L("Printable space"); + def->tooltip = L("Apply shear in global coordinates (position-aware) rather than object-local coordinates."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + }; + add_belt_shear_mode("belt_shear_x", "Function", BeltShearMode::None); add_belt_shear_angle("belt_shear_x_angle", "Angle"); add_belt_axis_enum("belt_shear_x_from", "From", "Source axis for X shear.", BeltAxis::Z); + add_belt_shear_global("belt_shear_x_global", "Global"); add_belt_shear_mode("belt_shear_y", "Function", BeltShearMode::PosCot); add_belt_shear_angle("belt_shear_y_angle", "Angle"); add_belt_axis_enum("belt_shear_y_from", "From", "Source axis for Y shear.", BeltAxis::Z); + add_belt_shear_global("belt_shear_y_global", "Global"); add_belt_shear_mode("belt_shear_z", "Function", BeltShearMode::None); add_belt_shear_angle("belt_shear_z_angle", "Angle"); add_belt_axis_enum("belt_shear_z_from", "From", "Source axis for Z shear.", BeltAxis::Y); + add_belt_shear_global("belt_shear_z_global", "Global"); // Per-axis scale controls for belt printer auto add_belt_scale_mode = [this](const char *key, const char *label, BeltScaleMode default_mode) { @@ -6109,6 +6136,43 @@ void PrintConfigDef::init_fff_params() add_belt_remap("belt_gcode_remap_y", "Y", "Which slicing axis maps to machine Y in G-code output.", BeltRemapAxis::PosY); add_belt_remap("belt_gcode_remap_z", "Z", "Which slicing axis maps to machine Z in G-code output.", BeltRemapAxis::PosZ); + // Belt support floor debug controls + def = this->add("belt_support_floor_offset", coFloat); + def->label = L("Floor Z offset"); + def->category = L("Printable space"); + def->tooltip = L("Shifts the computed belt floor up or down (mm). Negative values lower the floor, allowing more supports to survive. Use this to diagnose belt floor formula issues."); + def->sidetext = L("mm"); + def->min = -500; + def->max = 500; + 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::get_enum_values(); + def->enum_values = {"none", "generator_only"}; + def->enum_labels = {L("None"), L("Generator only")}; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(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::get_enum_values(); + def->enum_values = {"none", "unconditional", "raft_only"}; + def->enum_labels = {L("None"), L("Unconditional"), L("Raft only")}; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(BeltSupportZOffsetMode::Unconditional)); + } + def = this->add("tree_support_branch_angle", coFloat); def->label = L("Tree support branch angle"); def->category = L("Support"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 4f149d9c8b..fc533bf6bb 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -189,6 +189,21 @@ enum class BeltRemapAxis 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 + ClipOnly, // Only post-processing clipping + Both, // Both generator and clipping +}; + +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 +}; + enum SupportMaterialPattern { smpDefault, smpRectilinear, smpRectilinearGrid, smpHoneycomb, @@ -536,6 +551,8 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltShearMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltScaleMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltAxis) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltRemapAxis) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltSupportFloorMode) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltSupportZOffsetMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialPattern) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialStyle) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialInterfacePattern) @@ -1456,12 +1473,15 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionEnum, belt_shear_x)) ((ConfigOptionFloat, belt_shear_x_angle)) ((ConfigOptionEnum, belt_shear_x_from)) + ((ConfigOptionBool, belt_shear_x_global)) ((ConfigOptionEnum, belt_shear_y)) ((ConfigOptionFloat, belt_shear_y_angle)) ((ConfigOptionEnum, belt_shear_y_from)) + ((ConfigOptionBool, belt_shear_y_global)) ((ConfigOptionEnum, belt_shear_z)) ((ConfigOptionFloat, belt_shear_z_angle)) ((ConfigOptionEnum, belt_shear_z_from)) + ((ConfigOptionBool, belt_shear_z_global)) ((ConfigOptionEnum, belt_scale_x)) ((ConfigOptionFloat, belt_scale_x_angle)) ((ConfigOptionEnum, belt_scale_y)) @@ -1471,6 +1491,9 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionEnum, belt_gcode_remap_x)) ((ConfigOptionEnum, belt_gcode_remap_y)) ((ConfigOptionEnum, belt_gcode_remap_z)) + ((ConfigOptionFloat, belt_support_floor_offset)) + ((ConfigOptionEnum, belt_support_floor_mode)) + ((ConfigOptionEnum, belt_support_z_offset_mode)) //BBS ((ConfigOptionInts, additional_cooling_fan_speed)) ((ConfigOptionBool, reduce_crossing_wall)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index abd309fbff..f803cc0369 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -9,6 +9,7 @@ #include "MutablePolygon.hpp" #include "PrintConfig.hpp" #include "Support/SupportMaterial.hpp" +#include "Support/SupportCommon.hpp" #include "Support/SupportSpotsGenerator.hpp" #include "Support/TreeSupport.hpp" #include "Surface.hpp" @@ -1365,7 +1366,12 @@ bool PrintObject::invalidate_step(PrintObjectStep step) } else if (step == posSupportMaterial) { invalidated |= this->invalidate_steps({ posSimplifySupportPath }); invalidated |= m_print->invalidate_steps({ psSkirtBrim }); - m_slicing_params.valid = false; + // NOTE: do NOT set m_slicing_params.valid = false here. + // belt_floor_z_shift is patched to an exact value during posSlice + // (PrintObjectSlice.cpp, after slice_volumes). Invalidating slicing + // params here causes update_slicing_parameters() to overwrite that + // exact value with a bounding-box approximation, while posSlice does + // not re-run to correct it — breaking belt support clipping. } // Wipe tower depends on the ordering of extruders, which in turn depends on everything. @@ -3392,6 +3398,10 @@ void PrintObject::update_slicing_parameters() // Orca: updated function call for XYZ shrinkage compensation if (!m_slicing_params.valid) { coordf_t object_height = this->model_object()->max_z(); + // Belt floor parameters for support clipping (populated below if belt Z-shear is active). + double belt_floor_shear_factor_out = 0.0; + int belt_floor_from_axis_out = 1; + double belt_floor_z_shift_out = 0.0; // Belt shear/scale may change the effective Z height. const auto &pcfg = this->print()->config(); if (pcfg.belt_printer.value) { @@ -3435,6 +3445,12 @@ void PrintObject::update_slicing_parameters() max_rz = std::max(max_rz, new_z); } object_height = max_rz - min_rz; + belt_floor_shear_factor_out = shear_factor; + belt_floor_from_axis_out = from; + // Belt contact surface starts at bb.min.z() pre-shear; add the + // slicing Z-shift that keeps the mesh above Z=0. + // Exact value is patched after slice_volumes() in posSlice. + belt_floor_z_shift_out = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.); } else { object_height *= scale_z; } @@ -3442,6 +3458,10 @@ void PrintObject::update_slicing_parameters() } m_slicing_params = SlicingParameters::create_from_config(pcfg, m_config, object_height, this->object_extruders(), this->print()->shrinkage_compensation()); + // Populate belt floor parameters into slicing params for support clipping. + m_slicing_params.belt_floor_shear_factor = belt_floor_shear_factor_out; + m_slicing_params.belt_floor_from_axis = belt_floor_from_axis_out; + m_slicing_params.belt_floor_z_shift = belt_floor_z_shift_out; } } @@ -3479,6 +3499,11 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full sort_remove_duplicates(object_extruders); //FIXME add painting extruders + // Belt floor parameters for support clipping (populated below if belt Z-shear is active). + double belt_floor_shear_factor_out = 0.0; + int belt_floor_from_axis_out = 1; + double belt_floor_z_shift_out = 0.0; + if (object_max_z <= 0.f) { BoundingBoxf3 bb = model_object.raw_bounding_box(); object_max_z = (float)bb.size().z(); @@ -3523,13 +3548,20 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full max_rz = std::max(max_rz, new_z); } object_max_z = (float)(max_rz - min_rz); + belt_floor_shear_factor_out = shear_factor; + belt_floor_from_axis_out = from; + belt_floor_z_shift_out = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.); } else { object_max_z *= (float)scale_z; } } } } - return SlicingParameters::create_from_config(print_config, object_config, object_max_z, object_extruders, object_shrinkage_compensation); + SlicingParameters params = SlicingParameters::create_from_config(print_config, object_config, object_max_z, object_extruders, object_shrinkage_compensation); + params.belt_floor_shear_factor = belt_floor_shear_factor_out; + params.belt_floor_from_axis = belt_floor_from_axis_out; + params.belt_floor_z_shift = belt_floor_z_shift_out; + return params; } // returns 0-based indices of extruders used to print the object (without brim, support and other helper extrusions) @@ -4038,6 +4070,67 @@ void PrintObject::combine_infill() } } +// Belt printer: clip an ExtrusionEntityCollection to a region defined by clip_expoly. +// Handles ExtrusionPath, ExtrusionMultiPath, ExtrusionLoop, and nested ExtrusionEntityCollection. +static void clip_support_fills(ExtrusionEntityCollection &fills, const ExPolygons &clip_region) +{ + ExtrusionEntitiesPtr new_entities; + for (ExtrusionEntity *entity : fills.entities) { + if (auto *path = dynamic_cast(entity)) { + ExtrusionEntityCollection clipped; + path->intersect_expolygons(clip_region, &clipped); + if (!clipped.empty()) { + for (ExtrusionEntity *e : clipped.entities) + new_entities.push_back(e->clone()); + } + delete entity; + } else if (auto *multipath = dynamic_cast(entity)) { + ExtrusionPaths new_paths; + for (const ExtrusionPath &p : multipath->paths) { + ExtrusionEntityCollection clipped; + p.intersect_expolygons(clip_region, &clipped); + for (ExtrusionEntity *e : clipped.entities) + if (auto *cp = dynamic_cast(e)) + new_paths.push_back(std::move(*cp)); + } + if (!new_paths.empty()) { + multipath->paths = std::move(new_paths); + new_entities.push_back(multipath); + } else { + delete entity; + } + } else if (auto *loop = dynamic_cast(entity)) { + ExtrusionPaths new_paths; + for (const ExtrusionPath &p : loop->paths) { + ExtrusionEntityCollection clipped; + p.intersect_expolygons(clip_region, &clipped); + for (ExtrusionEntity *e : clipped.entities) + if (auto *cp = dynamic_cast(e)) + new_paths.push_back(std::move(*cp)); + } + if (!new_paths.empty()) { + // Loop is no longer a closed loop after clipping; emit as individual paths. + for (auto &p : new_paths) + new_entities.push_back(new ExtrusionPath(std::move(p))); + delete entity; + } else { + delete entity; + } + } else if (auto *coll = dynamic_cast(entity)) { + clip_support_fills(*coll, clip_region); + if (!coll->empty()) { + new_entities.push_back(coll); + } else { + delete entity; + } + } else { + // Unknown entity type — keep as-is. + new_entities.push_back(entity); + } + } + fills.entities = std::move(new_entities); +} + void PrintObject::_generate_support_material() { if (is_tree(m_config.support_type.value)) { @@ -4049,6 +4142,25 @@ void PrintObject::_generate_support_material() PrintObjectSupportMaterial support_material(this, m_slicing_params); support_material.generate(*this); } + // Global Z offset for support layers: + // - Normal support: layers already inherit global_z_offset from object layers. + // - 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 + // independently and does NOT inherit the offset — apply it here. + // Belt floor polygon clipping for non-organic tree support is done inside + // draw_circles() before area_groups and toolpaths are built. + if (is_tree(m_config.support_type.value) && std::abs(m_belt_global_z_offset) > EPSILON) { + // Resolve effective support style (same logic as SupportParameters). + auto style = m_config.support_style.value; + if (style == smsDefault) + style = smsTreeOrganic; + if (style == smsTreeOrganic) { + for (SupportLayer *sl : m_support_layers) + sl->print_z += m_belt_global_z_offset; + } + } + } // BBS diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 294c245eaa..404550da1a 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -1,4 +1,5 @@ #include +#include #include @@ -125,7 +126,8 @@ static std::vector slice_volumes_inner( ModelVolumePtrs model_volumes, const std::vector &layer_ranges, const std::vector &zs, - const std::function &throw_on_cancel_callback) + const std::function &throw_on_cancel_callback, + double *out_belt_min_z = nullptr) { model_volumes_sort_by_id(model_volumes); @@ -202,8 +204,32 @@ static std::vector slice_volumes_inner( } // Apply: scale * shear * trafo (shear first, then scale). - if (has_shear || has_scale) + if (has_shear || has_scale) { params_base.trafo = belt_scale * belt_shear * params_base.trafo; + + // After the shear/scale transform, the mesh may clip through the + // build plate (Z < 0). Detect this and shift the mesh up. + Transform3d combined = params_base.trafo; + double min_z = std::numeric_limits::max(); + for (const ModelVolume *mv : model_volumes) { + if (!mv->is_model_part()) continue; + for (const stl_vertex &v : mv->mesh().its.vertices) { + Vec3d pt = combined * v.cast(); + min_z = std::min(min_z, pt.z()); + } + } + double belt_z_shift_val = (min_z < 0. && min_z != std::numeric_limits::max()) ? -min_z : 0.; + BOOST_LOG_TRIVIAL(warning) << "Belt Z-shift: min_z=" << min_z + << " z_shift=" << belt_z_shift_val + << " trafo_z=" << object_trafo.matrix()(2, 3); + if (belt_z_shift_val > 0.) { + Transform3d z_shift = Transform3d::Identity(); + z_shift.matrix()(2, 3) = belt_z_shift_val; + params_base.trafo = z_shift * params_base.trafo; + } + if (out_belt_min_z) + *out_belt_min_z = (min_z != std::numeric_limits::max()) ? min_z : 0.; + } } //BBS: 0.0025mm is safe enough to simplify the data to speed slicing up for high-resolution model. //Also has on influence on arc fitting which has default resolution 0.0125mm. @@ -866,6 +892,20 @@ void PrintObject::slice() m_layers = new_layers(this, generate_object_layers(m_slicing_params, layer_height_profile, m_config.precise_z_height.value)); this->slice_volumes(); m_print->throw_if_canceled(); + + // After slicing, m_belt_min_z holds the exact post-shear minimum Z + // in trafo_centered space (which includes the ensure_on_bed Z offset). + // The belt surface is at Z=0 in trafo_centered space; after shear it + // becomes Z = sf*Y, and after the z-shift that keeps the mesh above + // Z=0 it becomes Z = sf*Y + z_shift_val. So belt_floor_z_shift is + // simply the z-shift applied, i.e. max(0, -m_belt_min_z). + // NOTE: do NOT add raw_bounding_box().min.z() here — m_belt_min_z + // already includes the ensure_on_bed offset, unlike the min_rz used + // in update_slicing_parameters() which needs that compensation. + if (std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON) { + m_slicing_params.belt_floor_z_shift = (m_belt_min_z < 0.) ? -m_belt_min_z : 0.; + } + int firstLayerReplacedBy = 0; #if 0 @@ -904,6 +944,83 @@ void PrintObject::slice() if (m_layers.empty()) throw Slic3r::SlicingError(L("No layers were detected. You might want to repair your STL file(s) or check their size or thickness and retry.\n")); + // Belt printer global mode: offset all layer Z values so objects at + // different bed positions print at different heights on the tilted belt. + // This is a post-slicing adjustment — the sliced geometry is identical + // regardless of global mode, only the output Z coordinates change. + { + const auto &pcfg = this->print()->config(); + BOOST_LOG_TRIVIAL(warning) << "Belt global check: belt_printer=" << pcfg.belt_printer.value + << " belt_shear_z=" << int(pcfg.belt_shear_z.value) + << " belt_shear_z_global=" << pcfg.belt_shear_z_global.value + << " object=" << this->model_object()->name; + if (pcfg.belt_printer.value) { + auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad); + double cos_a = std::cos(angle_rad); + switch (mode) { + case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; + case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; + case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; + case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; + default: return 0.; + } + }; + + Point inst_shift = this->instances().empty() ? Point(0, 0) + : this->instances().front().shift - this->center_offset(); + BOOST_LOG_TRIVIAL(warning) << "Belt global: object " << this->model_object()->name + << " instances=" << this->instances().size() + << " shift=(" << unscale(inst_shift.x()) << ", " << unscale(inst_shift.y()) << ")"; + double global_z_offset = 0.; + + struct GAxis { BeltShearMode mode; double angle; int from; bool global; }; + GAxis gaxes[3] = { + { pcfg.belt_shear_x.value, pcfg.belt_shear_x_angle.value, int(pcfg.belt_shear_x_from.value), pcfg.belt_shear_x_global.value }, + { pcfg.belt_shear_y.value, pcfg.belt_shear_y_angle.value, int(pcfg.belt_shear_y_from.value), pcfg.belt_shear_y_global.value }, + { pcfg.belt_shear_z.value, pcfg.belt_shear_z_angle.value, int(pcfg.belt_shear_z_from.value), pcfg.belt_shear_z_global.value }, + }; + + // Only the Z-row shear contributes a Z offset from global mode. + // (X/Y row shears with global would offset X/Y, not Z — not useful here.) + // Offsets are RELATIVE: we subtract the minimum shift across all + // PrintObjects so the lowest-positioned object stays at Z=0. + const auto &za = gaxes[2]; // Z row + if (za.global && za.mode != BeltShearMode::None && za.from < 2) { + double factor = compute_shear_factor(za.mode, za.angle); + // The Z-shift brought the mesh's lowest sheared vertex to + // Z=0. That vertex's physical Y determines the belt contact + // point. With trafo_z preserved (ensure_on_bed offset), + // min_z = Y_at_contact * factor for bottom-face vertices, + // so: z_offset = center_Y * factor + min_z. + Point phys = inst_shift; // already has center_offset subtracted + double center_on_axis = (za.from == 0) ? unscale(phys.x()) : unscale(phys.y()); + global_z_offset += center_on_axis * factor + m_belt_min_z; + } + + BOOST_LOG_TRIVIAL(warning) << "Belt global: z_offset=" << global_z_offset + << " za.global=" << za.global << " za.mode=" << int(za.mode) << " za.from=" << za.from + << " (relative to min across " << this->print()->objects().size() << " objects)"; + m_belt_global_z_offset = global_z_offset; + if (std::abs(global_z_offset) > EPSILON) { + for (Layer *layer : m_layers) + layer->print_z += global_z_offset; + // Keep belt floor clipping in sync with the shifted print_z + // values — the support generator sees globally-offset object + // layer print_z, so belt_floor_z_shift must match. + m_slicing_params.belt_floor_z_shift += global_z_offset; + } + if (!m_layers.empty()) { + BOOST_LOG_TRIVIAL(warning) << "Belt global: first_layer_z=" << m_layers.front()->print_z + << " last_layer_z=" << m_layers.back()->print_z + << " num_layers=" << m_layers.size() + << " center_offset=(" << unscale(m_center_offset.x()) + << ", " << unscale(m_center_offset.y()) << ")"; + } + } + } + // BBS this->set_done(posSlice); } @@ -1208,7 +1325,8 @@ void PrintObject::slice_volumes() if (!slice_zs.empty()) { objSliceByVolume = slice_volumes_inner( print->config(), this->config(), this->trafo_centered(), - this->model_object()->volumes, m_shared_regions->layer_ranges, slice_zs, throw_on_cancel_callback); + this->model_object()->volumes, m_shared_regions->layer_ranges, slice_zs, throw_on_cancel_callback, + &m_belt_min_z); } //BBS: "model_part" volumes are grouded according to their connections diff --git a/src/libslic3r/Slicing.hpp b/src/libslic3r/Slicing.hpp index a7b21a140d..b06f2a7bc4 100644 --- a/src/libslic3r/Slicing.hpp +++ b/src/libslic3r/Slicing.hpp @@ -110,6 +110,13 @@ struct SlicingParameters coordf_t object_print_z_uncompensated_max { 0 }; // Scaling factor for compensating shrinkage in Z-axis. coordf_t object_shrinkage_compensation_z { 0 }; + + // Belt printer: floor plane parameters for support clipping. + // Belt contact surface in slicing coords: Z = bb_min_z + sf*Y + slicing_z_shift. + // cutoff = (print_z - belt_floor_z_shift - floor_offset) / shear_factor + double belt_floor_shear_factor { 0.0 }; // shear factor (e.g. cot(45deg)) + int belt_floor_from_axis { 1 }; // which axis the shear is from (0=X, 1=Y) + double belt_floor_z_shift { 0.0 }; // bb_min_z + max(0, -min_z_after_shear) }; static_assert(IsTriviallyCopyable::value, "SlicingParameters class is not POD (and it should be - see constructor)."); diff --git a/src/libslic3r/Support/SupportCommon.hpp b/src/libslic3r/Support/SupportCommon.hpp index 531daaa0a8..f15c7d417a 100644 --- a/src/libslic3r/Support/SupportCommon.hpp +++ b/src/libslic3r/Support/SupportCommon.hpp @@ -144,6 +144,12 @@ int idx_lower_or_equal(const std::vector &vec, int idx, FN_LOWER_EQUAL fn_lo return idx_lower_or_equal(vec.begin(), vec.end(), idx, fn_lower_equal); } +// Belt floor: compute the belt-side half-plane polygon at a given print_z. +// Used to clip support polygons against the belt surface. +Polygons belt_floor_surface_polygon( + const SlicingParameters &slicing_params, const PrintConfig &print_config, + const PrintObject &object, coordf_t print_z); + } // namespace Slic3r #endif /* slic3r_SupportCommon_hpp_ */ diff --git a/src/libslic3r/Support/SupportMaterial.cpp b/src/libslic3r/Support/SupportMaterial.cpp index c230924ac8..d70153a7dd 100644 --- a/src/libslic3r/Support/SupportMaterial.cpp +++ b/src/libslic3r/Support/SupportMaterial.cpp @@ -367,10 +367,21 @@ inline void layers_append(SupportGeneratorLayersPtr &dst, const SupportGenerator } // Support layer that is covered by some form of dense interface. -static constexpr const std::initializer_list support_types_interface { +static constexpr const std::initializer_list support_types_interface { SupporLayerType::RaftInterface, SupporLayerType::BottomContact, SupporLayerType::BottomInterface, SupporLayerType::TopContact, SupporLayerType::TopInterface }; +// Forward declarations for belt floor helpers (defined later in this file). +// belt_floor_surface_polygon is declared in SupportCommon.hpp (non-static, +// shared with TreeSupport.cpp). + +static Polygons belt_floor_valid_region_polygon( + const SlicingParameters &slicing_params, const PrintConfig &print_config, + const PrintObject &object, coordf_t print_z); +static void trim_support_layers_by_belt_floor( + const SlicingParameters &slicing_params, const PrintConfig &print_config, + const PrintObject &object, SupportGeneratorLayersPtr &support_layers); + void PrintObjectSupportMaterial::generate(PrintObject &object) { BOOST_LOG_TRIVIAL(info) << "Support generator - Start"; @@ -443,11 +454,12 @@ void PrintObjectSupportMaterial::generate(PrintObject &object) object, bottom_contacts, top_contacts, layer_storage); this->trim_support_layers_by_object(object, top_contacts, m_slicing_params.gap_support_object, m_slicing_params.gap_object_support, m_support_params.gap_xy); + trim_support_layers_by_belt_floor(m_slicing_params, *m_print_config, object, top_contacts); #ifdef SLIC3R_DEBUG for (const SupportGeneratorLayer *layer : top_contacts) Slic3r::SVG::export_expolygons( - debug_out_path("support-top-contacts-trimmed-by-object-%d-%lf.svg", iRun, layer->print_z), + debug_out_path("support-top-contacts-trimmed-by-object-%d-%lf.svg", iRun, layer->print_z), union_ex(layer->polygons)); #endif @@ -603,6 +615,139 @@ Polygons collect_region_slices_by_type(const Layer &layer, SurfaceType surface_t return out; } +// Belt printer: compute the belt-side half-plane polygon at a given print_z. +// This represents the region where the belt surface exists (the "phantom top surface"). +// Support that overlaps with this polygon should terminate with a bottom contact. +// Returns empty if belt floor is not active. +Polygons belt_floor_surface_polygon( + const SlicingParameters &slicing_params, + const PrintConfig &print_config, + const PrintObject &object, + coordf_t print_z) +{ + const double shear_factor = slicing_params.belt_floor_shear_factor; + if (std::abs(shear_factor) < EPSILON) + return {}; + + const int from_axis = slicing_params.belt_floor_from_axis; // 0=X, 1=Y + const double floor_offset = print_config.belt_support_floor_offset.value; + + // Belt floor line in slicing coordinates: Z = sf * Y + z_shift. + // z_shift accounts for the upward shift applied when post-shear geometry + // extends below the bed (overhangs). Solving for Y: + // cutoff = (print_z - z_shift - floor_offset) / sf + const double z_shift = slicing_params.belt_floor_z_shift; + const double cutoff = (print_z - z_shift - floor_offset) / shear_factor; + const coord_t cutoff_scaled = scale_(cutoff); + const coord_t large_bound = scale_(1e4); + + // Build the belt-side half-plane (inverted from the valid region). + // If shear_factor > 0: valid region is from_axis < cutoff, so belt surface is from_axis >= cutoff. + // If shear_factor < 0: valid region is from_axis > cutoff, so belt surface is from_axis <= cutoff. + Polygon belt_poly; + if (from_axis == 0) { + if (shear_factor > 0) { + // Belt surface: X >= cutoff + belt_poly.points = { + Point(cutoff_scaled, -large_bound), + Point(large_bound, -large_bound), + Point(large_bound, large_bound), + Point(cutoff_scaled, large_bound) + }; + } else { + // Belt surface: X <= cutoff + belt_poly.points = { + Point(-large_bound, -large_bound), + Point(cutoff_scaled, -large_bound), + Point(cutoff_scaled, large_bound), + Point(-large_bound, large_bound) + }; + } + } else { + if (shear_factor > 0) { + // Belt surface: Y >= cutoff + belt_poly.points = { + Point(-large_bound, cutoff_scaled), + Point( large_bound, cutoff_scaled), + Point( large_bound, large_bound), + Point(-large_bound, large_bound) + }; + } else { + // Belt surface: Y <= cutoff + belt_poly.points = { + Point(-large_bound, -large_bound), + Point( large_bound, -large_bound), + Point( large_bound, cutoff_scaled), + Point(-large_bound, cutoff_scaled) + }; + } + } + return { belt_poly }; +} + +// Belt printer: compute the valid-region half-plane polygon at a given print_z. +// This is the region where support is allowed to exist (above the belt). +// Used to clip the downward-propagating support projection. +static Polygons belt_floor_valid_region_polygon( + const SlicingParameters &slicing_params, + const PrintConfig &print_config, + const PrintObject &object, + coordf_t print_z) +{ + const double shear_factor = slicing_params.belt_floor_shear_factor; + if (std::abs(shear_factor) < EPSILON) + return {}; + + const int from_axis = slicing_params.belt_floor_from_axis; + const double floor_offset = print_config.belt_support_floor_offset.value; + + const double z_shift = slicing_params.belt_floor_z_shift; + const double cutoff = (print_z - z_shift - floor_offset) / shear_factor; + const coord_t cutoff_scaled = scale_(cutoff); + const coord_t large_bound = scale_(1e4); + + // Valid region: the complement of the belt surface polygon. + Polygon valid_poly; + if (from_axis == 0) { + if (shear_factor > 0) { + // Valid: X < cutoff + valid_poly.points = { + Point(-large_bound, -large_bound), + Point(cutoff_scaled, -large_bound), + Point(cutoff_scaled, large_bound), + Point(-large_bound, large_bound) + }; + } else { + // Valid: X > cutoff + valid_poly.points = { + Point(cutoff_scaled, -large_bound), + Point(large_bound, -large_bound), + Point(large_bound, large_bound), + Point(cutoff_scaled, large_bound) + }; + } + } else { + if (shear_factor > 0) { + // Valid: Y < cutoff + valid_poly.points = { + Point(-large_bound, -large_bound), + Point( large_bound, -large_bound), + Point( large_bound, cutoff_scaled), + Point(-large_bound, cutoff_scaled) + }; + } else { + // Valid: Y > cutoff + valid_poly.points = { + Point(-large_bound, cutoff_scaled), + Point( large_bound, cutoff_scaled), + Point( large_bound, large_bound), + Point(-large_bound, large_bound) + }; + } + } + return { valid_poly }; +} + // Collect outer contours of all slices of this layer. // This is useful for calculating the support base with holes filled. Polygons collect_slices_outer(const Layer &layer) @@ -2516,6 +2661,82 @@ static inline SupportGeneratorLayer* detect_bottom_contacts( return &layer_new; } +// Belt printer: detect bottom contacts where support meets the belt floor plane. +// Modeled on detect_bottom_contacts() but uses the belt plane polygon instead of stTop surfaces. +static inline SupportGeneratorLayer* detect_belt_floor_bottom_contacts( + const SlicingParameters &slicing_params, + const SupportParameters &support_params, + const PrintConfig &print_config, + const PrintObject &object, + const Layer &layer, + // Existing top contact layers, for snapping. + const SupportGeneratorLayersPtr &top_contacts, + size_t contact_idx, + SupportGeneratorLayerStorage &layer_storage, + std::vector &layer_support_areas, + const Polygons &supports_projected) +{ + // Compute the belt surface polygon at this layer's Z. + Polygons belt_surface = belt_floor_surface_polygon(slicing_params, print_config, object, layer.print_z); + if (belt_surface.empty()) + return nullptr; + + // Find where projected support overlaps the belt surface. + Polygons touching = intersection(belt_surface, supports_projected); + if (touching.empty()) + return nullptr; + + assert(layer.id() >= slicing_params.raft_layers()); + size_t layer_id = layer.id() - slicing_params.raft_layers(); + + // Allocate a new bottom contact layer resting on the belt plane. + SupportGeneratorLayer &layer_new = layer_storage.allocate_unguarded(SupporLayerType::BottomContact); + + // No object layer to sync with -- compute heights directly from flow parameters. + layer_new.height = support_params.support_material_bottom_interface_flow.height(); + layer_new.print_z = layer.print_z + layer_new.height + slicing_params.gap_object_support; + layer_new.bottom_z = layer.print_z; + layer_new.idx_object_layer_below = layer_id; + layer_new.bridging = ! slicing_params.soluble_interface && object.config().thick_bridges; + layer_new.polygons = expand(touching, float(support_params.support_material_flow.scaled_width()), SUPPORT_SURFACES_OFFSET_PARAMETERS); + + if (! slicing_params.soluble_interface) { + // Snap to nearby top contact layers to avoid very thin support layers. + for (size_t top_idx = size_t(std::max(0, int(contact_idx))); + top_idx < top_contacts.size() && top_contacts[top_idx]->print_z < layer_new.print_z + support_params.support_layer_height_min + EPSILON; + ++ top_idx) { + if (top_contacts[top_idx]->print_z > layer_new.print_z - support_params.support_layer_height_min - EPSILON) { + coordf_t diff = layer_new.print_z - top_contacts[top_idx]->print_z; + assert(std::abs(diff) <= support_params.support_layer_height_min + EPSILON); + if (diff > 0.F) { + if (layer_new.height - diff > support_params.support_layer_height_min) { + layer_new.print_z = top_contacts[top_idx]->print_z; + layer_new.height -= diff; + } else { + continue; + } + } else { + layer_new.print_z = top_contacts[top_idx]->print_z; + layer_new.height -= diff; + } + break; + } + } + } + + // Trim the already created base layers above this belt contact. + touching = expand(touching, float(SCALED_EPSILON)); + for (int layer_id_above = int(layer_id) + 1; layer_id_above < int(object.total_layer_count()); ++ layer_id_above) { + const Layer &layer_above = *object.layers()[layer_id_above]; + if (layer_above.print_z > layer_new.print_z - EPSILON) + break; + if (Polygons &above = layer_support_areas[layer_id_above]; ! above.empty()) + above = diff(above, touching); + } + + return &layer_new; +} + // Returns polygons to print + polygons to propagate downwards. // Called twice: First for normal supports, possibly trimmed by "on build plate only", second for support enforcers not trimmed by "on build plate only". static inline std::pair project_support_to_grid(const Layer &layer, const SupportGridParams &grid_params, const Polygons &overhangs, Polygons *layer_buildplate_covered @@ -2615,6 +2836,8 @@ 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; // Allocate empty surface areas, one per object layer. layer_support_areas.assign(object.total_layer_count(), Polygons()); @@ -2675,8 +2898,9 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::bottom_contact_layers_and_ tbb::task_group task_group; const Polygons &overhangs_for_bottom_contacts = buildplate_only ? enforcers_projection_raw : overhangs_projection_raw; if (! overhangs_for_bottom_contacts.empty()) - // Find the bottom contact layers above the top surfaces of this layer. - task_group.run([this, &object, &layer, &top_contacts, contact_idx, &layer_storage, &layer_support_areas, &bottom_contacts, &overhangs_for_bottom_contacts + // Find the bottom contact layers above the top surfaces of this layer, + // and also detect belt floor contacts if belt mode is active. + task_group.run([this, &object, &layer, &top_contacts, contact_idx, &layer_storage, &layer_support_areas, &bottom_contacts, &overhangs_for_bottom_contacts, has_belt_floor #ifdef SLIC3R_DEBUG , iRun, &polygons_new #endif // SLIC3R_DEBUG @@ -2690,6 +2914,15 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::bottom_contact_layers_and_ ); if (layer_new) bottom_contacts.push_back(layer_new); + // Belt floor phantom surface: detect where support meets the belt plane. + if (has_belt_floor) { + SupportGeneratorLayer *belt_layer = detect_belt_floor_bottom_contacts( + m_slicing_params, m_support_params, *m_print_config, object, + layer, top_contacts, contact_idx, layer_storage, + layer_support_areas, overhangs_for_bottom_contacts); + if (belt_layer) + bottom_contacts.push_back(belt_layer); + } }); Polygons &layer_support_area = layer_support_areas[layer_id]; @@ -2728,6 +2961,23 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::bottom_contact_layers_and_ task_group.wait(); + // Belt floor: clip projections and support areas so support doesn't + // propagate below the belt plane. + if (has_belt_floor) { + Polygons valid_region = belt_floor_valid_region_polygon( + m_slicing_params, *m_print_config, object, layer.print_z); + if (! valid_region.empty()) { + if (! overhangs_projection.empty()) + overhangs_projection = intersection(overhangs_projection, valid_region); + if (! enforcers_projection.empty()) + enforcers_projection = intersection(enforcers_projection, valid_region); + if (! layer_support_area.empty()) + layer_support_area = intersection(layer_support_area, valid_region); + if (! layer_support_area_enforcers.empty()) + layer_support_area_enforcers = intersection(layer_support_area_enforcers, valid_region); + } + } + if (! layer_support_area_enforcers.empty()) { if (layer_support_area.empty()) layer_support_area = std::move(layer_support_area_enforcers); @@ -2738,6 +2988,7 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::bottom_contact_layers_and_ std::reverse(bottom_contacts.begin(), bottom_contacts.end()); trim_support_layers_by_object(object, bottom_contacts, m_slicing_params.gap_support_object, m_slicing_params.gap_object_support, m_support_params.gap_xy); + trim_support_layers_by_belt_floor(m_slicing_params, *m_print_config, object, bottom_contacts); return bottom_contacts; } @@ -3108,6 +3359,37 @@ void PrintObjectSupportMaterial::generate_base_layers( #endif /* SLIC3R_DEBUG */ this->trim_support_layers_by_object(object, intermediate_layers, m_slicing_params.gap_support_object, m_slicing_params.gap_object_support, m_support_params.gap_xy); + trim_support_layers_by_belt_floor(m_slicing_params, *m_print_config, object, intermediate_layers); +} + +// Belt printer: trim support layer polygons by the belt floor plane. +// For each support layer, computes the belt floor half-plane at that layer's print_z +// and subtracts it from the support polygons. This follows the same diff() pattern +// as trim_support_layers_by_object() so that interface layers derived from trimmed +// intermediates automatically inherit the belt floor trimming. +static void trim_support_layers_by_belt_floor( + const SlicingParameters &slicing_params, + const PrintConfig &print_config, + const PrintObject &object, + SupportGeneratorLayersPtr &support_layers) +{ + if (std::abs(slicing_params.belt_floor_shear_factor) < EPSILON) + return; + if (print_config.belt_support_floor_mode.value != BeltSupportFloorMode::GeneratorOnly) + return; + + tbb::parallel_for(tbb::blocked_range(0, support_layers.size()), + [&](const tbb::blocked_range &range) { + for (size_t i = range.begin(); i < range.end(); ++ i) { + SupportGeneratorLayer *layer = support_layers[i]; + if (layer->polygons.empty()) + continue; + Polygons belt_surface = belt_floor_surface_polygon( + slicing_params, print_config, object, layer->print_z); + if (! belt_surface.empty()) + layer->polygons = diff(layer->polygons, belt_surface); + } + }); } void PrintObjectSupportMaterial::trim_support_layers_by_object( diff --git a/src/libslic3r/Support/TreeModelVolumes.cpp b/src/libslic3r/Support/TreeModelVolumes.cpp index 0267c0c327..e270b62aab 100644 --- a/src/libslic3r/Support/TreeModelVolumes.cpp +++ b/src/libslic3r/Support/TreeModelVolumes.cpp @@ -94,6 +94,46 @@ TreeModelVolumes::TreeModelVolumes( #else { m_anti_overhang = print_object.slice_support_blockers(); + // Belt floor: add belt surface polygons to anti_overhang so support + // is never generated inside the belt. Only in global shear mode — + // in local mode the belt floor clipping handles everything and + // anti_overhang at the bottom layers would block all support. + { + const auto &sp = print_object.slicing_parameters(); + const auto &pcfg = print_object.print()->config(); + const double sf = sp.belt_floor_shear_factor; + if (std::abs(sf) > EPSILON + && std::abs(print_object.belt_global_z_offset()) > EPSILON + && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + const int from_axis = sp.belt_floor_from_axis; + const double floor_off = pcfg.belt_support_floor_offset.value; + const double z_shift = sp.belt_floor_z_shift - print_object.belt_global_z_offset(); + size_t num_layers_needed = print_object.layer_count(); + // Ensure m_anti_overhang is large enough. + if (m_anti_overhang.size() < num_layers_needed) + m_anti_overhang.resize(num_layers_needed, Polygons{}); + for (size_t layer_idx = 0; layer_idx < num_layers_needed; ++layer_idx) { + double print_z = print_object.get_layer(layer_idx)->print_z + - print_object.belt_global_z_offset(); + double cutoff = (print_z - z_shift - floor_off) / sf; + coord_t cutoff_sc = scale_(cutoff); + coord_t big = scale_(1e4); + Polygon belt_poly; + if (from_axis == 0) { + if (sf > 0) + belt_poly.points = {{cutoff_sc,-big},{big,-big},{big,big},{cutoff_sc,big}}; + else + belt_poly.points = {{-big,-big},{cutoff_sc,-big},{cutoff_sc,big},{-big,big}}; + } else { + if (sf > 0) + belt_poly.points = {{-big,cutoff_sc},{big,cutoff_sc},{big,big},{-big,big}}; + else + belt_poly.points = {{-big,-big},{big,-big},{big,cutoff_sc},{-big,cutoff_sc}}; + } + append(m_anti_overhang[layer_idx], Polygons{belt_poly}); + } + } + } TreeSupportMeshGroupSettings mesh_settings(print_object); const TreeSupportSettings config{ mesh_settings, print_object.slicing_parameters() }; m_current_min_xy_dist = config.xy_min_distance; @@ -102,6 +142,27 @@ 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(print_object.model_object()->raw_bounding_box().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 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()); + } + } + } m_current_outline_idx = 0; m_layer_outlines.emplace_back(mesh_settings, std::vector{}); @@ -114,6 +175,47 @@ TreeModelVolumes::TreeModelVolumes( for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) outlines[layer_idx] = polygons_simplify(to_polygons(print_object.get_layer(layer_idx - num_raft_layers)->lslices), mesh_settings.resolution, polygons_strictly_simple); }); + + // Belt floor: pre-compute belt surface polygon per-layer for clipping. + // Branches grow toward the belt and their slices are clipped at the belt + // surface in organic_draw_branches(). The organic pipeline works in LOCAL + // Z (no global_z_offset), so use local z_shift and local print_z. + const auto &slicing_params = print_object.slicing_parameters(); + const auto &pcfg = print_object.print()->config(); + const double sf = slicing_params.belt_floor_shear_factor; + if (std::abs(sf) > EPSILON + && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + const int from_axis = slicing_params.belt_floor_from_axis; + const double floor_off = pcfg.belt_support_floor_offset.value; + // Subtract global_z_offset to get the LOCAL z_shift — the organic + // pipeline's Z coordinates don't include the global offset. + const double z_shift = slicing_params.belt_floor_z_shift + - print_object.belt_global_z_offset(); + m_belt_floor.assign(num_layers, Polygons{}); + for (size_t layer_idx = 0; layer_idx < num_layers; ++layer_idx) { + // Use local print_z (subtract global offset from object layer). + double print_z = (layer_idx >= num_raft_layers) + ? print_object.get_layer(layer_idx - num_raft_layers)->print_z + - print_object.belt_global_z_offset() + : 0.; + double cutoff = (print_z - z_shift - floor_off) / sf; + coord_t cutoff_sc = scale_(cutoff); + coord_t big = scale_(1e4); + Polygon belt_poly; + if (from_axis == 0) { + if (sf > 0) + belt_poly.points = {{cutoff_sc,-big},{big,-big},{big,big},{cutoff_sc,big}}; + else + belt_poly.points = {{-big,-big},{cutoff_sc,-big},{cutoff_sc,big},{-big,big}}; + } else { + if (sf > 0) + belt_poly.points = {{-big,cutoff_sc},{big,cutoff_sc},{big,big},{-big,big}}; + else + belt_poly.points = {{-big,-big},{big,-big},{big,cutoff_sc},{-big,cutoff_sc}}; + } + m_belt_floor[layer_idx] = { belt_poly }; + } + } } #endif @@ -469,9 +571,9 @@ void TreeModelVolumes::calculateCollision(const coord_t radius, const LayerIndex }); // 2) Sum over top / bottom ranges. - const bool processing_last_mesh = outline_idx == layer_outline_indices.size(); + const bool processing_last_mesh = outline_idx == layer_outline_indices.back(); tbb::parallel_for(tbb::blocked_range(data.begin(), data.end()), - [&collision_areas_offsetted, &outlines, &machine_border = m_machine_border, &anti_overhang = m_anti_overhang, radius, + [&collision_areas_offsetted, &outlines, &machine_border = m_machine_border, &anti_overhang = m_anti_overhang, radius, xy_distance, z_distance_bottom_layers, z_distance_top_layers, min_resolution = m_min_resolution, &data, processing_last_mesh, &throw_on_cancel] (const tbb::blocked_range& range) { for (LayerIndex layer_idx = range.begin(); layer_idx != range.end(); ++ layer_idx) { @@ -517,9 +619,14 @@ void TreeModelVolumes::calculateCollision(const coord_t radius, const LayerIndex // not support an overhang<90 degree than to risk fusing to it. append(collisions, offset(union_ex(collision_areas_original), radius + required_range_x, ClipperLib::jtMiter, 1.2)); } - collisions = processing_last_mesh && layer_idx < int(anti_overhang.size()) ? - union_(collisions, offset(union_ex(anti_overhang[layer_idx]), radius, ClipperLib::jtMiter, 1.2)) : - union_(collisions); + if (processing_last_mesh) { + if (layer_idx < int(anti_overhang.size())) + append(collisions, offset(union_ex(anti_overhang[layer_idx]), radius, ClipperLib::jtMiter, 1.2)); + // NOTE: m_belt_floor is NOT added to collision here — branches + // should grow toward the belt and terminate at it, not avoid it. + // Belt floor clipping is done post-generation in organic_draw_branches(). + } + collisions = union_(collisions); auto &dst = data[layer_idx]; if (processing_last_mesh) { if (! dst.empty()) diff --git a/src/libslic3r/Support/TreeModelVolumes.hpp b/src/libslic3r/Support/TreeModelVolumes.hpp index eff4cadcaf..0ded395091 100644 --- a/src/libslic3r/Support/TreeModelVolumes.hpp +++ b/src/libslic3r/Support/TreeModelVolumes.hpp @@ -168,6 +168,9 @@ public: } Polygon m_bed_area; + // Belt floor polygons per layer — used for post-generation clipping + // in organic_draw_branches(). Public so the organic pipeline can access it. + std::vector m_belt_floor; private: // Caching polygons for a range of layers. diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 864a0eb0e0..4966a5a139 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -637,6 +637,20 @@ TreeSupport::TreeSupport(PrintObject& object, const SlicingParameters &slicing_p } +double TreeSupport::belt_floor_print_z(const Point &pos_slicing) const +{ + double sf = m_slicing_params.belt_floor_shear_factor; + if (std::abs(sf) < EPSILON) + return -std::numeric_limits::max(); // no belt floor + int from = m_slicing_params.belt_floor_from_axis; + // Belt floor in slicing coords: Z = sf * Y + z_shift + floor_offset. + // Inverse of cutoff = (Z - z_shift - floor_offset) / sf. + double pos = unscale(from == 0 ? pos_slicing.x() : pos_slicing.y()); + double floor_offset = m_print_config->belt_support_floor_offset.value; + double z_shift = m_slicing_params.belt_floor_z_shift; + return sf * pos + floor_offset + z_shift; +} + #define SUPPORT_SURFACES_OFFSET_PARAMETERS ClipperLib::jtSquare, 0. void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) { @@ -2141,6 +2155,41 @@ void TreeSupport::draw_circles() base_areas = diff_ex(base_areas, ClipperUtils::clip_clipper_polygons_with_subject_bbox(roofs, get_extents(base_areas))); base_areas = intersection_ex(base_areas, m_machine_border); + // Belt floor: clip tree support polygons by the belt surface plane. + // ts_layer->print_z is at LOCAL Z (global offset applied later in + // _generate_support_material), but belt_floor_z_shift includes + // global_z_offset — subtract it to get the cutoff in local coords. + if (std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON + && m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + const double sf = m_slicing_params.belt_floor_shear_factor; + const int from_axis = m_slicing_params.belt_floor_from_axis; + const double floor_off = m_print_config->belt_support_floor_offset.value; + const double z_shift_local = m_slicing_params.belt_floor_z_shift + - m_object->belt_global_z_offset(); + const double cutoff = (ts_layer->print_z - z_shift_local - floor_off) / sf; + const coord_t cutoff_sc = scale_(cutoff); + const coord_t big = scale_(1e4); + + Polygon belt_poly; + if (from_axis == 0) { + if (sf > 0) + belt_poly.points = { {cutoff_sc,-big}, {big,-big}, {big,big}, {cutoff_sc,big} }; + else + belt_poly.points = { {-big,-big}, {cutoff_sc,-big}, {cutoff_sc,big}, {-big,big} }; + } else { + if (sf > 0) + belt_poly.points = { {-big,cutoff_sc}, {big,cutoff_sc}, {big,big}, {-big,big} }; + else + belt_poly.points = { {-big,-big}, {big,-big}, {big,cutoff_sc}, {-big,cutoff_sc} }; + } + Polygons belt_surface = { belt_poly }; + base_areas = diff_ex(base_areas, belt_surface); + roof_areas = diff_ex(roof_areas, belt_surface); + roof_1st_layer = diff_ex(roof_1st_layer, belt_surface); + floor_areas = diff_ex(floor_areas, belt_surface); + roof_gap_areas = diff_ex(roof_gap_areas, belt_surface); + } + if (SQUARE_SUPPORT) { // simplify support contours ExPolygons base_areas_simplified; @@ -2445,6 +2494,9 @@ void TreeSupport::drop_nodes() 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 size_t top_interface_layers = config.support_interface_top_layers.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 size_t bottom_interface_layers = config.support_interface_bottom_layers.value < 0 ? top_interface_layers : config.support_interface_bottom_layers.value; 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 @@ -2677,15 +2729,22 @@ 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); - const bool to_buildplate = !is_inside_ex(get_collision(0, obj_layer_nr_next), next_position); - SupportNode* next_node = m_ts_data->create_node(next_position, node_parent->distance_to_top + 1, obj_layer_nr_next, node_parent->support_roof_layers_below - 1, to_buildplate, node_parent, - print_z_next, height_next); - get_max_move_dist(next_node); - m_ts_data->m_mutex.lock(); - contact_nodes[layer_nr_next].push_back(next_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)) { + node_parent->to_buildplate = false; + } else { + const bool to_buildplate = !is_inside_ex(get_collision(0, obj_layer_nr_next), next_position); + SupportNode* next_node = m_ts_data->create_node(next_position, node_parent->distance_to_top + 1, obj_layer_nr_next, node_parent->support_roof_layers_below - 1, to_buildplate, node_parent, + print_z_next, height_next); + get_max_move_dist(next_node); + m_ts_data->m_mutex.lock(); + contact_nodes[layer_nr_next].push_back(next_node); + m_ts_data->m_mutex.unlock(); + } neighbour->valid = false; p_node->valid = false; - m_ts_data->m_mutex.unlock(); } else if (neighbours.size() > 1) //Don't merge leaf nodes because we would then incur movement greater than the maximum move distance. { @@ -2729,6 +2788,12 @@ 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. + // Treat as object-surface termination (not buildplate). + if (has_belt_floor && print_z_next <= belt_floor_print_z(next_pt)) { + p_node->to_buildplate = false; + continue; + } SupportNode *next_node = m_ts_data->create_node(next_pt, p_node->distance_to_top + 1, obj_layer_nr_next, p_node->support_roof_layers_below - 1, to_buildplate, p_node, print_z_next, height_next); next_node->max_move_dist = 0; @@ -2873,6 +2938,12 @@ void TreeSupport::drop_nodes() if (is_outside) { next_layer_vertex = candidate_vertex; } } } + // Belt floor: don't drop regular node below belt surface. + // Treat as object-surface termination (not buildplate). + if (has_belt_floor && print_z_next <= belt_floor_print_z(next_layer_vertex)) { + p_node->to_buildplate = false; + return; // from parallel_for_each lambda + } auto next_collision = get_collision(0, obj_layer_nr_next); const bool to_buildplate = !is_inside_ex(m_ts_data->m_layer_outlines[obj_layer_nr_next], next_layer_vertex); SupportNode * next_node = m_ts_data->create_node(next_layer_vertex, node.distance_to_top + 1, obj_layer_nr_next, node.support_roof_layers_below - 1, to_buildplate, p_node, @@ -3167,6 +3238,9 @@ void TreeSupport::generate_contact_points() bool on_buildplate_only = m_object_config->support_on_build_plate_only.value; const bool roof_enabled = config.support_interface_top_layers.value > 0; const bool force_tip_to_roof = roof_enabled && m_support_params.soluble_interface; + 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; //First generate grid points to cover the entire area of the print. BoundingBox bounding_box = m_object->bounding_box(); @@ -3257,6 +3331,10 @@ void TreeSupport::generate_contact_points() auto insert_point = [&](Point pt, const ExPolygon& overhang, double radius, bool force_add = false, bool add_interface=true) { + // Belt floor: skip contact points whose bottom_z is at or below + // the belt floor at this XY position (overhang rests on the belt). + if (has_belt_floor && bottom_z <= belt_floor_print_z(pt)) + return (SupportNode*) nullptr; Point hash_pos = pt / ((radius_scaled + 1) / 1); SupportNode* contact_node = nullptr; if (force_add || !already_inserted.count(hash_pos)) { @@ -3292,8 +3370,10 @@ void TreeSupport::generate_contact_points() double radius = unscale_(overhang_bounds.radius()); Point candidate = overhang_bounds.center(); SupportNode *contact_node = insert_point(candidate, overhang, radius, true, true); - contact_node->type = ePolygon; - curr_nodes.emplace_back(contact_node); + if (contact_node) { + contact_node->type = ePolygon; + curr_nodes.emplace_back(contact_node); + } } }else{ // otherwise, all nodes should be circle nodes diff --git a/src/libslic3r/Support/TreeSupport.hpp b/src/libslic3r/Support/TreeSupport.hpp index e0446ad5f1..c3bd186df1 100644 --- a/src/libslic3r/Support/TreeSupport.hpp +++ b/src/libslic3r/Support/TreeSupport.hpp @@ -446,6 +446,10 @@ private: bool is_slim = false; bool with_infill = false; + // 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; + /*! diff --git a/src/libslic3r/Support/TreeSupport3D.cpp b/src/libslic3r/Support/TreeSupport3D.cpp index 2c24b933b2..b551a42ece 100644 --- a/src/libslic3r/Support/TreeSupport3D.cpp +++ b/src/libslic3r/Support/TreeSupport3D.cpp @@ -3366,6 +3366,36 @@ 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(); + const double sf = sp.belt_floor_shear_factor; + if (std::abs(sf) > EPSILON && 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(po.model_object()->raw_bounding_box().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 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(); @@ -3561,6 +3591,43 @@ static void generate_support_areas(Print &print, TreeSupport* tree_support, cons if (layer) layer->polygons = intersection(layer->polygons, volumes.m_bed_area); }); + // Belt floor: clip ALL organic support layers (including intermediate/base + // fill) against the belt surface. The branch slices were already clipped + // in organic_draw_branches(), but intermediate layers generated between + // branches and the build plate need clipping too. + // Compute the belt floor polygon directly from each layer's print_z + // rather than mapping to a layer index (avoids index mismatch issues). + { + const auto &sp = print_object.slicing_parameters(); + const double sf = sp.belt_floor_shear_factor; + const double z_shift = sp.belt_floor_z_shift - print_object.belt_global_z_offset(); + const double floor_off = print_object.print()->config().belt_support_floor_offset.value; + const int from_axis = sp.belt_floor_from_axis; + if (std::abs(sf) > EPSILON + && print_object.print()->config().belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + tbb::parallel_for_each(layers_sorted.begin(), layers_sorted.end(), [&](SupportGeneratorLayer *layer) { + if (!layer || layer->polygons.empty()) + return; + double cutoff = (layer->print_z - z_shift - floor_off) / sf; + coord_t cutoff_sc = scale_(cutoff); + coord_t big = scale_(1e4); + Polygon belt_poly; + if (from_axis == 0) { + if (sf > 0) + belt_poly.points = {{cutoff_sc,-big},{big,-big},{big,big},{cutoff_sc,big}}; + else + belt_poly.points = {{-big,-big},{cutoff_sc,-big},{cutoff_sc,big},{-big,big}}; + } else { + if (sf > 0) + belt_poly.points = {{-big,cutoff_sc},{big,cutoff_sc},{big,big},{-big,big}}; + else + belt_poly.points = {{-big,-big},{big,-big},{big,cutoff_sc},{-big,cutoff_sc}}; + } + layer->polygons = diff(layer->polygons, Polygons{belt_poly}); + }); + } + } + print.set_status(69, _L("Generating support")); generate_support_toolpaths(print_object.support_layers(), print_object.config(), support_params, print_object.slicing_parameters(), raft_layers, bottom_contacts, top_contacts, intermediate_layers, interface_layers, base_interface_layers); @@ -3816,6 +3883,10 @@ void organic_draw_branches( for (LayerIndex i = 0; i < LayerIndex(slices.size()); ++i) { slices[i] = diff_clipped(slices[i], volumes.getCollision(0, layer_begin + i, true)); // FIXME parent_uses_min || draw_area.element->state.use_min_xy_dist); slices[i] = intersection(slices[i], volumes.m_bed_area); + // 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]); } size_t num_empty = 0; if (slices.front().empty()) { @@ -3850,6 +3921,9 @@ void organic_draw_branches( //double support_area_min = 0.1 * support_area_min_radius; for (LayerIndex layer_idx = layer_begin - 1; layer_idx >= layer_bottommost; -- layer_idx) { rest_support = diff_clipped(rest_support.empty() ? slices.front() : rest_support, volumes.getCollision(0, layer_idx, false)); + // Belt floor: clip propagated support at belt surface. + if (layer_idx < LayerIndex(volumes.m_belt_floor.size()) && !volumes.m_belt_floor[layer_idx].empty()) + rest_support = diff(rest_support, volumes.m_belt_floor[layer_idx]); double rest_support_area = area(rest_support); if (rest_support_area < support_area_stop) // Don't propagate a fraction of the tree contact surface. diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 25b4ba4e9c..b9fe17ff2a 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -2210,17 +2210,11 @@ void GCodeViewer::render_toolpaths() { const Camera& camera = wxGetApp().plater()->get_camera(); Matrix4f view = camera.get_view_matrix().matrix().cast(); - // Belt "designed" view: apply inverse shear to view matrix so toolpaths appear - // upright (as originally designed) instead of sheared on the belt. - if (m_belt_show_designed && m_belt_view_enabled && m_belt_angle_deg > 0.f) { - double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle_deg)); - double sin_a = std::sin(angle_rad); - if (sin_a > 1e-6) { - double cot_alpha = std::cos(angle_rad) / sin_a; - Transform3d inverse_shear = Transform3d::Identity(); - inverse_shear.matrix()(1, 2) = -cot_alpha; // Y -= Z * cot(α) - view = (camera.get_view_matrix() * inverse_shear).matrix().cast(); - } + // Belt "designed" view: apply the precomputed inverse of the full belt + // shear+scale transform so toolpaths appear upright (as originally designed) + // instead of transformed on the belt. + if (m_belt_show_designed && m_belt_view_enabled) { + view = (camera.get_view_matrix() * m_belt_inverse_transform).matrix().cast(); } const libvgcode::Mat4x4 converted_view_matrix = libvgcode::convert(view); const libvgcode::Mat4x4 converted_projetion_matrix = libvgcode::convert(static_cast(camera.get_projection_matrix().matrix().cast())); @@ -4418,12 +4412,18 @@ 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 raw slicing-frame G-code + // 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::Checkbox("Show designed view (upright)", &m_belt_show_designed); + ImGui::TextColored(ImVec4(0.f, 0.59f, 0.53f, 1.f), "%s", _u8L("Belt Printer").c_str()); + ImGui::Dummy({ window_padding, 0 }); + ImGui::SameLine(); + ImGui::Checkbox(_u8L("Show designed view (upright) [B]").c_str(), &m_belt_show_designed); } legend_height = ImGui::GetCurrentWindow()->Size.y; diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 184c56e915..cd97613b2a 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -240,6 +240,7 @@ 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 = false; // Toggle: show designed (upright) view via inverse shear + Transform3d m_belt_inverse_transform{Transform3d::Identity()}; libvgcode::Viewer m_viewer; bool m_loaded_as_preview{ false }; @@ -341,7 +342,10 @@ public: void export_toolpaths_to_obj(const char* filename) const; void set_belt_printer(bool enabled, float angle_deg) { m_belt_view_enabled = enabled; m_belt_angle_deg = angle_deg; } + void set_belt_inverse_transform(const Transform3d& t) { m_belt_inverse_transform = t; } 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(); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index a19bed05a8..3d964c7dab 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -3463,8 +3463,16 @@ void GLCanvas3D::on_char(wxKeyEvent& evt) post_event(SimpleEvent(EVT_GLCANVAS_ARRANGE)); break; } - //case 'B': - //case 'b': { zoom_to_bed(); break; } + case 'B': + case 'b': { + // Toggle belt printer "show designed" view when in G-code preview with belt mode active. + if (dynamic_cast(m_canvas->GetParent()) != nullptr && + m_gcode_viewer.is_belt_view()) { + m_gcode_viewer.toggle_belt_show_designed(); + m_dirty = true; + } + break; + } case 'C': case 'c': { wxGetApp().toggle_show_gcode_window(); m_dirty = true; request_extra_frame(); break; } //case 'G': diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index b20e26653a..ea7b40893f 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -2516,7 +2516,7 @@ void ObjectList::load_mesh_object(const TriangleMesh &mesh, const wxString &name Slic3r::save_object_mesh(*new_object); // BBS: find an empty cell to put the copied object - auto start_point = wxGetApp().plater()->build_volume().bounding_volume2d().center(); + auto start_point = wxGetApp().plater()->build_volume().bed_center(); auto empty_cell = wxGetApp().plater()->canvas3D()->get_nearest_empty_cell({start_point(0), start_point(1)}); new_object->instances[0]->set_offset(center ? to_3d(Vec2d(empty_cell(0), empty_cell(1)), -new_object->origin_translation.z()) : bb.center()); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 373ee2a3dc..ee235d4192 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -6907,7 +6907,7 @@ std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& mode // BBS: find an empty cell to put the copied object for (auto& instance : new_instances) { auto offset = instance->get_offset(); - auto start_point = this->bed.build_volume().bounding_volume2d().center(); + auto start_point = this->bed.build_volume().bed_center(); bool plate_empty = partplate_list.get_curr_plate()->empty(); Vec3d displacement; if (plate_empty) @@ -10991,10 +10991,83 @@ void Plater::priv::set_bed_shape(const Pointfs &shape, bed.set_belt_printer(true, static_cast(belt_angle)); if (preview) preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(true, static_cast(belt_angle)); + + // Compute the inverse of the full belt shear+scale transform for the G-code viewer. + auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad); + double cos_a = std::cos(angle_rad); + switch (mode) { + case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; + case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; + case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; + case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; + default: return 0.; + } + }; + auto compute_scale_factor = [](BeltScaleMode mode, double angle_deg) -> double { + if (mode == BeltScaleMode::None) return 1.; + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad); + double cos_a = std::cos(angle_rad); + switch (mode) { + case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; + case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; + case BeltScaleMode::Sin: return sin_a; + case BeltScaleMode::Cos: return cos_a; + default: return 1.; + } + }; + + // Read shear configs. + auto get_shear_mode = [this](const char *key) -> BeltShearMode { + auto opt = config->option>(key); + return opt ? opt->value : BeltShearMode::None; + }; + auto get_axis = [this](const char *key) -> BeltAxis { + auto opt = config->option>(key); + return opt ? opt->value : BeltAxis::X; + }; + auto get_scale_mode = [this](const char *key) -> BeltScaleMode { + auto opt = config->option>(key); + return opt ? opt->value : BeltScaleMode::None; + }; + + struct AxisShear { BeltShearMode mode; double angle; int from; }; + AxisShear axes[3] = { + { get_shear_mode("belt_shear_x"), config->opt_float("belt_shear_x_angle"), int(get_axis("belt_shear_x_from")) }, + { get_shear_mode("belt_shear_y"), config->opt_float("belt_shear_y_angle"), int(get_axis("belt_shear_y_from")) }, + { get_shear_mode("belt_shear_z"), config->opt_float("belt_shear_z_angle"), int(get_axis("belt_shear_z_from")) }, + }; + + Transform3d belt_shear = Transform3d::Identity(); + for (int row = 0; row < 3; ++row) { + if (axes[row].mode != BeltShearMode::None) { + double factor = compute_shear_factor(axes[row].mode, axes[row].angle); + if (std::abs(factor) > EPSILON) + belt_shear.matrix()(row, axes[row].from) += factor; + } + } + + double sx = compute_scale_factor(get_scale_mode("belt_scale_x"), config->opt_float("belt_scale_x_angle")); + double sy = compute_scale_factor(get_scale_mode("belt_scale_y"), config->opt_float("belt_scale_y_angle")); + double sz = compute_scale_factor(get_scale_mode("belt_scale_z"), config->opt_float("belt_scale_z_angle")); + Transform3d belt_scale = Transform3d::Identity(); + belt_scale.matrix()(0, 0) = sx; + belt_scale.matrix()(1, 1) = sy; + belt_scale.matrix()(2, 2) = sz; + + // Forward transform: scale * shear. Inverse for the viewer. + Transform3d forward = belt_scale * belt_shear; + Transform3d inverse = forward.inverse(); + if (preview) + preview->get_canvas3d()->get_gcode_viewer().set_belt_inverse_transform(inverse); } else { bed.set_belt_printer(false, 0.f); - if (preview) + if (preview) { preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(false, 0.f); + preview->get_canvas3d()->get_gcode_viewer().set_belt_inverse_transform(Transform3d::Identity()); + } } } @@ -13286,6 +13359,15 @@ void Plater::load_gcode(const wxString& filename) current_print.set_gcode_file_ready(); + // Belt printer: detect belt_printer_angle from loaded G-code header and enable + // belt view mode on the GCodeViewer so the "Show designed view" toggle appears. + if (current_result->belt_printer_angle > 0.f) { + float angle = current_result->belt_printer_angle; + p->preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(true, angle); + } else { + p->preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(false, 0.f); + } + // show results p->preview->reload_print(m_only_gcode); //BBS: zoom to bed 0 for gcode preview diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 87e97c5ff7..e6de38df78 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4364,58 +4364,6 @@ void TabPrinter::build_fff() optgroup->append_single_option_line(option, "printer_basic_information_printable_space#excluded-bed-area"); // optgroup->append_single_option_line("printable_area"); optgroup->append_single_option_line("printable_height", "printer_basic_information_printable_space#printable-height"); - optgroup->append_single_option_line("build_plate_tilt_x"); - optgroup->append_single_option_line("build_plate_tilt_y"); - optgroup->append_single_option_line("belt_printer"); - optgroup->append_single_option_line("belt_printer_angle"); - optgroup->append_single_option_line("belt_printer_infinite_y"); - // Per-axis shear: group mode + angle + source on one row per axis - { - Line line = { L("Shear X"), L("Shear applied to the X axis before slicing") }; - line.append_option(optgroup->get_option("belt_shear_x")); - line.append_option(optgroup->get_option("belt_shear_x_angle")); - line.append_option(optgroup->get_option("belt_shear_x_from")); - optgroup->append_line(line); - } - { - Line line = { L("Shear Y"), L("Shear applied to the Y axis before slicing") }; - line.append_option(optgroup->get_option("belt_shear_y")); - line.append_option(optgroup->get_option("belt_shear_y_angle")); - line.append_option(optgroup->get_option("belt_shear_y_from")); - optgroup->append_line(line); - } - { - Line line = { L("Shear Z"), L("Shear applied to the Z axis before slicing") }; - line.append_option(optgroup->get_option("belt_shear_z")); - line.append_option(optgroup->get_option("belt_shear_z_angle")); - line.append_option(optgroup->get_option("belt_shear_z_from")); - optgroup->append_line(line); - } - { - Line line = { L("Scale X"), L("Scale applied to the X axis before slicing") }; - line.append_option(optgroup->get_option("belt_scale_x")); - line.append_option(optgroup->get_option("belt_scale_x_angle")); - optgroup->append_line(line); - } - { - Line line = { L("Scale Y"), L("Scale applied to the Y axis before slicing") }; - line.append_option(optgroup->get_option("belt_scale_y")); - line.append_option(optgroup->get_option("belt_scale_y_angle")); - optgroup->append_line(line); - } - { - Line line = { L("Scale Z"), L("Scale applied to the Z axis before slicing") }; - line.append_option(optgroup->get_option("belt_scale_z")); - line.append_option(optgroup->get_option("belt_scale_z_angle")); - optgroup->append_line(line); - } - { - Line line = { L("G-code axis remap"), L("Remap slicing-frame axes to machine axes in G-code output") }; - line.append_option(optgroup->get_option("belt_gcode_remap_x")); - line.append_option(optgroup->get_option("belt_gcode_remap_y")); - line.append_option(optgroup->get_option("belt_gcode_remap_z")); - optgroup->append_line(line); - } optgroup->append_single_option_line("support_multi_bed_types","printer_basic_information_printable_space#support-multi-bed-types"); optgroup->append_single_option_line("best_object_pos", "printer_basic_information_printable_space#best-object-position"); // todo: for multi_extruder test @@ -4434,6 +4382,68 @@ void TabPrinter::build_fff() //option.opt.full_width = true; //optgroup->append_single_option_line(option); optgroup->append_single_option_line("disable_m73", "printer_basic_information_advanced#disable-set-remaining-print-time"); + optgroup->append_single_option_line("build_plate_tilt_x"); + optgroup->append_single_option_line("build_plate_tilt_y"); + optgroup->append_single_option_line("belt_printer"); + optgroup->append_single_option_line("belt_printer_angle"); + optgroup->append_single_option_line("belt_printer_infinite_y"); + // Per-axis shear: group mode + angle + source on one row per axis + { + Line line = { L("Mesh shear X"), L("Shear applied to the X axis before slicing") }; + line.append_option(optgroup->get_option("belt_shear_x")); + line.append_option(optgroup->get_option("belt_shear_x_angle")); + line.append_option(optgroup->get_option("belt_shear_x_from")); + line.append_option(optgroup->get_option("belt_shear_x_global")); + optgroup->append_line(line); + } + { + Line line = { L("Mesh shear Y"), L("Shear applied to the Y axis before slicing") }; + line.append_option(optgroup->get_option("belt_shear_y")); + line.append_option(optgroup->get_option("belt_shear_y_angle")); + line.append_option(optgroup->get_option("belt_shear_y_from")); + line.append_option(optgroup->get_option("belt_shear_y_global")); + optgroup->append_line(line); + } + { + Line line = { L("Mesh shear Z"), L("Shear applied to the Z axis before slicing") }; + line.append_option(optgroup->get_option("belt_shear_z")); + line.append_option(optgroup->get_option("belt_shear_z_angle")); + line.append_option(optgroup->get_option("belt_shear_z_from")); + line.append_option(optgroup->get_option("belt_shear_z_global")); + optgroup->append_line(line); + } + { + Line line = { L("Mesh scale X"), L("Scale applied to the X axis before slicing") }; + line.append_option(optgroup->get_option("belt_scale_x")); + line.append_option(optgroup->get_option("belt_scale_x_angle")); + optgroup->append_line(line); + } + { + Line line = { L("Mesh scale Y"), L("Scale applied to the Y axis before slicing") }; + line.append_option(optgroup->get_option("belt_scale_y")); + line.append_option(optgroup->get_option("belt_scale_y_angle")); + optgroup->append_line(line); + } + { + Line line = { L("Mesh scale Z"), L("Scale applied to the Z axis before slicing") }; + line.append_option(optgroup->get_option("belt_scale_z")); + line.append_option(optgroup->get_option("belt_scale_z_angle")); + optgroup->append_line(line); + } + { + Line line = { L("G-code axis remap"), L("Remap slicing-frame axes to machine axes in G-code output") }; + line.append_option(optgroup->get_option("belt_gcode_remap_x")); + line.append_option(optgroup->get_option("belt_gcode_remap_y")); + line.append_option(optgroup->get_option("belt_gcode_remap_z")); + optgroup->append_line(line); + } + { + Line line = { L("Support floor"), L("Belt floor awareness for support generation and clipping") }; + line.append_option(optgroup->get_option("belt_support_floor_mode")); + line.append_option(optgroup->get_option("belt_support_floor_offset")); + line.append_option(optgroup->get_option("belt_support_z_offset_mode")); + optgroup->append_line(line); + } option = optgroup->get_option("thumbnails"); option.opt.full_width = true; optgroup->append_single_option_line(option, "printer_basic_information_advanced#g-code-thumbnails"); @@ -5283,6 +5293,33 @@ void TabPrinter::toggle_options() "belt_scale_x", "belt_scale_y", "belt_scale_z", "belt_gcode_remap_x"}) toggle_line(el, is_belt); + + // Gray out angle/from sub-options when their parent shear/scale mode is None. + auto sx = m_config->option>("belt_shear_x")->value; + toggle_option("belt_shear_x_angle", is_belt && sx != BeltShearMode::None); + toggle_option("belt_shear_x_from", is_belt && sx != BeltShearMode::None); + toggle_option("belt_shear_x_global", is_belt && sx != BeltShearMode::None); + + auto sy = m_config->option>("belt_shear_y")->value; + toggle_option("belt_shear_y_angle", is_belt && sy != BeltShearMode::None); + toggle_option("belt_shear_y_from", is_belt && sy != BeltShearMode::None); + toggle_option("belt_shear_y_global", is_belt && sy != BeltShearMode::None); + + auto sz = m_config->option>("belt_shear_z")->value; + toggle_option("belt_shear_z_angle", is_belt && sz != BeltShearMode::None); + toggle_option("belt_shear_z_from", is_belt && sz != BeltShearMode::None); + toggle_option("belt_shear_z_global", is_belt && sz != BeltShearMode::None); + + auto scx = m_config->option>("belt_scale_x")->value; + toggle_option("belt_scale_x_angle", is_belt && scx != BeltScaleMode::None); + + auto scy = m_config->option>("belt_scale_y")->value; + toggle_option("belt_scale_y_angle", is_belt && scy != BeltScaleMode::None); + + auto scz = m_config->option>("belt_scale_z")->value; + toggle_option("belt_scale_z_angle", is_belt && scz != BeltScaleMode::None); + + toggle_line("belt_support_floor_mode", is_belt); } From b297f68921a74b95b108a9afb5d4f08fed3256c9 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 25 Mar 2026 22:19:31 -0500 Subject: [PATCH 006/224] Part 2.7: Add G-code back-transform and tree support belt floor clipping - Add BeltBackTransform class that inverts the shear/scale matrix and applies it in GCodeWriter::to_machine_coords() so G-code outputs in the machine's physical coordinate space, gated by new belt_gcode_back_transform config option - Extend belt floor clipping to all three tree support pipelines (Prusa-style, Orca organic, TreeModelVolumes) with per-layer polygon clipping, anti-overhang integration, and belt raft extension layers - Fix tree drop_nodes() belt termination, organic support global Z offset, collision calculation index bug, and first-layer brim/empty layer checks for belt printers two-shot - first build built but didn't plumb to UI. Woah. add pre-slice axis remap, because Y needs to be Z going to change tactic and move based on bbox min switch to per axis snapping per axis swap snap now per object build plate tilt wasn't invalidating slicer settings support upper bound now correct, need to get lower bound corrected axis swapped support termination corrected Z Shear works with and without pre-slice remap now --- src/libslic3r/CMakeLists.txt | 2 + src/libslic3r/GCode.cpp | 142 +++++++++++++++++++ src/libslic3r/GCode.hpp | 5 + src/libslic3r/GCode/BeltBackTransform.cpp | 150 +++++++++++++++++++++ src/libslic3r/GCode/BeltBackTransform.hpp | 45 +++++++ src/libslic3r/GCode/GCodeProcessor.cpp | 22 +++ src/libslic3r/GCode/GCodeProcessor.hpp | 6 + src/libslic3r/GCodeWriter.cpp | 32 ++++- src/libslic3r/GCodeWriter.hpp | 9 ++ src/libslic3r/Preset.cpp | 6 +- src/libslic3r/Print.cpp | 11 +- src/libslic3r/Print.hpp | 31 +++++ src/libslic3r/PrintConfig.cpp | 60 ++++++++- src/libslic3r/PrintConfig.hpp | 10 ++ src/libslic3r/PrintObject.cpp | 19 ++- src/libslic3r/PrintObjectSlice.cpp | 117 +++++++++++++--- src/libslic3r/Support/TreeModelVolumes.cpp | 2 +- src/libslic3r/Support/TreeSupport.cpp | 93 +++++++++++++ src/libslic3r/Support/TreeSupport3D.cpp | 2 +- src/slic3r/GUI/Tab.cpp | 39 +++++- 20 files changed, 767 insertions(+), 36 deletions(-) create mode 100644 src/libslic3r/GCode/BeltBackTransform.cpp create mode 100644 src/libslic3r/GCode/BeltBackTransform.hpp diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 5f9591452f..c2d92ee800 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -208,6 +208,8 @@ set(lisbslic3r_sources GCode/AdaptivePAProcessor.hpp GCode/AvoidCrossingPerimeters.cpp GCode/AvoidCrossingPerimeters.hpp + GCode/BeltBackTransform.cpp + GCode/BeltBackTransform.hpp GCode/ConflictChecker.cpp GCode/ConflictChecker.hpp GCode/CoolingBuffer.cpp diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 41cfa7b72a..8391f9b820 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2432,6 +2432,16 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Build volume extents for Rev remap mode. BoundingBoxf bbox_bed(print.config().printable_area.values); m_writer.set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(), print.config().printable_height.value)); + // Initialize the back-transform that undoes slicing shear/scale. + m_writer.set_belt_back_transform(print.config()); + // Per-axis origin snap: store config; actual snap is computed + // per-instance in update_origin_snap() called from set_origin(). + m_origin_snap[0] = print.config().belt_origin_snap_x.value; + m_origin_snap[1] = print.config().belt_origin_snap_y.value; + m_origin_snap[2] = print.config().belt_origin_snap_z.value; + m_origin_snap_offset[0] = print.config().belt_origin_offset_x.value; + m_origin_snap_offset[1] = print.config().belt_origin_offset_y.value; + m_origin_snap_offset[2] = print.config().belt_origin_offset_z.value; } // How many times will be change_layer() called? @@ -2544,6 +2554,9 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.write_format("; belt_scale_y_angle = %.1f\n", print.config().belt_scale_y_angle.value); file.write_format("; belt_scale_z = %s\n", full_cfg.opt_serialize("belt_scale_z").c_str()); file.write_format("; belt_scale_z_angle = %.1f\n", print.config().belt_scale_z_angle.value); + file.write_format("; belt_preslice_remap_x = %s\n", full_cfg.opt_serialize("belt_preslice_remap_x").c_str()); + file.write_format("; belt_preslice_remap_y = %s\n", full_cfg.opt_serialize("belt_preslice_remap_y").c_str()); + file.write_format("; belt_preslice_remap_z = %s\n", full_cfg.opt_serialize("belt_preslice_remap_z").c_str()); } if (is_bbl_printers) file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str()); @@ -3248,6 +3261,8 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } print.throw_if_canceled(); this->set_origin(unscale((*print_object_instance_sequential_active)->shift)); + this->update_origin_snap((*print_object_instance_sequential_active)->print_object, + (*print_object_instance_sequential_active)->shift); // BBS: prime extruder if extruder change happens before this object instance bool prime_extruder = false; @@ -5212,6 +5227,7 @@ LayerResult GCode::process_layer( m_avoid_crossing_perimeters.use_external_mp_once(); m_last_obj_copy = this_object_copy; this->set_origin(unscale(offset)); + this->update_origin_snap(&instance_to_print.print_object, offset); if (instance_to_print.object_by_extruder.support != nullptr) { m_layer = layers[instance_to_print.layer_id].support_layer; m_object_layer_over_raft = false; @@ -5235,6 +5251,7 @@ LayerResult GCode::process_layer( m_avoid_crossing_perimeters.use_external_mp_once(); m_last_obj_copy = this_object_copy; this->set_origin(unscale(offset)); + this->update_origin_snap(&instance_to_print.print_object, offset); ExtrusionEntityCollection support_eec; // BBS @@ -5283,6 +5300,7 @@ LayerResult GCode::process_layer( m_avoid_crossing_perimeters.use_external_mp_once(); m_last_obj_copy = this_object_copy; this->set_origin(unscale(offset)); + this->update_origin_snap(&instance_to_print.print_object, offset); //FIXME the following code prints regions in the order they are defined, the path is not optimized in any way. auto has_infill = [](const std::vector &by_region) { @@ -5509,6 +5527,130 @@ void GCode::set_origin(const Vec2d &pointf) m_origin = pointf; } +void GCode::update_origin_snap(const PrintObject *obj, const Point &inst_shift) +{ + if (!m_origin_snap[0] && !m_origin_snap[1] && !m_origin_snap[2]) + return; + + // Clear existing snap so to_machine_coords gives raw machine coords for bbox computation. + for (int a = 0; a < 3; ++a) + m_writer.set_origin_snap(a, false, 0., 0.); + + // Reconstruct the belt pipeline transform for this object (same as + // PrintObjectSlice.cpp: z_shift * scale * shear * pre_remap). + const auto &cfg = m_config; + Transform3d belt = Transform3d::Identity(); + + // Pre-slice remap + int pre_rx = int(cfg.belt_preslice_remap_x.value); + int pre_ry = int(cfg.belt_preslice_remap_y.value); + int pre_rz = int(cfg.belt_preslice_remap_z.value); + if (pre_rx != int(BeltRemapAxis::PosX) || + pre_ry != int(BeltRemapAxis::PosY) || + pre_rz != int(BeltRemapAxis::PosZ)) { + auto remap_col = [](int r) -> Vec3d { + int a = r % 3; Vec3d c = Vec3d::Zero(); + c[a] = (r < 3) ? 1.0 : -1.0; + return c; + }; + Matrix3d lin; + lin.col(0) = remap_col(pre_rx); + lin.col(1) = remap_col(pre_ry); + lin.col(2) = remap_col(pre_rz); + Transform3d pre = Transform3d::Identity(); + pre.linear() = lin; + if (pre_rx >= 6 || pre_ry >= 6 || pre_rz >= 6) { + BoundingBoxf bb(cfg.printable_area.values); + Vec3d vm(bb.max.x(), bb.max.y(), cfg.printable_height.value); + Vec3d tr = Vec3d::Zero(); + if (pre_rx >= 6) tr[0] = vm[pre_rx % 3]; + if (pre_ry >= 6) tr[1] = vm[pre_ry % 3]; + if (pre_rz >= 6) tr[2] = vm[pre_rz % 3]; + pre.translation() = tr; + } + belt = pre * belt; + } + + // Shear + auto shear_f = [](BeltShearMode m, double a) -> double { + double r = Geometry::deg2rad(a), s = std::sin(r), c = std::cos(r); + switch (m) { + case BeltShearMode::PosCot: return (s > EPSILON) ? c/s : 0.; + case BeltShearMode::NegCot: return (s > EPSILON) ? -c/s : 0.; + case BeltShearMode::PosTan: return (c > EPSILON) ? s/c : 0.; + case BeltShearMode::NegTan: return (c > EPSILON) ? -s/c : 0.; + default: return 0.; + } + }; + struct AS { BeltShearMode m; double a; int f; }; + AS axes[3] = { + {cfg.belt_shear_x.value, cfg.belt_shear_x_angle.value, int(cfg.belt_shear_x_from.value)}, + {cfg.belt_shear_y.value, cfg.belt_shear_y_angle.value, int(cfg.belt_shear_y_from.value)}, + {cfg.belt_shear_z.value, cfg.belt_shear_z_angle.value, int(cfg.belt_shear_z_from.value)}, + }; + Transform3d shear = Transform3d::Identity(); + for (int i = 0; i < 3; ++i) + if (axes[i].m != BeltShearMode::None) { + double f = shear_f(axes[i].m, axes[i].a); + if (std::abs(f) > EPSILON) + shear.matrix()(i, axes[i].f) += f; + } + + // Scale + auto scale_f = [](BeltScaleMode m, double a) -> double { + if (m == BeltScaleMode::None) return 1.; + double r = Geometry::deg2rad(a), s = std::sin(r), c = std::cos(r); + switch (m) { + case BeltScaleMode::InvSin: return (s > EPSILON) ? 1./s : 1.; + case BeltScaleMode::InvCos: return (c > EPSILON) ? 1./c : 1.; + case BeltScaleMode::Sin: return s; + case BeltScaleMode::Cos: return c; + default: return 1.; + } + }; + Transform3d sc = Transform3d::Identity(); + sc.matrix()(0,0) = scale_f(cfg.belt_scale_x.value, cfg.belt_scale_x_angle.value); + sc.matrix()(1,1) = scale_f(cfg.belt_scale_y.value, cfg.belt_scale_y_angle.value); + sc.matrix()(2,2) = scale_f(cfg.belt_scale_z.value, cfg.belt_scale_z_angle.value); + + belt = sc * shear * belt; + + // Z-shift + double zs = (obj->belt_min_z() < 0.) ? -obj->belt_min_z() : 0.; + if (zs > 0.) { + Transform3d zsh = Transform3d::Identity(); + zsh.matrix()(2, 3) = zs; + belt = zsh * belt; + } + + // Full transform: belt * trafo_centered + Transform3d full = belt * obj->trafo_centered(); + + // Instance shift in slicer space + global Z offset + Vec3d shift(unscale(inst_shift.x()), + unscale(inst_shift.y()), + obj->belt_global_z_offset()); + + // Compute this instance's machine-space bbox min + BoundingBoxf3 bb = obj->model_object()->raw_bounding_box(); + Vec3d mn = bb.min.cast(), mx = bb.max.cast(); + Vec3d inst_min(std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max()); + 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 mc = m_writer.to_machine_coords(full * c + shift); + for (int a = 0; a < 3; ++a) + inst_min[a] = std::min(inst_min[a], mc[a]); + } + + // Update writer snap for each enabled axis + for (int a = 0; a < 3; ++a) + m_writer.set_origin_snap(a, m_origin_snap[a], m_origin_snap_offset[a], inst_min[a]); +} + std::string GCode::preamble() { std::string gcode = m_writer.preamble(); diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index d379300bad..dbec7d0315 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -493,6 +493,11 @@ private: This affects the input arguments supplied to the extrude*() and travel_to() methods. */ Vec2d m_origin; + // Per-axis origin snap: shift G-code so each object's bbox min = offset. + bool m_origin_snap[3] = {false, false, false}; + double m_origin_snap_offset[3] = {0., 0., 0.}; + // Called when switching instances to recompute the writer's snap for this instance. + void update_origin_snap(const PrintObject *obj, const Point &inst_shift); FullPrintConfig m_config; DynamicConfig m_calib_config; // scaled G-code resolution diff --git a/src/libslic3r/GCode/BeltBackTransform.cpp b/src/libslic3r/GCode/BeltBackTransform.cpp new file mode 100644 index 0000000000..7cd5bc6b53 --- /dev/null +++ b/src/libslic3r/GCode/BeltBackTransform.cpp @@ -0,0 +1,150 @@ +#include "BeltBackTransform.hpp" +#include "../Geometry.hpp" +#include + +namespace Slic3r { + +// Keep in sync with PrintObjectSlice.cpp compute_shear_factor (lines ~147-157). +static double compute_shear_factor(BeltShearMode mode, double angle_deg) +{ + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad); + double cos_a = std::cos(angle_rad); + switch (mode) { + case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; + case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; + case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; + case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; + default: return 0.; + } +} + +// Keep in sync with PrintObjectSlice.cpp compute_scale_factor (lines ~180-192). +static double compute_scale_factor(BeltScaleMode mode, double angle_deg) +{ + if (mode == BeltScaleMode::None) return 1.; + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad); + double cos_a = std::cos(angle_rad); + switch (mode) { + case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; + case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; + case BeltScaleMode::Sin: return sin_a; + case BeltScaleMode::Cos: return cos_a; + default: return 1.; + } +} + +bool BeltBackTransform::init_from_config(const PrintConfig &config) +{ + m_active = false; + m_inverse = Transform3d::Identity(); + + if (!config.belt_printer.value || !config.belt_gcode_back_transform.value) + return false; + + // --- Pre-slice axis remap (same as PrintObjectSlice.cpp) --- + int pre_rx = int(config.belt_preslice_remap_x.value); + int pre_ry = int(config.belt_preslice_remap_y.value); + int pre_rz = int(config.belt_preslice_remap_z.value); + + bool has_preslice_remap = (pre_rx != int(BeltRemapAxis::PosX) || + pre_ry != int(BeltRemapAxis::PosY) || + pre_rz != int(BeltRemapAxis::PosZ)); + + // Require at least one active transform to proceed. + bool has_global_shear = config.belt_shear_x_global.value || + config.belt_shear_y_global.value || + config.belt_shear_z_global.value; + if (!has_global_shear && !has_preslice_remap) + return false; + + // Build pre-slice remap matrix. + Transform3d pre_remap = Transform3d::Identity(); + if (has_preslice_remap) { + auto remap_column = [](int r) -> Vec3d { + int axis = r % 3; + Vec3d col = Vec3d::Zero(); + if (r < 3) col[axis] = 1.0; + else if (r < 6) col[axis] = -1.0; + else col[axis] = -1.0; // Rev: max - pos + 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; + + // Rev mode translation (needs build volume extents). + Vec3d remap_trans = Vec3d::Zero(); + 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); + 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; + } + + // Build per-axis shear matrix (same as PrintObjectSlice.cpp). + struct AxisShear { BeltShearMode mode; double angle; int from; }; + AxisShear axes[3] = { + { config.belt_shear_x.value, config.belt_shear_x_angle.value, int(config.belt_shear_x_from.value) }, + { config.belt_shear_y.value, config.belt_shear_y_angle.value, int(config.belt_shear_y_from.value) }, + { config.belt_shear_z.value, config.belt_shear_z_angle.value, int(config.belt_shear_z_from.value) }, + }; + + Matrix3d shear = Matrix3d::Identity(); + bool has_shear = false; + for (int row = 0; row < 3; ++row) { + if (axes[row].mode != BeltShearMode::None) { + double factor = compute_shear_factor(axes[row].mode, axes[row].angle); + if (std::abs(factor) > EPSILON) { + shear(row, axes[row].from) += factor; + has_shear = true; + } + } + } + + // Build per-axis scale diagonal matrix (same as PrintObjectSlice.cpp). + double sx = compute_scale_factor(config.belt_scale_x.value, config.belt_scale_x_angle.value); + double sy = compute_scale_factor(config.belt_scale_y.value, config.belt_scale_y_angle.value); + double sz = compute_scale_factor(config.belt_scale_z.value, config.belt_scale_z_angle.value); + + Matrix3d scale = Matrix3d::Identity(); + bool has_scale = (std::abs(sx - 1.) > EPSILON || + std::abs(sy - 1.) > EPSILON || + std::abs(sz - 1.) > EPSILON); + if (has_scale) { + scale(0, 0) = sx; + scale(1, 1) = sy; + scale(2, 2) = sz; + } + + if (!has_shear && !has_scale && !has_preslice_remap) + return false; + + // Forward pipeline: scale * shear * pre_remap (same order as PrintObjectSlice.cpp). + Transform3d combined = Transform3d::Identity(); + combined.linear() = scale * shear; + combined = combined * pre_remap; + m_inverse = combined.inverse(); + m_active = true; + return true; +} + +Vec3d BeltBackTransform::apply(const Vec3d &pos) const +{ + if (!m_active) + return pos; + return m_inverse * pos; +} + +} // namespace Slic3r diff --git a/src/libslic3r/GCode/BeltBackTransform.hpp b/src/libslic3r/GCode/BeltBackTransform.hpp new file mode 100644 index 0000000000..5a75d94dc8 --- /dev/null +++ b/src/libslic3r/GCode/BeltBackTransform.hpp @@ -0,0 +1,45 @@ +#ifndef slic3r_BeltBackTransform_hpp_ +#define slic3r_BeltBackTransform_hpp_ + +#include "../libslic3r.h" +#include "../Point.hpp" +#include "../PrintConfig.hpp" + +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. +// +// Initialized once from PrintConfig, then applied per-point in +// GCodeWriter::to_machine_coords() before axis remapping. +// +// Active when belt_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. +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. + bool init_from_config(const PrintConfig &config); + + // Apply the inverse transform to a point. Returns pos unchanged if + // no back-transform is active. + Vec3d apply(const Vec3d &pos) const; + + // True if a non-identity back-transform is active. + bool is_active() const { return m_active; } + +private: + bool m_active = false; + Transform3d m_inverse = Transform3d::Identity(); +}; + +} // namespace Slic3r + +#endif // slic3r_BeltBackTransform_hpp_ diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 8def7c08db..4c630bb6de 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -3128,6 +3128,28 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers if (boost::starts_with(comment, " belt_scale_z_angle = ")) { try { m_result.belt_scale_z_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; } + // Pre-slice axis remap + auto parse_remap_axis = [](const std::string &s) -> BeltRemapAxis { + if (s == "pos_x") return BeltRemapAxis::PosX; + if (s == "pos_y") return BeltRemapAxis::PosY; + if (s == "pos_z") return BeltRemapAxis::PosZ; + if (s == "neg_x") return BeltRemapAxis::NegX; + if (s == "neg_y") return BeltRemapAxis::NegY; + if (s == "neg_z") return BeltRemapAxis::NegZ; + if (s == "rev_x") return BeltRemapAxis::RevX; + if (s == "rev_y") return BeltRemapAxis::RevY; + if (s == "rev_z") return BeltRemapAxis::RevZ; + return BeltRemapAxis::PosX; + }; + if (boost::starts_with(comment, " belt_preslice_remap_x = ")) { + m_result.belt_preslice_remap_x = parse_remap_axis(trim(std::string(comment.substr(25)))); return; + } + if (boost::starts_with(comment, " belt_preslice_remap_y = ")) { + m_result.belt_preslice_remap_y = parse_remap_axis(trim(std::string(comment.substr(25)))); return; + } + if (boost::starts_with(comment, " belt_preslice_remap_z = ")) { + m_result.belt_preslice_remap_z = parse_remap_axis(trim(std::string(comment.substr(25)))); return; + } } // wipe start tag if (boost::starts_with(comment, reserved_tag(ETags::Wipe_Start))) { diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index f953345f4e..7d08a9cdfd 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -246,6 +246,9 @@ class Print; float belt_scale_y_angle{ 45.f }; BeltScaleMode belt_scale_z{ BeltScaleMode::None }; float belt_scale_z_angle{ 45.f }; + BeltRemapAxis belt_preslice_remap_x{ BeltRemapAxis::PosX }; + BeltRemapAxis belt_preslice_remap_y{ BeltRemapAxis::PosY }; + BeltRemapAxis belt_preslice_remap_z{ BeltRemapAxis::PosZ }; SettingsIds settings_ids; size_t filaments_count; bool backtrace_enabled; @@ -321,6 +324,9 @@ class Print; belt_scale_y_angle = other.belt_scale_y_angle; belt_scale_z = other.belt_scale_z; belt_scale_z_angle = other.belt_scale_z_angle; + belt_preslice_remap_x = other.belt_preslice_remap_x; + belt_preslice_remap_y = other.belt_preslice_remap_y; + belt_preslice_remap_z = other.belt_preslice_remap_z; #if ENABLE_GCODE_VIEWER_STATISTICS time = other.time; #endif diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index d6bb5a0bb9..8c2f7122b8 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -37,18 +37,40 @@ void GCodeWriter::set_build_volume_max(const Vec3d &max) m_build_vol_max = max; } +void GCodeWriter::set_belt_back_transform(const PrintConfig &config) +{ + m_belt_back_transform.init_from_config(config); +} + +void GCodeWriter::set_origin_snap(int axis, bool enable, double offset, double bbox_min) +{ + if (axis >= 0 && axis < 3) { + m_origin_snap[axis] = enable; + m_origin_offset[axis] = offset; + m_origin_bbox_min[axis] = bbox_min; + } +} + Vec3d GCodeWriter::to_machine_coords(const Vec3d &pos) const { if (!is_belt_printer()) return pos; + // Step 1: Undo the shear/scale applied during slicing. + Vec3d p = m_belt_back_transform.apply(pos); + // Step 2: Apply axis remapping for the machine's coordinate convention. // BeltRemapAxis: 0-2 = +X/+Y/+Z, 3-5 = -X/-Y/-Z, 6-8 = Rev X/Y/Z - auto remap = [this, &pos](int r) -> double { + auto remap = [this, &p](int r) -> double { int axis = r % 3; - if (r < 3) return pos[axis]; - if (r < 6) return -pos[axis]; - return m_build_vol_max[axis] - pos[axis]; + if (r < 3) return p[axis]; + if (r < 6) return -p[axis]; + return m_build_vol_max[axis] - p[axis]; }; - return { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) }; + Vec3d result = { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) }; + // Per-axis origin snap: shift so bbox min on each enabled axis = offset. + for (int i = 0; i < 3; ++i) + if (m_origin_snap[i]) + result[i] -= (m_origin_bbox_min[i] - m_origin_offset[i]); + return result; } bool GCodeWriter::supports_separate_travel_acceleration(GCodeFlavor flavor) diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index 6074eb247c..2334f5d416 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -8,6 +8,7 @@ #include "Point.hpp" #include "PrintConfig.hpp" #include "GCode/CoolingBuffer.hpp" +#include "GCode/BeltBackTransform.hpp" namespace Slic3r { @@ -132,6 +133,10 @@ public: void set_axis_remap(int rx, int ry, int rz); // Set build volume extents for Rev remap mode (max X, Y, Z). void set_build_volume_max(const Vec3d &max); + // Initialize the belt back-transform that undoes slicing shear/scale. + void set_belt_back_transform(const PrintConfig &config); + // Set per-axis origin snap: shifts G-code so bbox min on this axis = offset. + void set_origin_snap(int axis, bool enable, double offset, double bbox_min); // Transform a point from the slicing frame to machine coordinates. Vec3d to_machine_coords(const Vec3d &pos) const; @@ -194,6 +199,10 @@ public: int m_remap_y = 1; int m_remap_z = 2; Vec3d m_build_vol_max = Vec3d::Zero(); + BeltBackTransform m_belt_back_transform; + bool m_origin_snap[3] = {false, false, false}; + double m_origin_offset[3] = {0., 0., 0.}; // target coord for bbox min + double m_origin_bbox_min[3] = {0., 0., 0.}; // computed bbox min in machine space double m_current_speed; bool m_is_first_layer = true; diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 9437b6098e..9cc5e03d40 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1014,7 +1014,11 @@ static std::vector s_Preset_printer_options { "belt_shear_y", "belt_shear_y_angle", "belt_shear_y_from", "belt_shear_y_global", "belt_shear_z", "belt_shear_z_angle", "belt_shear_z_from", "belt_shear_z_global", "belt_scale_x", "belt_scale_x_angle", "belt_scale_y", "belt_scale_y_angle", "belt_scale_z", "belt_scale_z_angle", - "belt_gcode_remap_x", "belt_gcode_remap_y", "belt_gcode_remap_z", + "belt_preslice_remap_x", "belt_preslice_remap_y", "belt_preslice_remap_z", + "belt_gcode_remap_x", "belt_gcode_remap_y", "belt_gcode_remap_z", "belt_gcode_back_transform", + "belt_origin_snap_x", "belt_origin_offset_x", + "belt_origin_snap_y", "belt_origin_offset_y", + "belt_origin_snap_z", "belt_origin_offset_z", "belt_support_floor_offset", "belt_support_floor_mode", "belt_support_z_offset_mode", "gcode_flavor", "fan_kickstart", "fan_speedup_time", "fan_speedup_overhangs", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 0aef12a74b..0b7190e8d1 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -103,6 +103,9 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n "belt_gcode_remap_x", "belt_gcode_remap_y", "belt_gcode_remap_z", + "belt_origin_snap_x", "belt_origin_offset_x", + "belt_origin_snap_y", "belt_origin_offset_y", + "belt_origin_snap_z", "belt_origin_offset_z", //BBS "additional_cooling_fan_speed", "reduce_crossing_wall", @@ -280,6 +283,9 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n // In Spiral Vase mode, holes are closed and only the largest area contour is kept at each layer. // Therefore toggling the Spiral Vase on / off requires complete reslicing. || opt_key == "spiral_mode" + // Build plate tilt changes slicing plane orientation. + || opt_key == "build_plate_tilt_x" + || opt_key == "build_plate_tilt_y" // Belt printer transform options change the mesh geometry before slicing. || opt_key == "belt_printer" || opt_key == "belt_printer_angle" @@ -300,7 +306,10 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "belt_scale_y" || opt_key == "belt_scale_y_angle" || opt_key == "belt_scale_z" - || opt_key == "belt_scale_z_angle") { + || opt_key == "belt_scale_z_angle" + || opt_key == "belt_preslice_remap_x" + || opt_key == "belt_preslice_remap_y" + || opt_key == "belt_preslice_remap_z") { osteps.emplace_back(posSlice); } else if ( opt_key == "belt_support_floor_offset" diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index d0c050f442..c13a1407c0 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -186,6 +186,36 @@ class ConstSupportLayerPtrsAdaptor : public ConstVectorOfPtrsAdaptor(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) +{ + BoundingBoxf3 bb = model_object.raw_bounding_box(); + int pre_rx = int(config.belt_preslice_remap_x.value); + int pre_ry = int(config.belt_preslice_remap_y.value); + int pre_rz = int(config.belt_preslice_remap_z.value); + if (pre_rx == int(BeltRemapAxis::PosX) && + pre_ry == int(BeltRemapAxis::PosY) && + pre_rz == int(BeltRemapAxis::PosZ)) + return bb; // Identity remap, no change. + 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(), mx = bb.max.cast(); + 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; +} + // Single instance of a PrintObject. // As multiple PrintObjects may be generated for a single ModelObject (their instances differ in rotation around Z), // ModelObject's instancess will be distributed among these multiple PrintObjects. @@ -579,6 +609,7 @@ private: double m_belt_min_z { 0.0 }; public: double belt_global_z_offset() const { return m_belt_global_z_offset; } + double belt_min_z() const { return m_belt_min_z; } private: diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 7d06f96a85..506bd17a29 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -6132,9 +6132,63 @@ void PrintConfigDef::init_fff_params() def->set_default_value(new ConfigOptionEnum(default_axis)); }; - add_belt_remap("belt_gcode_remap_x", "X", "Which slicing axis maps to machine X in G-code output.", BeltRemapAxis::PosX); - add_belt_remap("belt_gcode_remap_y", "Y", "Which slicing axis maps to machine Y in G-code output.", BeltRemapAxis::PosY); - add_belt_remap("belt_gcode_remap_z", "Z", "Which slicing axis maps to machine Z in G-code output.", BeltRemapAxis::PosZ); + add_belt_remap("belt_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.", + BeltRemapAxis::PosX); + add_belt_remap("belt_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.", + BeltRemapAxis::PosY); + add_belt_remap("belt_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.", + BeltRemapAxis::PosZ); + + add_belt_remap("belt_gcode_remap_x", "X", "Which slicing axis maps to machine X in G-code output. Applied AFTER slicing, during G-code generation.", BeltRemapAxis::PosX); + add_belt_remap("belt_gcode_remap_y", "Y", "Which slicing axis maps to machine Y in G-code output. Applied AFTER slicing, during G-code generation.", BeltRemapAxis::PosY); + add_belt_remap("belt_gcode_remap_z", "Z", "Which slicing axis maps to machine Z in G-code output. Applied AFTER slicing, during G-code generation.", BeltRemapAxis::PosZ); + + def = this->add("belt_gcode_back_transform", coBool); + def->label = L("G-code back-transform"); + def->category = L("Printable space"); + def->tooltip = L("Reverse the shear/scale transform applied during slicing so G-code " + "coordinates are in the machine's physical coordinate space. " + "Requires at least one shear axis with global mode enabled."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + + auto add_belt_origin_snap = [this](const char *key_snap, const char *key_offset, + const char *axis_label) { + auto def = this->add(key_snap, coBool); + def->label = L(axis_label); + def->category = L("Printable space"); + std::string tip = std::string("Shift G-code output so the object's bounding box minimum on machine ") + + axis_label + " equals the offset value."; + def->tooltip = L(tip); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + + def = this->add(key_offset, coFloat); + def->label = L("Offset"); + def->category = L("Printable space"); + def->tooltip = L("Target coordinate for the bounding box minimum on this machine axis."); + def->sidetext = L("mm"); + def->min = -10000; + def->max = 10000; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0)); + }; + add_belt_origin_snap("belt_origin_snap_x", "belt_origin_offset_x", "X"); + add_belt_origin_snap("belt_origin_snap_y", "belt_origin_offset_y", "Y"); + add_belt_origin_snap("belt_origin_snap_z", "belt_origin_offset_z", "Z"); // Belt support floor debug controls def = this->add("belt_support_floor_offset", coFloat); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index fc533bf6bb..8b469c4dc6 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1488,9 +1488,19 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionFloat, belt_scale_y_angle)) ((ConfigOptionEnum, belt_scale_z)) ((ConfigOptionFloat, belt_scale_z_angle)) + ((ConfigOptionEnum, belt_preslice_remap_x)) + ((ConfigOptionEnum, belt_preslice_remap_y)) + ((ConfigOptionEnum, belt_preslice_remap_z)) ((ConfigOptionEnum, belt_gcode_remap_x)) ((ConfigOptionEnum, belt_gcode_remap_y)) ((ConfigOptionEnum, belt_gcode_remap_z)) + ((ConfigOptionBool, belt_gcode_back_transform)) + ((ConfigOptionBool, belt_origin_snap_x)) + ((ConfigOptionFloat, belt_origin_offset_x)) + ((ConfigOptionBool, belt_origin_snap_y)) + ((ConfigOptionFloat, belt_origin_offset_y)) + ((ConfigOptionBool, belt_origin_snap_z)) + ((ConfigOptionFloat, belt_origin_offset_z)) ((ConfigOptionFloat, belt_support_floor_offset)) ((ConfigOptionEnum, belt_support_floor_mode)) ((ConfigOptionEnum, belt_support_z_offset_mode)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index f803cc0369..ba2ed408ce 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -3402,9 +3402,16 @@ void PrintObject::update_slicing_parameters() double belt_floor_shear_factor_out = 0.0; int belt_floor_from_axis_out = 1; double belt_floor_z_shift_out = 0.0; - // Belt shear/scale may change the effective Z height. + // Belt shear/scale/pre-remap may change the effective Z height. const auto &pcfg = this->print()->config(); if (pcfg.belt_printer.value) { + BoundingBoxf3 bb = belt_remapped_bbox(*this->model_object(), pcfg); + bool has_preslice_remap = (int(pcfg.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || + int(pcfg.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || + int(pcfg.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ)); + if (has_preslice_remap) + object_height = bb.size().z(); + bool has_z_shear = pcfg.belt_shear_z.value != BeltShearMode::None; bool has_z_scale = pcfg.belt_scale_z.value != BeltScaleMode::None; if (has_z_shear || has_z_scale) { @@ -3435,7 +3442,6 @@ void PrintObject::update_slicing_parameters() double scale_z = compute_scale_factor(pcfg.belt_scale_z.value, pcfg.belt_scale_z_angle.value); if (has_z_shear && std::abs(shear_factor) > EPSILON) { int from = int(pcfg.belt_shear_z_from.value); - BoundingBoxf3 bb = this->model_object()->raw_bounding_box(); double min_rz = std::numeric_limits::max(); double max_rz = std::numeric_limits::lowest(); for (double vz : {bb.min.z(), bb.max.z()}) @@ -3507,8 +3513,15 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full if (object_max_z <= 0.f) { BoundingBoxf3 bb = model_object.raw_bounding_box(); object_max_z = (float)bb.size().z(); - // Belt shear/scale may change the effective Z height. + // Belt pre-remap/shear/scale may change the effective Z height. if (print_config.belt_printer.value) { + bb = belt_remapped_bbox(model_object, print_config); + bool has_preslice_remap = (int(print_config.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || + int(print_config.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || + int(print_config.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ)); + if (has_preslice_remap) + object_max_z = (float)bb.size().z(); + bool has_z_shear = print_config.belt_shear_z.value != BeltShearMode::None; bool has_z_scale = print_config.belt_scale_z.value != BeltScaleMode::None; if (has_z_shear || has_z_scale) { diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 404550da1a..4c767ea0ed 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -143,6 +143,58 @@ static std::vector slice_volumes_inner( params_base.extra_offset = 0; params_base.trafo = object_trafo; if (print_config.belt_printer.value) { + // --- Pre-slice axis remap --- + // Permutes/negates model axes before slicing so the slicer's coordinate + // system matches the physical bed orientation (e.g. XZ bed instead of XY). + int pre_rx = int(print_config.belt_preslice_remap_x.value); + int pre_ry = int(print_config.belt_preslice_remap_y.value); + int pre_rz = int(print_config.belt_preslice_remap_z.value); + + bool has_preslice_remap = (pre_rx != int(BeltRemapAxis::PosX) || + pre_ry != int(BeltRemapAxis::PosY) || + pre_rz != int(BeltRemapAxis::PosZ)); + + if (has_preslice_remap) { + // Build volume extents for Rev mode. + BoundingBoxf bbox_bed(print_config.printable_area.values); + Vec3d vol_max(bbox_bed.max.x(), bbox_bed.max.y(), + print_config.printable_height.value); + + // Each remap value selects a source axis and sign. + // The column vector tells the matrix which input axis feeds this output. + 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); + + // Translation for Rev modes: output = max[src] - input[src]. + Vec3d remap_trans = Vec3d::Zero(); + auto add_rev_offset = [&](int r, int out_axis) { + if (r >= 6) { + int src_axis = r % 3; + remap_trans[out_axis] = vol_max[src_axis]; + } + }; + add_rev_offset(pre_rx, 0); + add_rev_offset(pre_ry, 1); + add_rev_offset(pre_rz, 2); + + Transform3d pre_remap = Transform3d::Identity(); + pre_remap.linear() = remap_lin; + pre_remap.translation() = remap_trans; + + params_base.trafo = pre_remap * params_base.trafo; + } + // Build per-axis shear matrix from 3 independent axis configs. auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { double angle_rad = Geometry::deg2rad(angle_deg); @@ -204,11 +256,12 @@ static std::vector slice_volumes_inner( } // Apply: scale * shear * trafo (shear first, then scale). - if (has_shear || has_scale) { + if (has_shear || has_scale) params_base.trafo = belt_scale * belt_shear * params_base.trafo; - // After the shear/scale transform, the mesh may clip through the - // build plate (Z < 0). Detect this and shift the mesh up. + // After pre-remap/shear/scale, the mesh may clip through the build + // plate (Z < 0). Detect this and shift the mesh up along slicer Z. + if (has_preslice_remap || has_shear || has_scale) { Transform3d combined = params_base.trafo; double min_z = std::numeric_limits::max(); for (const ModelVolume *mv : model_volumes) { @@ -893,17 +946,34 @@ void PrintObject::slice() this->slice_volumes(); m_print->throw_if_canceled(); - // After slicing, m_belt_min_z holds the exact post-shear minimum Z - // in trafo_centered space (which includes the ensure_on_bed Z offset). - // The belt surface is at Z=0 in trafo_centered space; after shear it - // becomes Z = sf*Y, and after the z-shift that keeps the mesh above - // Z=0 it becomes Z = sf*Y + z_shift_val. So belt_floor_z_shift is - // simply the z-shift applied, i.e. max(0, -m_belt_min_z). - // NOTE: do NOT add raw_bounding_box().min.z() here — m_belt_min_z - // already includes the ensure_on_bed offset, unlike the min_rz used - // in update_slicing_parameters() which needs that compensation. + // Belt floor Z-shift: where is the belt surface in final slicer space? + // + // The belt surface is at model_Y=0 (XZ belt plane). After the full + // pipeline (trafo_centered → pre_remap → shear → z_shift), the belt + // surface equation in slicer space is: + // Z_belt = sf * from_axis + belt_surface_z_centered + z_shift_val + // + // belt_surface_z_centered = remapped_bbox.min.z() (the Z position of + // the belt surface in centered-pre-shear slicer space, which is 0 + // without pre-remap but nonzero when e.g. Y↔Z swap shifts the belt + // surface away from Z=0 by the centering offset). + // + // z_shift_val = max(0, -m_belt_min_z) (lifts mesh above Z=0). + // + // So: belt_floor_z_shift = remapped_bb.min.z() + z_shift_val if (std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON) { - m_slicing_params.belt_floor_z_shift = (m_belt_min_z < 0.) ? -m_belt_min_z : 0.; + 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(); + bool has_preslice_remap = (int(pcfg.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || + int(pcfg.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || + int(pcfg.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ)); + double belt_surface_z = has_preslice_remap + ? belt_remapped_bbox(*this->model_object(), pcfg).min.z() : 0.; + m_slicing_params.belt_floor_z_shift = belt_surface_z + z_shift_val; } int firstLayerReplacedBy = 0; @@ -989,14 +1059,23 @@ void PrintObject::slice() const auto &za = gaxes[2]; // Z row if (za.global && za.mode != BeltShearMode::None && za.from < 2) { double factor = compute_shear_factor(za.mode, za.angle); - // The Z-shift brought the mesh's lowest sheared vertex to - // Z=0. That vertex's physical Y determines the belt contact - // point. With trafo_z preserved (ensure_on_bed offset), - // min_z = Y_at_contact * factor for bottom-face vertices, - // so: z_offset = center_Y * factor + min_z. + // The global Z offset accounts for the instance's position- + // dependent shear contribution. m_belt_min_z is the minimum Z + // of the mesh after pre_remap + shear + trafo_centered, which + // includes the centering offset on the remapped Z axis. + // Subtract the belt surface's centered Z position so we get + // only the shear-induced contribution (same correction as the + // belt_floor_z_shift fix). + // Same pre-remap guard as belt_floor_z_shift above. + bool has_preslice_remap2 = (int(pcfg.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || + int(pcfg.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || + int(pcfg.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ)); + double belt_surface_z = has_preslice_remap2 + ? belt_remapped_bbox(*this->model_object(), this->print()->config()).min.z() : 0.; + double shear_min_z = m_belt_min_z - belt_surface_z; Point phys = inst_shift; // already has center_offset subtracted double center_on_axis = (za.from == 0) ? unscale(phys.x()) : unscale(phys.y()); - global_z_offset += center_on_axis * factor + m_belt_min_z; + global_z_offset += center_on_axis * factor + shear_min_z; } BOOST_LOG_TRIVIAL(warning) << "Belt global: z_offset=" << global_z_offset diff --git a/src/libslic3r/Support/TreeModelVolumes.cpp b/src/libslic3r/Support/TreeModelVolumes.cpp index e270b62aab..ec38ffed73 100644 --- a/src/libslic3r/Support/TreeModelVolumes.cpp +++ b/src/libslic3r/Support/TreeModelVolumes.cpp @@ -151,7 +151,7 @@ TreeModelVolumes::TreeModelVolumes( 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(print_object.model_object()->raw_bounding_box().min.z()); + 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) { diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 4966a5a139..60e681add9 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -1733,6 +1733,99 @@ 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(). + { + const auto &sp = m_slicing_params; + const auto &pcfg = *m_print_config; + const double sf = sp.belt_floor_shear_factor; + if (std::abs(sf) > EPSILON + && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly + && m_object->support_layer_count() > 0) { + const int from_axis = sp.belt_floor_from_axis; + const double floor_off = pcfg.belt_support_floor_offset.value; + // Support layer print_z values are in GLOBAL Z (non-organic inherits + // from object layers which include global_z_offset). Use the GLOBAL + // belt_floor_z_shift to match. + const double z_shift = sp.belt_floor_z_shift; + // 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; + } + } + } + if (!source_areas.empty()) { + BoundingBoxf3 bb = belt_remapped_bbox(*m_object->model_object(), m_object->print()->config()); + double from_extent = std::abs(bb.min(from_axis)); + double bb_min_z = std::abs(bb.min.z()); + double first_z = m_object->get_support_layer(0)->print_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)); + int num_extra = std::max(0, (int)std::ceil(extra_depth / sp.layer_height)); + 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; + double cutoff = (print_z - z_shift - floor_off) / sf; + coord_t cutoff_sc = scale_(cutoff); + coord_t big = scale_(1e3); + Polygon belt_poly; + if (from_axis == 0) { + if (sf > 0) belt_poly.points = {{cutoff_sc,-big},{big,-big},{big,big},{cutoff_sc,big}}; + else belt_poly.points = {{-big,-big},{cutoff_sc,-big},{cutoff_sc,big},{-big,big}}; + } else { + if (sf > 0) belt_poly.points = {{-big,cutoff_sc},{big,cutoff_sc},{big,big},{-big,big}}; + else belt_poly.points = {{-big,-big},{big,-big},{big,cutoff_sc},{-big,cutoff_sc}}; + } + ExPolygons clipped = diff_ex(source_areas, Polygons{belt_poly}); + 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. + for (auto &expoly : sl->base_areas) + sl->area_groups.emplace_back(&expoly, SupportLayer::BaseType, 0); + 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()); + } + } + } + } + profiler.stage_start(STAGE_GENERATE_TOOLPATHS); m_object->print()->set_status(70, _u8L("Generating support")); generate_toolpaths(); diff --git a/src/libslic3r/Support/TreeSupport3D.cpp b/src/libslic3r/Support/TreeSupport3D.cpp index b551a42ece..9d35cb2dd8 100644 --- a/src/libslic3r/Support/TreeSupport3D.cpp +++ b/src/libslic3r/Support/TreeSupport3D.cpp @@ -3381,7 +3381,7 @@ static void generate_support_areas(Print &print, TreeSupport* tree_support, cons // 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(po.model_object()->raw_bounding_box().min.z()); + 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) { diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index e6de38df78..d5a6b38ebc 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4431,12 +4431,41 @@ void TabPrinter::build_fff() optgroup->append_line(line); } { - Line line = { L("G-code axis remap"), L("Remap slicing-frame axes to machine axes in G-code output") }; + 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.append_option(optgroup->get_option("belt_preslice_remap_x")); + line.append_option(optgroup->get_option("belt_preslice_remap_y")); + line.append_option(optgroup->get_option("belt_preslice_remap_z")); + optgroup->append_line(line); + } + { + Line line = { L("G-code axis remap (post-slice)"), L("Remap slicing-frame axes to machine axes in G-code output. Applied AFTER slicing, during G-code generation.") }; line.append_option(optgroup->get_option("belt_gcode_remap_x")); line.append_option(optgroup->get_option("belt_gcode_remap_y")); line.append_option(optgroup->get_option("belt_gcode_remap_z")); optgroup->append_line(line); } + optgroup->append_single_option_line("belt_gcode_back_transform"); + { + Line line = { L("Origin snap X"), L("Snap object bbox min X to offset in G-code output") }; + line.append_option(optgroup->get_option("belt_origin_snap_x")); + line.append_option(optgroup->get_option("belt_origin_offset_x")); + optgroup->append_line(line); + } + { + Line line = { L("Origin snap Y"), L("Snap object bbox min Y to offset in G-code output") }; + line.append_option(optgroup->get_option("belt_origin_snap_y")); + line.append_option(optgroup->get_option("belt_origin_offset_y")); + optgroup->append_line(line); + } + { + Line line = { L("Origin snap Z"), L("Snap object bbox min Z to offset in G-code output") }; + line.append_option(optgroup->get_option("belt_origin_snap_z")); + line.append_option(optgroup->get_option("belt_origin_offset_z")); + optgroup->append_line(line); + } { Line line = { L("Support floor"), L("Belt floor awareness for support generation and clipping") }; line.append_option(optgroup->get_option("belt_support_floor_mode")); @@ -5291,7 +5320,9 @@ void TabPrinter::toggle_options() toggle_line("belt_printer_infinite_y", is_belt); for (auto el : {"belt_shear_x", "belt_shear_y", "belt_shear_z", "belt_scale_x", "belt_scale_y", "belt_scale_z", - "belt_gcode_remap_x"}) + "belt_preslice_remap_x", + "belt_gcode_remap_x", "belt_gcode_back_transform", + "belt_origin_snap_x", "belt_origin_snap_y", "belt_origin_snap_z"}) toggle_line(el, is_belt); // Gray out angle/from sub-options when their parent shear/scale mode is None. @@ -5319,6 +5350,10 @@ void TabPrinter::toggle_options() auto scz = m_config->option>("belt_scale_z")->value; toggle_option("belt_scale_z_angle", is_belt && scz != BeltScaleMode::None); + toggle_option("belt_origin_offset_x", is_belt && m_config->opt_bool("belt_origin_snap_x")); + toggle_option("belt_origin_offset_y", is_belt && m_config->opt_bool("belt_origin_snap_y")); + toggle_option("belt_origin_offset_z", is_belt && m_config->opt_bool("belt_origin_snap_z")); + toggle_line("belt_support_floor_mode", is_belt); } From c7aa4ca3eff1ae459c7017b3d32a8e03b68d6966 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 27 Mar 2026 17:34:19 -0500 Subject: [PATCH 007/224] Part 3.1: refactor BeltTransform pipeline add BeltGCodeWriter add BeltGCode consolidate changes into shared classes for BeltGcode --- src/libslic3r/BeltGCode.cpp | 119 +++++ src/libslic3r/BeltGCode.hpp | 28 ++ src/libslic3r/BeltGCodeWriter.cpp | 243 +++++++++ src/libslic3r/BeltGCodeWriter.hpp | 50 ++ src/libslic3r/BeltSliceStrategy.cpp | 62 +++ src/libslic3r/BeltSliceStrategy.hpp | 45 ++ src/libslic3r/BeltTransform.cpp | 246 ++++++++++ src/libslic3r/BeltTransform.hpp | 146 ++++++ src/libslic3r/CMakeLists.txt | 10 + src/libslic3r/GCode.cpp | 542 +++++++-------------- src/libslic3r/GCode.hpp | 34 +- src/libslic3r/GCode/BeltBackTransform.cpp | 124 +---- src/libslic3r/GCodeWriter.cpp | 127 +---- src/libslic3r/GCodeWriter.hpp | 74 +-- src/libslic3r/Print.cpp | 14 +- src/libslic3r/Print.hpp | 26 +- src/libslic3r/PrintObject.cpp | 143 +----- src/libslic3r/PrintObjectSlice.cpp | 177 +------ src/libslic3r/Support/BeltFloorContext.cpp | 124 +++++ src/libslic3r/Support/BeltFloorContext.hpp | 74 +++ src/libslic3r/Support/SupportMaterial.cpp | 131 +---- src/libslic3r/Support/TreeModelVolumes.cpp | 74 +-- src/libslic3r/Support/TreeSupport.cpp | 102 ++-- src/libslic3r/Support/TreeSupport3D.cpp | 36 +- 24 files changed, 1522 insertions(+), 1229 deletions(-) create mode 100644 src/libslic3r/BeltGCode.cpp create mode 100644 src/libslic3r/BeltGCode.hpp create mode 100644 src/libslic3r/BeltGCodeWriter.cpp create mode 100644 src/libslic3r/BeltGCodeWriter.hpp create mode 100644 src/libslic3r/BeltSliceStrategy.cpp create mode 100644 src/libslic3r/BeltSliceStrategy.hpp create mode 100644 src/libslic3r/BeltTransform.cpp create mode 100644 src/libslic3r/BeltTransform.hpp create mode 100644 src/libslic3r/Support/BeltFloorContext.cpp create mode 100644 src/libslic3r/Support/BeltFloorContext.hpp diff --git a/src/libslic3r/BeltGCode.cpp b/src/libslic3r/BeltGCode.cpp new file mode 100644 index 0000000000..dc1b6ca439 --- /dev/null +++ b/src/libslic3r/BeltGCode.cpp @@ -0,0 +1,119 @@ +#include "BeltGCode.hpp" +#include "BeltGCodeWriter.hpp" +#include "BeltTransform.hpp" +#include "Print.hpp" + +#include + +namespace Slic3r { + +void BeltGCode::init_belt_writer(Print &print, bool is_bbl_printers) +{ + if (!print.config().belt_printer.value) + return; + + auto belt_writer = std::make_unique(); + belt_writer->set_is_bbl_machine(is_bbl_printers); + belt_writer->set_belt_angle(print.config().belt_printer_angle.value); + belt_writer->set_axis_remap( + int(print.config().belt_gcode_remap_x.value), + int(print.config().belt_gcode_remap_y.value), + int(print.config().belt_gcode_remap_z.value)); + BoundingBoxf bbox_bed(print.config().printable_area.values); + belt_writer->set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(), + print.config().printable_height.value)); + belt_writer->set_belt_back_transform(print.config()); + m_writer = std::move(belt_writer); + + // Per-axis origin snap config. + m_origin_snap[0] = print.config().belt_origin_snap_x.value; + m_origin_snap[1] = print.config().belt_origin_snap_y.value; + m_origin_snap[2] = print.config().belt_origin_snap_z.value; + m_origin_snap_offset[0] = print.config().belt_origin_offset_x.value; + m_origin_snap_offset[1] = print.config().belt_origin_offset_y.value; + m_origin_snap_offset[2] = print.config().belt_origin_offset_z.value; +} + +void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print) +{ + if (!print.config().belt_printer.value) + return; + + file.write_format("; belt_printer_angle = %.1f\n", print.config().belt_printer_angle.value); + // Shear configs + const auto &full_cfg = print.full_print_config(); + file.write_format("; belt_shear_x = %s\n", full_cfg.opt_serialize("belt_shear_x").c_str()); + file.write_format("; belt_shear_x_angle = %.1f\n", print.config().belt_shear_x_angle.value); + file.write_format("; belt_shear_x_from = %s\n", full_cfg.opt_serialize("belt_shear_x_from").c_str()); + file.write_format("; belt_shear_y = %s\n", full_cfg.opt_serialize("belt_shear_y").c_str()); + file.write_format("; belt_shear_y_angle = %.1f\n", print.config().belt_shear_y_angle.value); + file.write_format("; belt_shear_y_from = %s\n", full_cfg.opt_serialize("belt_shear_y_from").c_str()); + file.write_format("; belt_shear_z = %s\n", full_cfg.opt_serialize("belt_shear_z").c_str()); + file.write_format("; belt_shear_z_angle = %.1f\n", print.config().belt_shear_z_angle.value); + file.write_format("; belt_shear_z_from = %s\n", full_cfg.opt_serialize("belt_shear_z_from").c_str()); + // Scale configs + file.write_format("; belt_scale_x = %s\n", full_cfg.opt_serialize("belt_scale_x").c_str()); + file.write_format("; belt_scale_x_angle = %.1f\n", print.config().belt_scale_x_angle.value); + file.write_format("; belt_scale_y = %s\n", full_cfg.opt_serialize("belt_scale_y").c_str()); + file.write_format("; belt_scale_y_angle = %.1f\n", print.config().belt_scale_y_angle.value); + file.write_format("; belt_scale_z = %s\n", full_cfg.opt_serialize("belt_scale_z").c_str()); + file.write_format("; belt_scale_z_angle = %.1f\n", print.config().belt_scale_z_angle.value); + // Pre-slice remap configs + file.write_format("; belt_preslice_remap_x = %s\n", full_cfg.opt_serialize("belt_preslice_remap_x").c_str()); + file.write_format("; belt_preslice_remap_y = %s\n", full_cfg.opt_serialize("belt_preslice_remap_y").c_str()); + file.write_format("; belt_preslice_remap_z = %s\n", full_cfg.opt_serialize("belt_preslice_remap_z").c_str()); +} + +void BeltGCode::on_set_origin(const PrintObject *obj, const Point &inst_shift) +{ + if (!m_origin_snap[0] && !m_origin_snap[1] && !m_origin_snap[2]) + return; + + auto *belt_writer = dynamic_cast(m_writer.get()); + if (!belt_writer) + return; + + // Clear existing snap so to_machine_coords gives raw machine coords for bbox computation. + for (int a = 0; a < 3; ++a) + belt_writer->set_origin_snap(a, false, 0., 0.); + + // Reconstruct the belt pipeline transform for this object. + Transform3d belt = BeltTransformPipeline::build_forward_transform(m_config); + + // Z-shift + double zs = (obj->belt_min_z() < 0.) ? -obj->belt_min_z() : 0.; + if (zs > 0.) { + Transform3d zsh = Transform3d::Identity(); + zsh.matrix()(2, 3) = zs; + belt = zsh * belt; + } + + // Full transform: belt * trafo_centered + Transform3d full = belt * obj->trafo_centered(); + + // Instance shift in slicer space + global Z offset + Vec3d shift(unscale(inst_shift.x()), + unscale(inst_shift.y()), + obj->belt_global_z_offset()); + + // Compute this instance's machine-space bbox min + BoundingBoxf3 bb = obj->model_object()->raw_bounding_box(); + Vec3d mn = bb.min.cast(), mx = bb.max.cast(); + Vec3d inst_min(std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max()); + 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 mc = belt_writer->to_machine_coords(full * c + shift); + for (int a = 0; a < 3; ++a) + inst_min[a] = std::min(inst_min[a], mc[a]); + } + + // Update writer snap for each enabled axis + for (int a = 0; a < 3; ++a) + belt_writer->set_origin_snap(a, m_origin_snap[a], m_origin_snap_offset[a], inst_min[a]); +} + +} // namespace Slic3r diff --git a/src/libslic3r/BeltGCode.hpp b/src/libslic3r/BeltGCode.hpp new file mode 100644 index 0000000000..682deb7d65 --- /dev/null +++ b/src/libslic3r/BeltGCode.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "GCode.hpp" + +namespace Slic3r { + +// Belt-printer-specific GCode export. +// +// Inherits from GCode and overrides virtual hooks to: +// - Create a BeltGCodeWriter instead of a plain GCodeWriter +// - Write belt configuration to the G-code header +// - Update per-axis origin snap when switching instances +// - Disable arc fitting (G2/G3 not supported on belt printers) +class BeltGCode : public GCode +{ +protected: + void init_belt_writer(Print &print, bool is_bbl_printers) override; + void write_belt_header(GCodeOutputStream &file, const Print &print) override; + void on_set_origin(const PrintObject *obj, const Point &inst_shift) override; + bool should_disable_arc_fitting() const override { return true; } + +private: + // Per-axis origin snap config (set during init, used in on_set_origin). + bool m_origin_snap[3] = {false, false, false}; + double m_origin_snap_offset[3] = {0., 0., 0.}; +}; + +} // namespace Slic3r diff --git a/src/libslic3r/BeltGCodeWriter.cpp b/src/libslic3r/BeltGCodeWriter.cpp new file mode 100644 index 0000000000..645bf85a5b --- /dev/null +++ b/src/libslic3r/BeltGCodeWriter.cpp @@ -0,0 +1,243 @@ +#include "BeltGCodeWriter.hpp" +#include "Geometry.hpp" + +namespace Slic3r { + +// ---- Belt configuration --------------------------------------------------- + +void BeltGCodeWriter::set_belt_angle(double angle_deg) +{ + m_belt_angle_rad = Geometry::deg2rad(angle_deg); +} + +void BeltGCodeWriter::set_axis_remap(int rx, int ry, int rz) +{ + m_remap_x = rx; + m_remap_y = ry; + m_remap_z = rz; +} + +void BeltGCodeWriter::set_build_volume_max(const Vec3d &max) +{ + m_build_vol_max = max; +} + +void BeltGCodeWriter::set_belt_back_transform(const PrintConfig &config) +{ + m_belt_back_transform.init_from_config(config); +} + +void BeltGCodeWriter::set_origin_snap(int axis, bool enable, double offset, double bbox_min) +{ + if (axis >= 0 && axis < 3) { + m_origin_snap[axis] = enable; + m_origin_offset[axis] = offset; + m_origin_bbox_min[axis] = bbox_min; + } +} + +Vec3d BeltGCodeWriter::to_machine_coords(const Vec3d &pos) const +{ + // Step 1: Undo the shear/scale applied during slicing. + Vec3d p = m_belt_back_transform.apply(pos); + // Step 2: Apply axis remapping for the machine's coordinate convention. + // BeltRemapAxis: 0-2 = +X/+Y/+Z, 3-5 = -X/-Y/-Z, 6-8 = Rev X/Y/Z + auto remap = [this, &p](int r) -> double { + int axis = r % 3; + if (r < 3) return p[axis]; + if (r < 6) return -p[axis]; + return m_build_vol_max[axis] - p[axis]; + }; + Vec3d result = { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) }; + // Per-axis origin snap: shift so bbox min on each enabled axis = offset. + for (int i = 0; i < 3; ++i) + if (m_origin_snap[i]) + result[i] -= (m_origin_bbox_min[i] - m_origin_offset[i]); + return result; +} + +// ---- Overridden movement methods ------------------------------------------ + +std::string BeltGCodeWriter::travel_to_xy(const Vec2d &point, const std::string &comment) +{ + m_pos(0) = point(0); + m_pos(1) = point(1); + + this->set_current_position_clear(true); + Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; + + // Belt printer: transform to machine coordinates (XY travel also needs Z due to YZ rotation) + Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); + + GCodeG1Formatter w; + w.emit_xyz(machine); + auto speed = m_is_first_layer + ? this->config.get_abs_value("initial_layer_travel_speed") : this->config.travel_speed.value; + w.emit_f(speed * 60.0); + w.emit_comment(GCodeWriter::full_gcode_comment, comment); + return w.string(); +} + +std::string BeltGCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase) +{ + // Belt printer: force NormalLift since SpiralLift and SlopeLift compute + // slope angles that don't account for the YZ coordinate rotation. + return GCodeWriter::lazy_lift(LiftType::NormalLift, spiral_vase); +} + +std::string BeltGCodeWriter::eager_lift(const LiftType type) +{ + // Belt printer: force NormalLift (SpiralLift/SlopeLift don't account for YZ rotation). + return GCodeWriter::eager_lift(LiftType::NormalLift); +} + +std::string BeltGCodeWriter::_travel_to_z(double z, const std::string &comment) +{ + m_pos(2) = z; + + double speed = this->config.travel_speed_z.value; + if (speed == 0.) { + speed = m_is_first_layer ? this->config.get_abs_value("initial_layer_travel_speed") + : this->config.travel_speed.value; + } + + // Belt printer: a Z-only move in slicing frame needs to emit both Y and Z in machine coords. + Vec3d machine = to_machine_coords(Vec3d(m_pos.x() - m_x_offset, m_pos.y() - m_y_offset, z)); + + GCodeG1Formatter w; + w.emit_xyz(machine); + w.emit_f(speed * 60.0); + w.emit_comment(GCodeWriter::full_gcode_comment, comment); + return w.string(); +} + +std::string BeltGCodeWriter::extrude_to_xy(const Vec2d &point, double dE, const std::string &comment, bool force_no_extrusion) +{ + m_pos(0) = point(0); + m_pos(1) = point(1); + if (std::abs(dE) <= std::numeric_limits::epsilon()) + force_no_extrusion = true; + + if (!force_no_extrusion) + filament()->extrude(dE); + + Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; + + // Belt printer: transform and emit XYZ (Y and Z are coupled) + Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); + + GCodeG1Formatter w; + w.emit_xyz(machine); + if (!force_no_extrusion) + w.emit_e(filament()->E()); + w.emit_comment(GCodeWriter::full_gcode_comment, comment); + return w.string(); +} + +std::string BeltGCodeWriter::extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment, bool force_no_extrusion) +{ + m_pos = point; + m_lifted = 0; + if (!force_no_extrusion) + filament()->extrude(dE); + + Vec3d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset, point(2) }; + point_on_plate = to_machine_coords(point_on_plate); + + GCodeG1Formatter w; + w.emit_xyz(point_on_plate); + if (!force_no_extrusion) + w.emit_e(filament()->E()); + w.emit_comment(GCodeWriter::full_gcode_comment, comment); + return w.string(); +} + +std::string BeltGCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &comment, bool force_z) +{ + // Belt-specific override of travel_to_xyz. + // Key differences from base: + // 1. All coordinates go through to_machine_coords() + // 2. Always emit full XYZ (can't split XY and Z due to coupling) + // 3. Lift type forced to NormalLift (handled by lazy_lift/eager_lift overrides) + + Vec3d dest_point = point; + auto travel_speed = + m_is_first_layer ? this->config.get_abs_value("initial_layer_travel_speed") : this->config.travel_speed.value; + + // Handle pending z_hop + if (std::abs(m_to_lift) > EPSILON) { + assert(std::abs(m_lifted) < EPSILON); + if ((!this->is_current_position_clear() || m_pos != dest_point) && + m_to_lift + m_pos(2) > point(2)) { + m_lifted = m_to_lift + m_pos(2) - point(2); + dest_point(2) = m_to_lift + m_pos(2); + } + m_to_lift = 0.; + + std::string slop_move; + Vec3d source = { m_pos(0) - m_x_offset, m_pos(1) - m_y_offset, m_pos(2) }; + Vec3d target = { dest_point(0) - m_x_offset, dest_point(1) - m_y_offset, dest_point(2) }; + Vec3d delta = target - source; + Vec2d delta_no_z = { delta(0), delta(1) }; + + if (delta(2) > 0 && delta_no_z.norm() != 0.0f) { + // Belt: SpiralLift and SlopeLift are disabled (lazy_lift forces NormalLift), + // but handle NormalLift and fallthrough. + if (m_to_lift_type == LiftType::SlopeLift && + this->is_current_position_clear() && + atan2(delta(2), delta_no_z.norm()) < this->filament()->travel_slope()) { + Vec2d temp = delta_no_z.normalized() * delta(2) / tan(this->filament()->travel_slope()); + Vec3d slope_top_point = Vec3d(temp(0), temp(1), delta(2)) + source; + slope_top_point = to_machine_coords(slope_top_point); + GCodeG1Formatter w0; + w0.emit_xyz(slope_top_point); + w0.emit_f(travel_speed * 60.0); + w0.emit_comment(GCodeWriter::full_gcode_comment, comment); + slop_move = w0.string(); + } + else if (m_to_lift_type == LiftType::NormalLift) { + slop_move = _travel_to_z(target.z(), "normal lift Z"); + } + } + + std::string xy_z_move; + { + Vec3d emit_target = to_machine_coords(target); + GCodeG1Formatter w0; + // Belt mode: always emit full XYZ since Y and Z are coupled + w0.emit_xyz(emit_target); + w0.emit_f(travel_speed * 60.0); + w0.emit_comment(GCodeWriter::full_gcode_comment, comment); + xy_z_move = w0.string(); + } + m_pos = dest_point; + this->set_current_position_clear(true); + return slop_move + xy_z_move; + } + else if (!force_z && !this->will_move_z(point(2))) { + double nominal_z = m_pos(2) - m_lifted; + m_lifted -= (point(2) - nominal_z); + if (std::abs(m_lifted) < EPSILON) + m_lifted = 0.; + this->set_current_position_clear(true); + return this->travel_to_xy(to_2d(point)); + } + else { + m_lifted = 0; + } + + Vec3d point_on_plate = { dest_point(0) - m_x_offset, dest_point(1) - m_y_offset, dest_point(2) }; + point_on_plate = to_machine_coords(point_on_plate); + + // Belt mode: always emit full XYZ + GCodeG1Formatter w; + w.emit_xyz(point_on_plate); + w.emit_f(this->config.travel_speed.value * 60.0); + w.emit_comment(GCodeWriter::full_gcode_comment, comment); + + m_pos = dest_point; + this->set_current_position_clear(true); + return w.string(); +} + +} // namespace Slic3r diff --git a/src/libslic3r/BeltGCodeWriter.hpp b/src/libslic3r/BeltGCodeWriter.hpp new file mode 100644 index 0000000000..727135b578 --- /dev/null +++ b/src/libslic3r/BeltGCodeWriter.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include "GCodeWriter.hpp" +#include "GCode/BeltBackTransform.hpp" + +namespace Slic3r { + +// Belt-printer-specific GCode writer. +// +// Inherits from GCodeWriter and overrides movement methods to apply +// coordinate transformation (back-transform, axis remap, origin snap) +// and emit coupled XYZ moves (Y and Z are coupled due to belt tilt). +class BeltGCodeWriter : public GCodeWriter +{ +public: + BeltGCodeWriter() : GCodeWriter() {} + + // Belt configuration + void set_belt_angle(double angle_deg); + bool is_belt_printer() const { return m_belt_angle_rad != 0.; } + void set_axis_remap(int rx, int ry, int rz); + void set_build_volume_max(const Vec3d &max); + void set_belt_back_transform(const PrintConfig &config); + void set_origin_snap(int axis, bool enable, double offset, double bbox_min); + Vec3d to_machine_coords(const Vec3d &pos) const; + + // Overridden movement methods + std::string travel_to_xy(const Vec2d &point, const std::string &comment = std::string()) override; + std::string travel_to_xyz(const Vec3d &point, const std::string &comment = std::string(), bool force_z = false) override; + std::string extrude_to_xy(const Vec2d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false) override; + std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false) override; + std::string lazy_lift(LiftType lift_type = LiftType::NormalLift, bool spiral_vase = false) override; + std::string eager_lift(const LiftType type) override; + +protected: + std::string _travel_to_z(double z, const std::string &comment) override; + +private: + double m_belt_angle_rad = 0.; + int m_remap_x = 0; + int m_remap_y = 1; + int m_remap_z = 2; + Vec3d m_build_vol_max = Vec3d::Zero(); + BeltBackTransform m_belt_back_transform; + bool m_origin_snap[3] = {false, false, false}; + double m_origin_offset[3] = {0., 0., 0.}; + double m_origin_bbox_min[3] = {0., 0., 0.}; +}; + +} // namespace Slic3r diff --git a/src/libslic3r/BeltSliceStrategy.cpp b/src/libslic3r/BeltSliceStrategy.cpp new file mode 100644 index 0000000000..b3adeec64a --- /dev/null +++ b/src/libslic3r/BeltSliceStrategy.cpp @@ -0,0 +1,62 @@ +#include "BeltSliceStrategy.hpp" + +#include + +namespace Slic3r { + +std::unique_ptr BeltSliceStrategy::create(const PrintConfig &config) +{ + if (!config.belt_printer.value) + return nullptr; + return std::unique_ptr(new BeltSliceStrategy(config)); +} + +BeltSliceStrategy::BeltSliceStrategy(const PrintConfig &config) +{ + m_has_remap = BeltTransformPipeline::has_preslice_remap(config); + if (m_has_remap) + m_pre_remap = BeltTransformPipeline::build_preslice_remap(config); + + m_shear = BeltTransformPipeline::build_shear_matrix(config, &m_has_shear); + m_scale = BeltTransformPipeline::build_scale_matrix(config, &m_has_scale); +} + +void BeltSliceStrategy::apply_to_trafo(Transform3d &trafo, + const ModelVolumePtrs &model_volumes, + double *out_belt_min_z) const +{ + // Step 1: Pre-slice axis remap. + if (m_has_remap) + trafo = m_pre_remap * trafo; + + // Step 2: Shear + scale. + if (m_has_shear || m_has_scale) { + Transform3d belt_xform = Transform3d::Identity(); + belt_xform.linear() = m_scale * m_shear; + trafo = belt_xform * trafo; + } + + // Step 3: Z-shift — detect if mesh clips below build plate after transforms. + if (m_has_remap || m_has_shear || m_has_scale) { + double min_z = std::numeric_limits::max(); + for (const ModelVolume *mv : model_volumes) { + if (!mv->is_model_part()) continue; + for (const stl_vertex &v : mv->mesh().its.vertices) { + Vec3d pt = trafo * v.cast(); + min_z = std::min(min_z, pt.z()); + } + } + double belt_z_shift_val = (min_z < 0. && min_z != std::numeric_limits::max()) ? -min_z : 0.; + BOOST_LOG_TRIVIAL(warning) << "Belt Z-shift: min_z=" << min_z + << " z_shift=" << belt_z_shift_val; + if (belt_z_shift_val > 0.) { + Transform3d z_shift = Transform3d::Identity(); + z_shift.matrix()(2, 3) = belt_z_shift_val; + trafo = z_shift * trafo; + } + if (out_belt_min_z) + *out_belt_min_z = (min_z != std::numeric_limits::max()) ? min_z : 0.; + } +} + +} // namespace Slic3r diff --git a/src/libslic3r/BeltSliceStrategy.hpp b/src/libslic3r/BeltSliceStrategy.hpp new file mode 100644 index 0000000000..bf19243fd2 --- /dev/null +++ b/src/libslic3r/BeltSliceStrategy.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include "libslic3r.h" +#include "Point.hpp" +#include "BeltTransform.hpp" +#include "PrintConfig.hpp" +#include "Model.hpp" + +#include +#include + +namespace Slic3r { + +// Belt printer pre-slice transform strategy. +// +// Encapsulates the pre-remap, shear, scale, and Z-shift transforms +// that are applied to model geometry before slicing on belt printers. +// Used by PrintObjectSlice.cpp to isolate belt-specific logic from +// the slicing pipeline. +class BeltSliceStrategy +{ +public: + // Create a strategy if belt_printer is enabled; returns nullptr otherwise. + static std::unique_ptr create(const PrintConfig &config); + + // Apply belt transforms to the slicing trafo. + // Modifies trafo in-place: trafo = z_shift * scale * shear * pre_remap * trafo + // Scans all model_part volumes to detect minimum Z and add z-shift if needed. + // Sets *out_belt_min_z to the minimum Z of the mesh after transforms. + void apply_to_trafo(Transform3d &trafo, + const ModelVolumePtrs &model_volumes, + double *out_belt_min_z) const; + +private: + explicit BeltSliceStrategy(const PrintConfig &config); + + bool m_has_remap = false; + bool m_has_shear = false; + bool m_has_scale = false; + Transform3d m_pre_remap = Transform3d::Identity(); + Matrix3d m_shear = Matrix3d::Identity(); + Matrix3d m_scale = Matrix3d::Identity(); +}; + +} // namespace Slic3r diff --git a/src/libslic3r/BeltTransform.cpp b/src/libslic3r/BeltTransform.cpp new file mode 100644 index 0000000000..db7eea7d80 --- /dev/null +++ b/src/libslic3r/BeltTransform.cpp @@ -0,0 +1,246 @@ +#include "BeltTransform.hpp" +#include "Model.hpp" + +#include + +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.belt_preslice_remap_x.value); + int pre_ry = int(config.belt_preslice_remap_y.value); + int pre_rz = int(config.belt_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_shear_matrix(const PrintConfig &config, bool *has_shear_out) +{ + struct AxisShear { BeltShearMode mode; double angle; int from; }; + AxisShear axes[3] = { + { config.belt_shear_x.value, config.belt_shear_x_angle.value, int(config.belt_shear_x_from.value) }, + { config.belt_shear_y.value, config.belt_shear_y_angle.value, int(config.belt_shear_y_from.value) }, + { config.belt_shear_z.value, config.belt_shear_z_angle.value, int(config.belt_shear_z_from.value) }, + }; + + Matrix3d shear = Matrix3d::Identity(); + bool active = false; + for (int row = 0; row < 3; ++row) { + if (axes[row].mode != BeltShearMode::None) { + double factor = compute_shear_factor(axes[row].mode, axes[row].angle); + if (std::abs(factor) > EPSILON) { + shear(row, axes[row].from) += factor; + active = true; + } + } + } + if (has_shear_out) *has_shear_out = active; + return shear; +} + +Matrix3d BeltTransformPipeline::build_scale_matrix(const PrintConfig &config, bool *has_scale_out) +{ + double sx = compute_scale_factor(config.belt_scale_x.value, config.belt_scale_x_angle.value); + double sy = compute_scale_factor(config.belt_scale_y.value, config.belt_scale_y_angle.value); + double sz = compute_scale_factor(config.belt_scale_z.value, config.belt_scale_z_angle.value); + + bool active = (std::abs(sx - 1.) > EPSILON || + std::abs(sy - 1.) > EPSILON || + std::abs(sz - 1.) > EPSILON); + + Matrix3d scale = Matrix3d::Identity(); + if (active) { + scale(0, 0) = sx; + scale(1, 1) = sy; + scale(2, 2) = sz; + } + if (has_scale_out) *has_scale_out = active; + return scale; +} + +Transform3d BeltTransformPipeline::build_forward_transform(const PrintConfig &config) +{ + Transform3d pre_remap = build_preslice_remap(config); + bool shear_active = false; + Matrix3d shear = build_shear_matrix(config, &shear_active); + bool scale_active = false; + Matrix3d scale = build_scale_matrix(config, &scale_active); + + // Pipeline: scale * shear * pre_remap + Transform3d combined = Transform3d::Identity(); + combined.linear() = scale * shear; + combined = combined * pre_remap; + return combined; +} + +// ---- Bounding box remap --------------------------------------------------- + +BoundingBoxf3 BeltTransformPipeline::remap_bbox(const BoundingBoxf3 &bb, const PrintConfig &config) +{ + int pre_rx = int(config.belt_preslice_remap_x.value); + int pre_ry = int(config.belt_preslice_remap_y.value); + int pre_rz = int(config.belt_preslice_remap_z.value); + + if (pre_rx == int(BeltRemapAxis::PosX) && + pre_ry == int(BeltRemapAxis::PosY) && + pre_rz == int(BeltRemapAxis::PosZ)) + return bb; // Identity remap. + + 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(), mx = bb.max.cast(); + 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 { + +template +BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl( + const Config &config, const BoundingBoxf3 &bb, double original_height) +{ + BeltTransformPipeline::BeltHeightResult result; + result.object_height = original_height; + + // Extract Z-axis shear/scale config. + BeltShearMode z_shear_mode; + double z_shear_angle; + BeltScaleMode z_scale_mode; + double z_scale_angle; + int z_shear_from; + + if constexpr (std::is_same_v) { + z_shear_mode = config.belt_shear_z.value; + z_shear_angle = config.belt_shear_z_angle.value; + z_scale_mode = config.belt_scale_z.value; + z_scale_angle = config.belt_scale_z_angle.value; + z_shear_from = int(config.belt_shear_z_from.value); + } else { + // DynamicPrintConfig path + auto get_shear = [&](const char *key) { + auto *opt = config.template option>(key); + return opt ? opt->value : BeltShearMode::None; + }; + auto get_scale = [&](const char *key) { + auto *opt = config.template option>(key); + return opt ? opt->value : BeltScaleMode::None; + }; + auto get_float = [&](const char *key) { + auto *opt = config.template option(key); + return opt ? opt->value : 45.0; + }; + auto get_axis = [&](const char *key) { + auto *opt = config.template option>(key); + return opt ? int(opt->value) : 1; + }; + z_shear_mode = get_shear("belt_shear_z"); + z_shear_angle = get_float("belt_shear_z_angle"); + z_scale_mode = get_scale("belt_scale_z"); + z_scale_angle = get_float("belt_scale_z_angle"); + z_shear_from = get_axis("belt_shear_z_from"); + } + + bool has_z_shear = z_shear_mode != BeltShearMode::None; + bool has_z_scale = z_scale_mode != BeltScaleMode::None; + + if (!has_z_shear && !has_z_scale) + return result; + + double shear_factor = has_z_shear + ? BeltTransformPipeline::compute_shear_factor(z_shear_mode, z_shear_angle) : 0.; + double scale_z = BeltTransformPipeline::compute_scale_factor(z_scale_mode, z_scale_angle); + + if (has_z_shear && std::abs(shear_factor) > EPSILON) { + int from = z_shear_from; + double min_rz = std::numeric_limits::max(); + double max_rz = std::numeric_limits::lowest(); + for (double vz : {bb.min.z(), bb.max.z()}) + for (double vs : {bb.min(from), bb.max(from)}) { + double new_z = scale_z * (vz + shear_factor * vs); + min_rz = std::min(min_rz, new_z); + max_rz = std::max(max_rz, new_z); + } + result.object_height = max_rz - min_rz; + result.floor_params.shear_factor = shear_factor; + result.floor_params.from_axis = from; + result.floor_params.z_shift = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.); + } else { + result.object_height = original_height * scale_z; + } + + return result; +} + +} // anonymous namespace + +BeltTransformPipeline::BeltHeightResult BeltTransformPipeline::compute_belt_height_and_floor( + const PrintConfig &config, const BoundingBoxf3 &remapped_bbox, double original_height) +{ + return compute_belt_height_and_floor_impl(config, remapped_bbox, original_height); +} + +BeltTransformPipeline::BeltHeightResult BeltTransformPipeline::compute_belt_height_and_floor( + const DynamicPrintConfig &config, const BoundingBoxf3 &remapped_bbox, double original_height) +{ + return compute_belt_height_and_floor_impl(config, remapped_bbox, original_height); +} + +} // namespace Slic3r diff --git a/src/libslic3r/BeltTransform.hpp b/src/libslic3r/BeltTransform.hpp new file mode 100644 index 0000000000..b9a60473e5 --- /dev/null +++ b/src/libslic3r/BeltTransform.hpp @@ -0,0 +1,146 @@ +#pragma once + +#include "libslic3r.h" +#include "Point.hpp" +#include "BoundingBox.hpp" +#include "PrintConfig.hpp" +#include "Geometry.hpp" + +#include + +namespace Slic3r { + +class ModelObject; + +// Shared belt-printer transform math. +// +// The pre-slice pipeline applied in PrintObjectSlice.cpp is: +// trafo_out = z_shift * scale * shear * pre_remap * trafo_in +// +// This class provides the building blocks so every call site uses the +// same implementation. z_shift is object-dependent (computed from mesh +// vertex bounds) and is NOT included in build_forward_transform(). +class BeltTransformPipeline +{ +public: + // ---- Pure math helpers ------------------------------------------------ + + static double compute_shear_factor(BeltShearMode mode, double angle_deg) + { + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad); + double cos_a = std::cos(angle_rad); + switch (mode) { + case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; + case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; + case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; + case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; + default: return 0.; + } + } + + static double compute_scale_factor(BeltScaleMode mode, double angle_deg) + { + if (mode == BeltScaleMode::None) return 1.; + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad); + double cos_a = std::cos(angle_rad); + switch (mode) { + case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; + case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; + case BeltScaleMode::Sin: return sin_a; + case BeltScaleMode::Cos: return cos_a; + default: return 1.; + } + } + + // ---- Identity checks -------------------------------------------------- + + static bool has_preslice_remap(const PrintConfig &config) + { + return int(config.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || + int(config.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || + int(config.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ); + } + + // Overload accepting DynamicPrintConfig (used in static slicing_parameters). + static bool has_preslice_remap(const DynamicPrintConfig &config) + { + auto get_int = [&](const char *key) -> int { + auto *opt = config.option>(key); + return opt ? int(opt->value) : 0; + }; + return get_int("belt_preslice_remap_x") != int(BeltRemapAxis::PosX) || + get_int("belt_preslice_remap_y") != int(BeltRemapAxis::PosY) || + get_int("belt_preslice_remap_z") != int(BeltRemapAxis::PosZ); + } + + static bool has_shear(const PrintConfig &config) + { + return config.belt_shear_x.value != BeltShearMode::None || + config.belt_shear_y.value != BeltShearMode::None || + config.belt_shear_z.value != BeltShearMode::None; + } + + static bool has_scale(const PrintConfig &config) + { + double sx = compute_scale_factor(config.belt_scale_x.value, config.belt_scale_x_angle.value); + double sy = compute_scale_factor(config.belt_scale_y.value, config.belt_scale_y_angle.value); + double sz = compute_scale_factor(config.belt_scale_z.value, config.belt_scale_z_angle.value); + return std::abs(sx - 1.) > EPSILON || + std::abs(sy - 1.) > EPSILON || + std::abs(sz - 1.) > EPSILON; + } + + // ---- Matrix builders -------------------------------------------------- + + // Build the pre-slice axis remap transform (includes Rev-mode translation). + static Transform3d build_preslice_remap(const PrintConfig &config); + + // Build the 3x3 shear matrix. Returns Identity if no shear is active. + // Also sets has_shear_out if non-null. + static Matrix3d build_shear_matrix(const PrintConfig &config, bool *has_shear_out = nullptr); + + // Build the 3x3 diagonal scale matrix. Returns Identity if no scale. + // Also sets has_scale_out if non-null. + static Matrix3d build_scale_matrix(const PrintConfig &config, bool *has_scale_out = nullptr); + + // Combined forward transform: scale * shear * pre_remap. + // 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 { + double shear_factor = 0.0; + int from_axis = 1; + double z_shift = 0.0; + }; + + // Result of computing belt height + floor params. + struct BeltHeightResult { + double object_height; // Effective object height after shear/scale + BeltFloorParams floor_params; + }; + + // Compute effective object height and belt floor parameters from config + // and pre-remapped 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, + 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, + double original_height); +}; + +} // namespace Slic3r diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index c2d92ee800..a3e813b1da 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -79,6 +79,14 @@ set(lisbslic3r_sources BoundingBox.hpp BridgeDetector.cpp BridgeDetector.hpp + BeltGCode.cpp + BeltGCode.hpp + BeltGCodeWriter.cpp + BeltGCodeWriter.hpp + BeltSliceStrategy.cpp + BeltSliceStrategy.hpp + BeltTransform.cpp + BeltTransform.hpp Brim.cpp BrimEarsPoint.hpp Brim.hpp @@ -417,6 +425,8 @@ set(lisbslic3r_sources SlicingAdaptive.hpp Slicing.cpp Slicing.hpp + Support/BeltFloorContext.cpp + Support/BeltFloorContext.hpp Support/SupportCommon.cpp Support/SupportCommon.hpp Support/SupportLayer.hpp diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 8391f9b820..c8b6e540cf 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -8,6 +8,7 @@ #include "Exception.hpp" #include "ExtrusionEntity.hpp" #include "EdgeGrid.hpp" +#include "BeltTransform.hpp" #include "Geometry.hpp" #include "Geometry/ConvexHull.hpp" #include "GCode/PrintExtents.hpp" @@ -795,7 +796,7 @@ static std::vector get_path_of_change_filament(const Print& print) //BBS: if needed, write the gcode_label_objects_end then priming tower, if the retract, didn't did it. std::string object_end_label_temp; - gcodegen.m_writer.add_object_end_labels(object_end_label_temp); + gcodegen.m_writer->add_object_end_labels(object_end_label_temp); // Process the custom change_filament_gcode. If it is empty, provide a simple Tn command to change the filament. // Otherwise, leave control to the user completely. @@ -839,7 +840,7 @@ static std::vector get_path_of_change_filament(const Print& print) // config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); // BBS { - GCodeWriter& gcode_writer = gcodegen.m_writer; + GCodeWriter& gcode_writer = *gcodegen.m_writer; FullPrintConfig& full_config = gcodegen.m_config; // set volumetric speed of outer wall ,ignore per obejct,just use default setting @@ -1573,8 +1574,8 @@ static std::vector get_path_of_change_filament(const Print& print) const std::vector ColorPrintColors::Colors = { "#C0392B", "#E67E22", "#F1C40F", "#27AE60", "#1ABC9C", "#2980B9", "#9B59B6" }; -#define EXTRUDER_CONFIG(OPT) m_config.OPT.get_at(m_writer.filament()->extruder_id()) -#define FILAMENT_CONFIG(OPT) m_config.OPT.get_at(m_writer.filament()->id()) +#define EXTRUDER_CONFIG(OPT) m_config.OPT.get_at(m_writer->filament()->extruder_id()) +#define FILAMENT_CONFIG(OPT) m_config.OPT.get_at(m_writer->filament()->id()) void GCode::PlaceholderParserIntegration::reset() { @@ -1687,7 +1688,7 @@ void GCode::PlaceholderParserIntegration::validate_output_vector_variables() // Collect pairs of object_layer + support_layer sorted by print_z. // object_layer & support_layer are considered to be on the same print_z, if they are not further than EPSILON. -std::vector GCode::collect_layers_to_print(const PrintObject& object) +std::vector GCode::collect_layers_to_print(const PrintObject& object, bool skip_empty_first_layer) { std::vector layers_to_print; layers_to_print.reserve(object.layers().size() + object.support_layers().size()); @@ -1753,8 +1754,7 @@ std::vector GCode::collect_layers_to_print(const PrintObjec // an error. In global shear mode the object may also start above // Z=0 on the tilted belt surface. if (layers_to_print.size() == 1u) { - bool skip_empty_check = object.print()->config().belt_printer.value; - if (!has_extrusions && !skip_empty_check) + if (!has_extrusions && !skip_empty_first_layer) throw Slic3r::SlicingError(_(L("One object has an empty first layer and can't be printed. Please Cut the bottom or enable supports.")), object.id().id); } @@ -1827,7 +1827,7 @@ std::vector>> GCode::collec for (size_t i = 0; i < print.objects().size(); ++i) { try { - per_object[i] = collect_layers_to_print(*print.objects()[i]); + per_object[i] = collect_layers_to_print(*print.objects()[i], print.config().belt_printer.value); } catch (const Slic3r::SlicingError &e) { errors.push_back(e); continue; @@ -2016,7 +2016,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu BOOST_LOG_TRIVIAL(info) << boost::format("Will export G-code to %1% soon")%path; GCodeProcessor::s_IsBBLPrinter = print->is_BBL_printer(); - m_writer.set_is_bbl_machine(print->is_BBL_printer()); + m_writer->set_is_bbl_machine(print->is_BBL_printer()); print->set_started(psGCodeExport); // check if any custom gcode contains keywords used by the gcode processor to @@ -2113,7 +2113,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu m_processor.result().support_traditional_timelapse = m_support_traditional_timelapse; bool activate_long_retraction_when_cut = false; - for (const auto& filament : m_writer.extruders()) + for (const auto& filament : m_writer->extruders()) activate_long_retraction_when_cut |= ( m_config.long_retractions_when_cut.get_at(filament.id()) && m_config.retraction_distances_when_cut.get_at(filament.id()) > 0 @@ -2144,7 +2144,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu m_processor.finalize(true); // DoExport::update_print_estimated_times_stats(m_processor, print->m_print_statistics); - DoExport::update_print_estimated_stats(m_processor, m_writer.extruders(), print->m_print_statistics, print->config()); + DoExport::update_print_estimated_stats(m_processor, m_writer->extruders(), print->m_print_statistics, print->config()); if (result != nullptr) { *result = std::move(m_processor.extract_result()); // set the filename to the correct value @@ -2404,7 +2404,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato PROFILE_FUNC(); m_print = &print; - m_timelapse_pos_picker.init(&print,m_writer.get_xy_offset().cast()); + m_timelapse_pos_picker.init(&print,m_writer->get_xy_offset().cast()); // modifies m_silent_time_estimator_enabled DoExport::init_gcode_processor(print.config(), m_processor, m_silent_time_estimator_enabled); @@ -2420,29 +2420,10 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_fan_mover.release(); - m_writer.set_is_bbl_machine(is_bbl_printers); + m_writer->set_is_bbl_machine(is_bbl_printers); - // Belt printer: initialize coordinate transformation and axis remap on the writer. - if (print.config().belt_printer.value) { - m_writer.set_belt_angle(print.config().belt_printer_angle.value); - m_writer.set_axis_remap( - int(print.config().belt_gcode_remap_x.value), - int(print.config().belt_gcode_remap_y.value), - int(print.config().belt_gcode_remap_z.value)); - // Build volume extents for Rev remap mode. - BoundingBoxf bbox_bed(print.config().printable_area.values); - m_writer.set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(), print.config().printable_height.value)); - // Initialize the back-transform that undoes slicing shear/scale. - m_writer.set_belt_back_transform(print.config()); - // Per-axis origin snap: store config; actual snap is computed - // per-instance in update_origin_snap() called from set_origin(). - m_origin_snap[0] = print.config().belt_origin_snap_x.value; - m_origin_snap[1] = print.config().belt_origin_snap_y.value; - m_origin_snap[2] = print.config().belt_origin_snap_z.value; - m_origin_snap_offset[0] = print.config().belt_origin_offset_x.value; - m_origin_snap_offset[1] = print.config().belt_origin_offset_y.value; - m_origin_snap_offset[2] = print.config().belt_origin_offset_z.value; - } + // Belt printer: initialize belt-specific writer via virtual hook. + this->init_belt_writer(print, is_bbl_printers); // How many times will be change_layer() called? // change_layer() in turn increments the progress bar status. @@ -2533,31 +2514,8 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.write_format("; HEADER_BLOCK_START\n"); // Write information on the generator. file.write_format("; generated by %s on %s\n", Slic3r::header_slic3r_generated().c_str(), Slic3r::Utils::local_timestamp().c_str()); - // Belt printer: embed angle and transform configs in header for G-code processor detection. - if (print.config().belt_printer.value) { - file.write_format("; belt_printer_angle = %.1f\n", print.config().belt_printer_angle.value); - // Shear configs - const auto &full_cfg = print.full_print_config(); - file.write_format("; belt_shear_x = %s\n", full_cfg.opt_serialize("belt_shear_x").c_str()); - file.write_format("; belt_shear_x_angle = %.1f\n", print.config().belt_shear_x_angle.value); - file.write_format("; belt_shear_x_from = %s\n", full_cfg.opt_serialize("belt_shear_x_from").c_str()); - file.write_format("; belt_shear_y = %s\n", full_cfg.opt_serialize("belt_shear_y").c_str()); - file.write_format("; belt_shear_y_angle = %.1f\n", print.config().belt_shear_y_angle.value); - file.write_format("; belt_shear_y_from = %s\n", full_cfg.opt_serialize("belt_shear_y_from").c_str()); - file.write_format("; belt_shear_z = %s\n", full_cfg.opt_serialize("belt_shear_z").c_str()); - file.write_format("; belt_shear_z_angle = %.1f\n", print.config().belt_shear_z_angle.value); - file.write_format("; belt_shear_z_from = %s\n", full_cfg.opt_serialize("belt_shear_z_from").c_str()); - // Scale configs - file.write_format("; belt_scale_x = %s\n", full_cfg.opt_serialize("belt_scale_x").c_str()); - file.write_format("; belt_scale_x_angle = %.1f\n", print.config().belt_scale_x_angle.value); - file.write_format("; belt_scale_y = %s\n", full_cfg.opt_serialize("belt_scale_y").c_str()); - file.write_format("; belt_scale_y_angle = %.1f\n", print.config().belt_scale_y_angle.value); - file.write_format("; belt_scale_z = %s\n", full_cfg.opt_serialize("belt_scale_z").c_str()); - file.write_format("; belt_scale_z_angle = %.1f\n", print.config().belt_scale_z_angle.value); - file.write_format("; belt_preslice_remap_x = %s\n", full_cfg.opt_serialize("belt_preslice_remap_x").c_str()); - file.write_format("; belt_preslice_remap_y = %s\n", full_cfg.opt_serialize("belt_preslice_remap_y").c_str()); - file.write_format("; belt_preslice_remap_z = %s\n", full_cfg.opt_serialize("belt_preslice_remap_z").c_str()); - } + // Belt printer: embed angle and transform configs in header via virtual hook. + this->write_belt_header(file, print); if (is_bbl_printers) file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str()); //BBS: total layer number @@ -2804,13 +2762,13 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Disable fan. if (m_config.auxiliary_fan.value && print.config().close_fan_the_first_x_layers.get_at(initial_extruder_id)) { - file.write(m_writer.set_fan(0)); + file.write(m_writer->set_fan(0)); //BBS: disable additional fan - file.write(m_writer.set_additional_fan(0)); + file.write(m_writer->set_additional_fan(0)); } // Update output variables after the extruders were initialized. - m_placeholder_parser_integration.init(m_writer); + m_placeholder_parser_integration.init(*m_writer); // Let the start-up script prime the 1st printing tool. @@ -2883,7 +2841,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato { BoundingBoxf bbox_bed(print.config().printable_area.values); - Vec2f plate_offset = m_writer.get_xy_offset(); + Vec2f plate_offset = m_writer->get_xy_offset(); this->placeholder_parser().set("print_bed_min", new ConfigOptionFloats({ bbox_bed.min.x(), bbox_bed.min.y()})); this->placeholder_parser().set("print_bed_max", new ConfigOptionFloats({ bbox_bed.max.x(), bbox_bed.max.y()})); this->placeholder_parser().set("print_bed_size", new ConfigOptionFloats({ bbox_bed.size().x(), bbox_bed.size().y() })); @@ -2985,7 +2943,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } bool activate_chamber_temp_control = false; auto max_chamber_temp = 0; - for (const auto &extruder : m_writer.extruders()) { + for (const auto &extruder : m_writer->extruders()) { activate_chamber_temp_control |= m_config.activate_chamber_temp_control.get_at(extruder.id()); max_chamber_temp = std::max(max_chamber_temp, m_config.chamber_temperature.get_at(extruder.id())); } @@ -2993,7 +2951,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato BedType curr_bed_type = m_config.curr_bed_type; int min_temperature_vitrification = std::numeric_limits::max(); - for (const auto& extruder : m_writer.extruders()) + for (const auto& extruder : m_writer->extruders()) min_temperature_vitrification = std::min(min_temperature_vitrification, m_config.temperature_vitrification.get_at(extruder.id())); @@ -3107,18 +3065,18 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato if (activate_chamber_temp_control && max_chamber_temp > 0){ int temp_out =0; if(!custom_gcode_sets_temperature(machine_start_gcode,141,191,false,temp_out)) - file.write(m_writer.set_chamber_temperature(max_chamber_temp, true)); // set chamber_temperature + file.write(m_writer->set_chamber_temperature(max_chamber_temp, true)); // set chamber_temperature } // Write the custom start G-code file.writeln(machine_start_gcode); //BBS: gcode writer doesn't know where the real position of extruder is after inserting custom gcode - m_writer.set_current_position_clear(false); + m_writer->set_current_position_clear(false); m_start_gcode_filament = GCodeProcessor::get_gcode_last_filament(machine_start_gcode); if (is_bbl_printers) { - m_writer.init_extruder(initial_non_support_extruder_id); + m_writer->init_extruder(initial_non_support_extruder_id); // add the missing filament start gcode in machine start gcode { DynamicConfig config; @@ -3131,7 +3089,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } // Orca: add missing PA settings for initial filament if (m_config.enable_pressure_advance.get_at(initial_non_support_extruder_id)) { - file.write(m_writer.set_pressure_advance(m_config.pressure_advance.get_at(initial_non_support_extruder_id))); + file.write(m_writer->set_pressure_advance(m_config.pressure_advance.get_at(initial_non_support_extruder_id))); // Orca: Adaptive PA // Reset Adaptive PA processor last PA value m_pa_processor->resetPreviousPA(m_config.pressure_advance.get_at(initial_non_support_extruder_id)); @@ -3157,14 +3115,14 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Orca: when activate_air_filtration is set on any extruder, find and set the highest during_print_exhaust_fan_speed bool activate_air_filtration = false; int during_print_exhaust_fan_speed = 0; - for (const auto &extruder : m_writer.extruders()) { + for (const auto &extruder : m_writer->extruders()) { activate_air_filtration |= m_config.activate_air_filtration.get_at(extruder.id()); if (m_config.activate_air_filtration.get_at(extruder.id())) during_print_exhaust_fan_speed = std::max(during_print_exhaust_fan_speed, m_config.during_print_exhaust_fan_speed.get_at(extruder.id())); } if (activate_air_filtration) - file.write(m_writer.set_exhaust_fan(during_print_exhaust_fan_speed, true)); + file.write(m_writer->set_exhaust_fan(during_print_exhaust_fan_speed, true)); print.throw_if_canceled(); @@ -3180,7 +3138,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_seam_placer.init(print, throw_if_canceled_func); // BBS: get path for change filament - if (m_writer.multiple_extruders) { + if (m_writer->multiple_extruders) { std::vector points = get_path_of_change_filament(print); if (points.size() == 3) { travel_point_1 = points[0]; @@ -3212,12 +3170,12 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato std::string gcode; gcode += ";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Layer_Change) + "\n"; if ((print.default_object_config().outer_wall_acceleration.value > 0 && print.default_object_config().outer_wall_acceleration.value > 0)) { - gcode += m_writer.set_print_acceleration((unsigned int)floor(print.default_object_config().outer_wall_acceleration.value + 0.5)); + gcode += m_writer->set_print_acceleration((unsigned int)floor(print.default_object_config().outer_wall_acceleration.value + 0.5)); } if (print.default_object_config().outer_wall_jerk.value > 0) { double jerk = print.default_object_config().outer_wall_jerk.value; - gcode += m_writer.set_jerk_xy(jerk); + gcode += m_writer->set_jerk_xy(jerk); } auto params = print.calib_params(); @@ -3261,8 +3219,8 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } print.throw_if_canceled(); this->set_origin(unscale((*print_object_instance_sequential_active)->shift)); - this->update_origin_snap((*print_object_instance_sequential_active)->print_object, - (*print_object_instance_sequential_active)->shift); + this->on_set_origin((*print_object_instance_sequential_active)->print_object, + (*print_object_instance_sequential_active)->shift); // BBS: prime extruder if extruder change happens before this object instance bool prime_extruder = false; @@ -3272,7 +3230,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_enable_cooling_markers = false; // we're not filtering these moves through CoolingBuffer m_avoid_crossing_perimeters.use_external_mp_once(); // BBS. change tool before moving to origin point. - if (m_writer.need_toolchange(initial_extruder_id)) { + if (m_writer->need_toolchange(initial_extruder_id)) { const PrintObjectConfig& object_config = object.config(); coordf_t initial_layer_print_height = print.config().initial_layer_print_height.value; @@ -3285,7 +3243,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } else { file.write(this->retract()); } - file.write(m_writer.travel_to_z(m_max_layer_z + m_writer.config.z_hop.get_at(initial_extruder_id))); + file.write(m_writer->travel_to_z(m_max_layer_z + m_writer->config.z_hop.get_at(initial_extruder_id))); file.write(this->travel_to(Point(0, 0), erNone, "move to origin position for next object")); m_enable_cooling_markers = true; // Disable motion planner when traveling to first object point. @@ -3308,7 +3266,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // and export G-code into file. tool_ordering.cal_most_used_extruder(print.config()); m_printed_objects.emplace_back(&object); - this->process_layers(print, tool_ordering, collect_layers_to_print(object), *print_object_instance_sequential_active - object.instances().data(), file, + this->process_layers(print, tool_ordering, collect_layers_to_print(object, print.config().belt_printer.value), *print_object_instance_sequential_active - object.instances().data(), file, prime_extruder); { // save the flush statitics stored in tool ordering by object @@ -3325,7 +3283,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato { const auto plr_mode = print.config().enable_power_loss_recovery.value; if (m_second_layer_things_done && plr_mode == PowerLossRecoveryMode::Enable) { - file.write(m_writer.enable_power_loss_recovery(PowerLossRecoveryMode::Disable)); + file.write(m_writer->enable_power_loss_recovery(PowerLossRecoveryMode::Disable)); } } ++ finished_objects; @@ -3346,7 +3304,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_wipe_tower->set_wipe_tower_bbx(print.get_wipe_tower_bbx()); m_wipe_tower->set_rib_offset(print.get_rib_offset()); //BBS - file.write(m_writer.travel_to_z(initial_layer_print_height + m_config.z_offset.value, "Move to the first layer height")); + file.write(m_writer->travel_to_z(initial_layer_print_height + m_config.z_offset.value, "Move to the first layer height")); if (wipe_tower_type == WipeTowerType::Type2 && print.config().single_extruder_multi_material_priming) { file.write(m_wipe_tower->prime(*this)); @@ -3405,7 +3363,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Orca: disable power loss recovery if (m_second_layer_things_done && print.config().enable_power_loss_recovery.value == PowerLossRecoveryMode::Enable) { - file.write(m_writer.enable_power_loss_recovery(PowerLossRecoveryMode::Disable)); + file.write(m_writer->enable_power_loss_recovery(PowerLossRecoveryMode::Disable)); } if (m_wipe_tower) // Purge the extruder, pull out the active filament. @@ -3419,14 +3377,14 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // if needed, write the gcode_label_objects_end { std::string gcode; - m_writer.add_object_change_labels(gcode); + m_writer->add_object_change_labels(gcode); file.write(gcode); } - file.write(m_writer.set_fan(0)); + file.write(m_writer->set_fan(0)); //BBS: make sure the additional fan is closed when end if(m_config.auxiliary_fan.value) - file.write(m_writer.set_additional_fan(0)); + file.write(m_writer->set_additional_fan(0)); if (is_bbl_printers) { //BBS: close spaghetti detector //Note: M981 is also used to tell xcam the last layer is finished, so we need always send it even if spaghetti option is disabled. @@ -3442,12 +3400,12 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato DynamicConfig config; config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index)); //BBS - config.set_key_value("layer_z", new ConfigOptionFloat(m_writer.get_position()(2) - m_config.z_offset.value)); + config.set_key_value("layer_z", new ConfigOptionFloat(m_writer->get_position()(2) - m_config.z_offset.value)); config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); if (print.config().single_extruder_multi_material) { // Process the filament_end_gcode for the active filament only. - int extruder_id = m_writer.filament()->id(); + int extruder_id = m_writer->filament()->id(); config.set_key_value("filament_extruder_id", new ConfigOptionInt(extruder_id)); file.writeln(this->placeholder_parser_process("filament_end_gcode", print.config().filament_end_gcode.get_at(extruder_id), extruder_id, &config)); } else { @@ -3457,20 +3415,20 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.writeln(this->placeholder_parser_process("filament_end_gcode", end_gcode, extruder_id, &config)); } } - file.writeln(this->placeholder_parser_process("machine_end_gcode", print.config().machine_end_gcode, m_writer.filament()->id(), &config)); + file.writeln(this->placeholder_parser_process("machine_end_gcode", print.config().machine_end_gcode, m_writer->filament()->id(), &config)); } - file.write(m_writer.update_progress(m_layer_count, m_layer_count, true)); // 100% - file.write(m_writer.postamble()); + file.write(m_writer->update_progress(m_layer_count, m_layer_count, true)); // 100% + file.write(m_writer->postamble()); if (activate_chamber_temp_control && max_chamber_temp > 0) - file.write(m_writer.set_chamber_temperature(0, false)); //close chamber_temperature + file.write(m_writer->set_chamber_temperature(0, false)); //close chamber_temperature if (activate_air_filtration) { int complete_print_exhaust_fan_speed = 0; - for (const auto& extruder : m_writer.extruders()) + for (const auto& extruder : m_writer->extruders()) if (m_config.activate_air_filtration.get_at(extruder.id())) complete_print_exhaust_fan_speed = std::max(complete_print_exhaust_fan_speed, m_config.complete_print_exhaust_fan_speed.get_at(extruder.id())); - file.write(m_writer.set_exhaust_fan(complete_print_exhaust_fan_speed, true)); + file.write(m_writer->set_exhaust_fan(complete_print_exhaust_fan_speed, true)); } // adds tags for time estimators file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Last_Line_M73_Placeholder).c_str()); @@ -3482,7 +3440,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.write(DoExport::update_print_stats_and_format_filament_stats( // Const inputs has_wipe_tower, print.wipe_tower_data(), - m_writer.extruders(), + m_writer->extruders(), // Modifies print.m_print_statistics)); print.m_print_statistics.initial_tool = initial_extruder_id; @@ -3656,7 +3614,7 @@ void GCode::process_layers( ); const auto fan_mover = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, - [&fan_mover = this->m_fan_mover, &config = this->config(), &writer = this->m_writer](std::string in)->std::string { + [&fan_mover = this->m_fan_mover, &config = this->config(), &writer = *this->m_writer](std::string in)->std::string { CNumericLocalesSetter locales_setter; @@ -3756,7 +3714,7 @@ void GCode::process_layers( ); const auto fan_mover = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, - [&fan_mover = this->m_fan_mover, &config = this->config(), &writer = this->m_writer](std::string in)->std::string { + [&fan_mover = this->m_fan_mover, &config = this->config(), &writer = *this->m_writer](std::string in)->std::string { if (config.fan_speedup_time.value != 0 || config.fan_kickstart.value > 0) { if (fan_mover.get() == nullptr) @@ -3824,21 +3782,21 @@ std::string GCode::placeholder_parser_process(const std::string &name, const std PlaceholderParserIntegration &ppi = m_placeholder_parser_integration; try { - ppi.update_from_gcodewriter(m_writer); + ppi.update_from_gcodewriter(*m_writer); std::string output = ppi.parser.process(templ, current_filament_id, config_override, &ppi.output_config, &ppi.context); ppi.validate_output_vector_variables(); if (const std::vector &pos = ppi.opt_position->values; ppi.position != pos) { // Update G-code writer. - m_writer.set_position({ pos[0], pos[1], pos[2] }); + m_writer->set_position({ pos[0], pos[1], pos[2] }); this->set_last_pos(this->gcode_to_point({ pos[0], pos[1] })); } - for (const Extruder &e : m_writer.extruders()) { + for (const Extruder &e : m_writer->extruders()) { unsigned int eid = e.id(); assert(eid < ppi.num_extruders); if ( eid < ppi.num_extruders) { - if (! m_writer.config.use_relative_e_distances && ! is_approx(ppi.e_position[eid], ppi.opt_e_position->values[eid])) + if (! m_writer->config.use_relative_e_distances && ! is_approx(ppi.e_position[eid], ppi.opt_e_position->values[eid])) const_cast(e).set_position(ppi.opt_e_position->values[eid]); if (! is_approx(ppi.e_retracted[eid], ppi.opt_e_retracted->values[eid]) || ! is_approx(ppi.e_restart_extra[eid], ppi.opt_e_restart_extra->values[eid])) @@ -3963,9 +3921,9 @@ void GCode::_print_first_layer_bed_temperature(GCodeOutputStream &file, Print &p temp = temp_by_gcode; #endif - // Always call m_writer.set_bed_temperature() so it will set the internal "current" state of the bed temp as if + // Always call m_writer->set_bed_temperature() so it will set the internal "current" state of the bed temp as if // the custom start G-code emited these. - std::string set_temp_gcode = m_writer.set_bed_temperature(bed_temp, wait); + std::string set_temp_gcode = m_writer->set_bed_temperature(bed_temp, wait); if (! temp_set_by_gcode) file.write(set_temp_gcode); } @@ -3985,14 +3943,14 @@ void GCode::_print_first_layer_extruder_temperatures(GCodeOutputStream &file, Pr int temp = print.config().nozzle_temperature_initial_layer.get_at(first_printing_extruder_id); if (temp_by_gcode >= 0 && temp_by_gcode < 1000) temp = temp_by_gcode; - m_writer.set_temperature(temp, wait, first_printing_extruder_id); + m_writer->set_temperature(temp, wait, first_printing_extruder_id); } else { // Custom G-code does not set the extruder temperature. Do it now. if (print.config().single_extruder_multi_material.value) { // Set temperature of the first printing extruder only. int temp = print.config().nozzle_temperature_initial_layer.get_at(first_printing_extruder_id); if (temp > 0) - file.write(m_writer.set_temperature(temp, wait, first_printing_extruder_id)); + file.write(m_writer->set_temperature(temp, wait, first_printing_extruder_id)); } else { // Set temperatures of all the printing extruders. for (unsigned int tool_id : print.extruders()) { @@ -4004,7 +3962,7 @@ void GCode::_print_first_layer_extruder_temperatures(GCodeOutputStream &file, Pr temp = print.config().idle_temperature.get_at(tool_id); } if (temp > 0) - file.write(m_writer.set_temperature(temp, wait, tool_id)); + file.write(m_writer->set_temperature(temp, wait, tool_id)); } } } @@ -4427,7 +4385,7 @@ LayerResult GCode::process_layer( //support is attached above the object, and support layers has independent layer height, then the lowest support //interface layer id is 0. bool first_layer = (layer.id() == 0 && abs(layer.bottom_z()) < EPSILON); - m_writer.set_is_first_layer(first_layer); + m_writer->set_is_first_layer(first_layer); unsigned int first_extruder_id = layer_tools.extruders.front(); // Initialize config with the 1st object to be printed at this layer. @@ -4477,7 +4435,7 @@ LayerResult GCode::process_layer( config.set_key_value("layer_z", new ConfigOptionFloat(print_z)); config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); gcode += this->placeholder_parser_process("before_layer_change_gcode", - print.config().before_layer_change_gcode.value, m_writer.filament()->id(), &config) + print.config().before_layer_change_gcode.value, m_writer->filament()->id(), &config) + "\n"; } @@ -4512,7 +4470,7 @@ LayerResult GCode::process_layer( config.set_key_value("layer_z", new ConfigOptionFloat(print_z)); config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); gcode += this->placeholder_parser_process("timelapse_gcode", - print.config().time_lapse_gcode.value, m_writer.filament()->id(), &config) + print.config().time_lapse_gcode.value, m_writer->filament()->id(), &config) + "\n"; } @@ -4522,7 +4480,7 @@ LayerResult GCode::process_layer( config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index)); config.set_key_value("layer_z", new ConfigOptionFloat(print_z)); gcode += this->placeholder_parser_process("layer_change_gcode", - print.config().layer_change_gcode.value, m_writer.filament()->id(), &config) + print.config().layer_change_gcode.value, m_writer->filament()->id(), &config) + "\n"; config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); } @@ -4562,7 +4520,7 @@ LayerResult GCode::process_layer( case CalibMode::Calib_Input_shaping_freq: { if (m_layer_index == 1){ gcode += writer().set_input_shaping('A', print.calib_params().start, 0.f, print.calib_params().shaper_type); - if (m_writer.get_gcode_flavor() == gcfKlipper) { + if (m_writer->get_gcode_flavor() == gcfKlipper) { // Disable minimum cruise ratio to ensure consistent motion for calibration gcode += "SET_VELOCITY_LIMIT MINIMUM_CRUISE_RATIO=0\n"; } @@ -4578,7 +4536,7 @@ LayerResult GCode::process_layer( } case CalibMode::Calib_Input_shaping_damp: { if (m_layer_index == 1){ - if (m_writer.get_gcode_flavor() == gcfKlipper) { + if (m_writer->get_gcode_flavor() == gcfKlipper) { // Disable minimum cruise ratio to ensure consistent motion for calibration gcode += "SET_VELOCITY_LIMIT MINIMUM_CRUISE_RATIO=0\n"; } @@ -4590,7 +4548,7 @@ LayerResult GCode::process_layer( break; } case CalibMode::Calib_Cornering: { - if (m_writer.get_gcode_flavor() == gcfMarlinFirmware && + if (m_writer->get_gcode_flavor() == gcfMarlinFirmware && !m_config.machine_max_junction_deviation.values.empty() && m_config.machine_max_junction_deviation.values.front() > 0) { gcode += writer().set_junction_deviation(this->interpolate_value_across_layers(print.calib_params().start, print.calib_params().end)); @@ -4605,22 +4563,22 @@ LayerResult GCode::process_layer( if (first_layer) { // Orca: we don't need to optimize the Klipper as only set once if (m_config.default_acceleration.value > 0 && m_config.initial_layer_acceleration.value > 0) { - gcode += m_writer.set_print_acceleration((unsigned int)floor(m_config.initial_layer_acceleration.value + 0.5)); + gcode += m_writer->set_print_acceleration((unsigned int)floor(m_config.initial_layer_acceleration.value + 0.5)); } if (m_config.default_jerk.value > 0 && m_config.initial_layer_jerk.value > 0) { - gcode += m_writer.set_jerk_xy(m_config.initial_layer_jerk.value); + gcode += m_writer->set_jerk_xy(m_config.initial_layer_jerk.value); } - if (m_writer.get_gcode_flavor() == gcfMarlinFirmware && m_config.default_junction_deviation.value > 0) { - gcode += m_writer.set_junction_deviation(m_config.default_junction_deviation.value); + if (m_writer->get_gcode_flavor() == gcfMarlinFirmware && m_config.default_junction_deviation.value > 0) { + gcode += m_writer->set_junction_deviation(m_config.default_junction_deviation.value); } } if (!first_layer && !m_second_layer_things_done) { // Orca: set power loss recovery const auto plr_mode = print.config().enable_power_loss_recovery.value; - gcode += m_writer.enable_power_loss_recovery(plr_mode); + gcode += m_writer->enable_power_loss_recovery(plr_mode); if (print.is_BBL_printer()) { // BBS: open first layer inspection at second layer @@ -4635,23 +4593,23 @@ LayerResult GCode::process_layer( // Reset acceleration at sencond layer // Orca: only set once, don't need to call set_accel_and_jerk if (m_config.default_acceleration.value > 0 && m_config.initial_layer_acceleration.value > 0) { - gcode += m_writer.set_print_acceleration((unsigned int) floor(m_config.default_acceleration.value + 0.5)); + gcode += m_writer->set_print_acceleration((unsigned int) floor(m_config.default_acceleration.value + 0.5)); } if (m_config.default_jerk.value > 0 && m_config.initial_layer_jerk.value > 0) { - gcode += m_writer.set_jerk_xy(m_config.default_jerk.value); + gcode += m_writer->set_jerk_xy(m_config.default_jerk.value); } // Transition from 1st to 2nd layer. Adjust nozzle temperatures as prescribed by the nozzle dependent // nozzle_temperature_initial_layer vs. temperature settings. - for (const Extruder& extruder : m_writer.extruders()) { + for (const Extruder& extruder : m_writer->extruders()) { if ((print.config().single_extruder_multi_material.value || m_ooze_prevention.enable) && - extruder.id() != m_writer.filament()->id()) + extruder.id() != m_writer->filament()->id()) // In single extruder multi material mode, set the temperature for the current extruder only. continue; int temperature = print.config().nozzle_temperature.get_at(extruder.id()); if (temperature > 0 && temperature != print.config().nozzle_temperature_initial_layer.get_at(extruder.id())) - gcode += m_writer.set_temperature(temperature, false, extruder.id()); + gcode += m_writer->set_temperature(temperature, false, extruder.id()); } // BBS @@ -4660,14 +4618,14 @@ LayerResult GCode::process_layer( bed_temp = get_highest_bed_temperature(false,print); else bed_temp = get_bed_temperature(first_extruder_id, false, m_config.curr_bed_type); - gcode += m_writer.set_bed_temperature(bed_temp); + gcode += m_writer->set_bed_temperature(bed_temp); // Mark the temperature transition from 1st to 2nd layer to be finished. m_second_layer_things_done = true; } if (single_object_instance_idx == size_t(-1)) { // Normal (non-sequential) print. - gcode += ProcessLayer::emit_custom_gcode_per_print_z(*this, layer_tools.custom_gcode, m_writer.filament()->id(), first_extruder_id, print.config()); + gcode += ProcessLayer::emit_custom_gcode_per_print_z(*this, layer_tools.custom_gcode, m_writer->filament()->id(), first_extruder_id, print.config()); } // BBS: get next extruder according to flush and soluble @@ -4958,9 +4916,9 @@ LayerResult GCode::process_layer( auto insert_timelapse_gcode = [this, print_z, &print, &most_used_extruder, &layer_object_label_ids,&printed_objects = std::as_const(m_printed_objects)]() -> std::string { PosPickCtx ctx; - ctx.curr_pos = { (coord_t)(scale_(m_writer.get_position().x())),(coord_t)(scale_(m_writer.get_position().y())) }; + ctx.curr_pos = { (coord_t)(scale_(m_writer->get_position().x())),(coord_t)(scale_(m_writer->get_position().y())) }; ctx.curr_layer = this->layer(); - ctx.curr_extruder_id = m_writer.filament()->extruder_id(); + ctx.curr_extruder_id = m_writer->filament()->extruder_id(); ctx.picture_extruder_id = most_used_extruder; if (m_config.print_sequence == PrintSequence::ByObject) ctx.printed_objects = printed_objects; @@ -4978,17 +4936,17 @@ LayerResult GCode::process_layer( config.set_key_value("timelapse_pos_x", new ConfigOptionInt((int)timelapse_pos.x())); config.set_key_value("timelapse_pos_y", new ConfigOptionInt((int)timelapse_pos.y())); config.set_key_value("has_timelapse_safe_pos", new ConfigOptionBool(timelapse_pos != DefaultTimelapsePos)); - timelapse_gcode = this->placeholder_parser_process("timelapse_gcode", print.config().time_lapse_gcode.value, m_writer.filament()->id(), &config) + "\n"; + timelapse_gcode = this->placeholder_parser_process("timelapse_gcode", print.config().time_lapse_gcode.value, m_writer->filament()->id(), &config) + "\n"; } if (!timelapse_gcode.empty()) { - m_writer.set_current_position_clear(false); + m_writer->set_current_position_clear(false); double temp_z_after_tool_change; if (GCodeProcessor::get_last_z_from_gcode(timelapse_gcode, temp_z_after_tool_change)) { - Vec3d pos = m_writer.get_position(); + Vec3d pos = m_writer->get_position(); pos(2) = temp_z_after_tool_change; - m_writer.set_position(pos); + m_writer->set_position(pos); } } @@ -5034,16 +4992,16 @@ LayerResult GCode::process_layer( config.set_key_value("layer_z", new ConfigOptionFloat(print_z)); config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); config.set_key_value("most_used_physical_extruder_id", new ConfigOptionInt(m_config.physical_extruder_map.get_at(most_used_extruder))); - config.set_key_value("curr_physical_extruder_id", new ConfigOptionInt(m_config.physical_extruder_map.get_at(m_writer.filament()->extruder_id()))); - wrapping_gcode = this->placeholder_parser_process("wrapping_detection_gcode", print.config().wrapping_detection_gcode.value, m_writer.filament()->id(), &config) +"\n"; + config.set_key_value("curr_physical_extruder_id", new ConfigOptionInt(m_config.physical_extruder_map.get_at(m_writer->filament()->extruder_id()))); + wrapping_gcode = this->placeholder_parser_process("wrapping_detection_gcode", print.config().wrapping_detection_gcode.value, m_writer->filament()->id(), &config) +"\n"; } - m_writer.set_current_position_clear(false); + m_writer->set_current_position_clear(false); double temp_z_after_tool_change; if (GCodeProcessor::get_last_z_from_gcode(wrapping_gcode, temp_z_after_tool_change)) { - Vec3d pos = m_writer.get_position(); + Vec3d pos = m_writer->get_position(); pos(2) = temp_z_after_tool_change; - m_writer.set_position(pos); + m_writer->set_position(pos); } return wrapping_gcode; }; @@ -5076,7 +5034,7 @@ LayerResult GCode::process_layer( if (should_insert) { gcode += this->retract(false, false, auto_lift_type, true); - m_writer.add_object_change_labels(gcode); + m_writer->add_object_change_labels(gcode); gcode += insert_timelapse_gcode(); has_insert_timelapse_gcode = true; @@ -5093,12 +5051,12 @@ LayerResult GCode::process_layer( } else { if (need_insert_timelapse_gcode_for_traditional && !has_insert_timelapse_gcode && - m_writer.need_toolchange(extruder_id) && + m_writer->need_toolchange(extruder_id) && m_config.nozzle_diameter.values.size() == 2 && writer().filament() && (get_extruder_id(writer().filament()->id()) == most_used_extruder)) { gcode += this->retract(false, false, auto_lift_type, true); - m_writer.add_object_change_labels(gcode); + m_writer->add_object_change_labels(gcode); gcode += insert_timelapse_gcode(); has_insert_timelapse_gcode = true; @@ -5197,19 +5155,19 @@ LayerResult GCode::process_layer( // exclude objects if (m_enable_exclude_object) { if (is_BBL_Printer()) { - m_writer.set_object_start_str( + m_writer->set_object_start_str( std::string("; start printing object, unique label id: ") + std::to_string(instance_to_print.label_object_id) + "\n" + "M624 " + _encode_label_ids_to_base64({instance_to_print.label_object_id}) + "\n"); } else { const auto gflavor = print.config().gcode_flavor.value; if (gflavor == gcfKlipper) { - m_writer.set_object_start_str(std::string("EXCLUDE_OBJECT_START NAME=") + + m_writer->set_object_start_str(std::string("EXCLUDE_OBJECT_START NAME=") + get_instance_name(&instance_to_print.print_object, inst.id) + "\n"); } else if (gflavor == gcfMarlinLegacy || gflavor == gcfMarlinFirmware || gflavor == gcfRepRapFirmware) { std::string str = std::string("M486 S") + std::to_string(inst.unique_id) + "\n"; - m_writer.set_object_start_str(str); + m_writer->set_object_start_str(str); } } } @@ -5227,7 +5185,7 @@ LayerResult GCode::process_layer( m_avoid_crossing_perimeters.use_external_mp_once(); m_last_obj_copy = this_object_copy; this->set_origin(unscale(offset)); - this->update_origin_snap(&instance_to_print.print_object, offset); + this->on_set_origin(&instance_to_print.print_object, offset); if (instance_to_print.object_by_extruder.support != nullptr) { m_layer = layers[instance_to_print.layer_id].support_layer; m_object_layer_over_raft = false; @@ -5251,7 +5209,7 @@ LayerResult GCode::process_layer( m_avoid_crossing_perimeters.use_external_mp_once(); m_last_obj_copy = this_object_copy; this->set_origin(unscale(offset)); - this->update_origin_snap(&instance_to_print.print_object, offset); + this->on_set_origin(&instance_to_print.print_object, offset); ExtrusionEntityCollection support_eec; // BBS @@ -5300,7 +5258,7 @@ LayerResult GCode::process_layer( m_avoid_crossing_perimeters.use_external_mp_once(); m_last_obj_copy = this_object_copy; this->set_origin(unscale(offset)); - this->update_origin_snap(&instance_to_print.print_object, offset); + this->on_set_origin(&instance_to_print.print_object, offset); //FIXME the following code prints regions in the order they are defined, the path is not optimized in any way. auto has_infill = [](const std::vector &by_region) { @@ -5337,20 +5295,20 @@ LayerResult GCode::process_layer( } // exclude objects // Don't set m_gcode_label_objects_end if you don't had to write the m_gcode_label_objects_start. - if (!m_writer.is_object_start_str_empty()) { - m_writer.set_object_start_str(""); + if (!m_writer->is_object_start_str_empty()) { + m_writer->set_object_start_str(""); } else if (m_enable_exclude_object) { if (is_BBL_Printer()) { - m_writer.set_object_end_str(std::string("; stop printing object, unique label id: ") + + m_writer->set_object_end_str(std::string("; stop printing object, unique label id: ") + std::to_string(instance_to_print.label_object_id) + "\n" + "M625\n"); } else { const auto gflavor = print.config().gcode_flavor.value; if (gflavor == gcfKlipper) { - m_writer.set_object_end_str(std::string("EXCLUDE_OBJECT_END NAME=") + + m_writer->set_object_end_str(std::string("EXCLUDE_OBJECT_END NAME=") + get_instance_name(&instance_to_print.print_object, inst.id) + "\n"); } else if (gflavor == gcfMarlinLegacy || gflavor == gcfMarlinFirmware || gflavor == gcfRepRapFirmware) { - m_writer.set_object_end_str(std::string("M486 S-1\n")); + m_writer->set_object_end_str(std::string("M486 S-1\n")); } } } @@ -5402,7 +5360,7 @@ LayerResult GCode::process_layer( if (FILAMENT_CONFIG(retract_when_changing_layer)) { gcode += this->retract(false, false, auto_lift_type, true); } - m_writer.add_object_change_labels(gcode); + m_writer->add_object_change_labels(gcode); gcode += insert_timelapse_gcode(); } @@ -5414,7 +5372,7 @@ LayerResult GCode::process_layer( void GCode::apply_print_config(const PrintConfig &print_config) { - m_writer.apply_print_config(print_config); + m_writer->apply_print_config(print_config); m_config.apply(print_config); m_scaled_resolution = scaled(print_config.resolution.value); m_enable_exclude_object = m_config.exclude_object; @@ -5504,7 +5462,7 @@ void GCode::append_full_config(const Print &print, std::string &str) void GCode::set_extruders(const std::vector &extruder_ids) { - m_writer.set_extruders(extruder_ids); + m_writer->set_extruders(extruder_ids); // enable wipe path generation if any extruder has wipe enabled m_wipe.enable = false; @@ -5527,139 +5485,15 @@ void GCode::set_origin(const Vec2d &pointf) m_origin = pointf; } -void GCode::update_origin_snap(const PrintObject *obj, const Point &inst_shift) -{ - if (!m_origin_snap[0] && !m_origin_snap[1] && !m_origin_snap[2]) - return; - - // Clear existing snap so to_machine_coords gives raw machine coords for bbox computation. - for (int a = 0; a < 3; ++a) - m_writer.set_origin_snap(a, false, 0., 0.); - - // Reconstruct the belt pipeline transform for this object (same as - // PrintObjectSlice.cpp: z_shift * scale * shear * pre_remap). - const auto &cfg = m_config; - Transform3d belt = Transform3d::Identity(); - - // Pre-slice remap - int pre_rx = int(cfg.belt_preslice_remap_x.value); - int pre_ry = int(cfg.belt_preslice_remap_y.value); - int pre_rz = int(cfg.belt_preslice_remap_z.value); - if (pre_rx != int(BeltRemapAxis::PosX) || - pre_ry != int(BeltRemapAxis::PosY) || - pre_rz != int(BeltRemapAxis::PosZ)) { - auto remap_col = [](int r) -> Vec3d { - int a = r % 3; Vec3d c = Vec3d::Zero(); - c[a] = (r < 3) ? 1.0 : -1.0; - return c; - }; - Matrix3d lin; - lin.col(0) = remap_col(pre_rx); - lin.col(1) = remap_col(pre_ry); - lin.col(2) = remap_col(pre_rz); - Transform3d pre = Transform3d::Identity(); - pre.linear() = lin; - if (pre_rx >= 6 || pre_ry >= 6 || pre_rz >= 6) { - BoundingBoxf bb(cfg.printable_area.values); - Vec3d vm(bb.max.x(), bb.max.y(), cfg.printable_height.value); - Vec3d tr = Vec3d::Zero(); - if (pre_rx >= 6) tr[0] = vm[pre_rx % 3]; - if (pre_ry >= 6) tr[1] = vm[pre_ry % 3]; - if (pre_rz >= 6) tr[2] = vm[pre_rz % 3]; - pre.translation() = tr; - } - belt = pre * belt; - } - - // Shear - auto shear_f = [](BeltShearMode m, double a) -> double { - double r = Geometry::deg2rad(a), s = std::sin(r), c = std::cos(r); - switch (m) { - case BeltShearMode::PosCot: return (s > EPSILON) ? c/s : 0.; - case BeltShearMode::NegCot: return (s > EPSILON) ? -c/s : 0.; - case BeltShearMode::PosTan: return (c > EPSILON) ? s/c : 0.; - case BeltShearMode::NegTan: return (c > EPSILON) ? -s/c : 0.; - default: return 0.; - } - }; - struct AS { BeltShearMode m; double a; int f; }; - AS axes[3] = { - {cfg.belt_shear_x.value, cfg.belt_shear_x_angle.value, int(cfg.belt_shear_x_from.value)}, - {cfg.belt_shear_y.value, cfg.belt_shear_y_angle.value, int(cfg.belt_shear_y_from.value)}, - {cfg.belt_shear_z.value, cfg.belt_shear_z_angle.value, int(cfg.belt_shear_z_from.value)}, - }; - Transform3d shear = Transform3d::Identity(); - for (int i = 0; i < 3; ++i) - if (axes[i].m != BeltShearMode::None) { - double f = shear_f(axes[i].m, axes[i].a); - if (std::abs(f) > EPSILON) - shear.matrix()(i, axes[i].f) += f; - } - - // Scale - auto scale_f = [](BeltScaleMode m, double a) -> double { - if (m == BeltScaleMode::None) return 1.; - double r = Geometry::deg2rad(a), s = std::sin(r), c = std::cos(r); - switch (m) { - case BeltScaleMode::InvSin: return (s > EPSILON) ? 1./s : 1.; - case BeltScaleMode::InvCos: return (c > EPSILON) ? 1./c : 1.; - case BeltScaleMode::Sin: return s; - case BeltScaleMode::Cos: return c; - default: return 1.; - } - }; - Transform3d sc = Transform3d::Identity(); - sc.matrix()(0,0) = scale_f(cfg.belt_scale_x.value, cfg.belt_scale_x_angle.value); - sc.matrix()(1,1) = scale_f(cfg.belt_scale_y.value, cfg.belt_scale_y_angle.value); - sc.matrix()(2,2) = scale_f(cfg.belt_scale_z.value, cfg.belt_scale_z_angle.value); - - belt = sc * shear * belt; - - // Z-shift - double zs = (obj->belt_min_z() < 0.) ? -obj->belt_min_z() : 0.; - if (zs > 0.) { - Transform3d zsh = Transform3d::Identity(); - zsh.matrix()(2, 3) = zs; - belt = zsh * belt; - } - - // Full transform: belt * trafo_centered - Transform3d full = belt * obj->trafo_centered(); - - // Instance shift in slicer space + global Z offset - Vec3d shift(unscale(inst_shift.x()), - unscale(inst_shift.y()), - obj->belt_global_z_offset()); - - // Compute this instance's machine-space bbox min - BoundingBoxf3 bb = obj->model_object()->raw_bounding_box(); - Vec3d mn = bb.min.cast(), mx = bb.max.cast(); - Vec3d inst_min(std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max()); - 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 mc = m_writer.to_machine_coords(full * c + shift); - for (int a = 0; a < 3; ++a) - inst_min[a] = std::min(inst_min[a], mc[a]); - } - - // Update writer snap for each enabled axis - for (int a = 0; a < 3; ++a) - m_writer.set_origin_snap(a, m_origin_snap[a], m_origin_snap_offset[a], inst_min[a]); -} - std::string GCode::preamble() { - std::string gcode = m_writer.preamble(); + std::string gcode = m_writer->preamble(); /* Perform a *silent* move to z_offset: we need this to initialize the Z position of our writer object so that any initial lift taking place before the first layer change will raise the extruder from the correct initial Z instead of 0. */ - m_writer.travel_to_z(m_config.z_offset.value); + m_writer->travel_to_z(m_config.z_offset.value); return gcode; } @@ -5670,28 +5504,28 @@ std::string GCode::change_layer(coordf_t print_z) std::string gcode; if (m_layer_count > 0) // Increment a progress bar indicator. - gcode += m_writer.update_progress(++ m_layer_index, m_layer_count); + gcode += m_writer->update_progress(++ m_layer_index, m_layer_count); //BBS coordf_t z = print_z + m_config.z_offset.value; // in unscaled coordinates - if (FILAMENT_CONFIG(retract_when_changing_layer) && m_writer.will_move_z(z)) { + if (FILAMENT_CONFIG(retract_when_changing_layer) && m_writer->will_move_z(z)) { LiftType lift_type = this->to_lift_type(ZHopType(FILAMENT_CONFIG(z_hop_types))); //BBS: force to use SpiralLift when change layer if lift type is auto gcode += this->retract(false, false, ZHopType(FILAMENT_CONFIG(z_hop_types)) == ZHopType::zhtAuto ? LiftType::SpiralLift : lift_type); } - m_writer.add_object_change_labels(gcode); + m_writer->add_object_change_labels(gcode); if (m_spiral_vase) { //BBS: force to normal lift immediately in spiral vase mode std::ostringstream comment; comment << "move to next layer (" << m_layer_index << ")"; - gcode += m_writer.travel_to_z(z, comment.str()); + gcode += m_writer->travel_to_z(z, comment.str()); } m_need_change_layer_lift_z = true; m_nominal_z = z; - m_writer.get_position().z() = z; + m_writer->get_position().z() = z; // forget last wiping path as wiping after raising Z is pointless // BBS. Dont forget wiping path to reduce stringing. @@ -5990,7 +5824,7 @@ std::string GCode::extrude_loop(ExtrusionLoop loop, std::string description, dou //Point pt = ((nd * nd >= l2) ? (p1+v*0.4): (p1 + 0.2 * v * (nd / sqrt(l2)))).cast(); pt.rotate(angle, paths.front().polyline.points.front()); // generate the travel move - gcode += m_writer.extrude_to_xy(this->point_to_gcode(pt), 0,"move inwards before travel",true); + gcode += m_writer->extrude_to_xy(this->point_to_gcode(pt), 0,"move inwards before travel",true); } return gcode; @@ -6313,7 +6147,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, bool slope_need_z_travel = false; if (sloped != nullptr && !sloped->is_flat()) { auto target_z = get_sloped_z(sloped->slope_begin.z_ratio); - slope_need_z_travel = m_writer.will_move_z(target_z); + slope_need_z_travel = m_writer->will_move_z(target_z); } // Move to first point of extrusion path // path is 2D. But in slope lift case, lift z is done in travel_to function. @@ -6336,7 +6170,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // if needed, write the gcode_label_objects_end then gcode_label_objects_start // should be already done by travel_to, but just in case - m_writer.add_object_change_labels(gcode); + m_writer->add_object_change_labels(gcode); // compensate retraction gcode += this->unretract(); @@ -6390,12 +6224,12 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } } - if (m_writer.get_gcode_flavor() == gcfKlipper) { - gcode += m_writer.set_accel_and_jerk(acceleration_i, jerk); + if (m_writer->get_gcode_flavor() == gcfKlipper) { + gcode += m_writer->set_accel_and_jerk(acceleration_i, jerk); } else { - gcode += m_writer.set_print_acceleration(acceleration_i); - gcode += m_writer.set_jerk_xy(jerk); + gcode += m_writer->set_print_acceleration(acceleration_i); + gcode += m_writer->set_jerk_xy(jerk); } // calculate effective extrusion length per distance unit (e_per_mm) @@ -6440,8 +6274,8 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } // Effective extrusion length per distance unit = (filament_flow_ratio/cross_section) * mm3_per_mm / print flow ratio - // m_writer.extruder()->e_per_mm3() below is (filament flow ratio / cross-sectional area) - double e_per_mm = m_writer.filament()->e_per_mm3() * _mm3_per_mm; + // m_writer->extruder()->e_per_mm3() below is (filament flow ratio / cross-sectional area) + double e_per_mm = m_writer->filament()->e_per_mm3() * _mm3_per_mm; e_per_mm /= filament_flow_ratio; // set speed @@ -6667,7 +6501,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index + 1)); config.set_key_value("layer_z", new ConfigOptionFloat(m_layer == nullptr ? m_last_height : m_layer->print_z)); gcode += this->placeholder_parser_process("change_extrusion_role_gcode", - m_config.change_extrusion_role_gcode.value, m_writer.filament()->id(), &config) + m_config.change_extrusion_role_gcode.value, m_writer->filament()->id(), &config) + "\n"; } @@ -6722,7 +6556,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, if (m_multi_flow_segment_path_average_mm3_per_mm > 0) { sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer.filament()->id(), + m_writer->filament()->id(), m_multi_flow_segment_path_average_mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -6735,7 +6569,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // to issue a zero flow PA change command for this sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer.filament()->id(), + m_writer->filament()->id(), _mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -6841,7 +6675,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer.filament()->id(), + m_writer->filament()->id(), _mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -6856,7 +6690,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer.filament()->id(), + m_writer->filament()->id(), _mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -6871,7 +6705,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // ORCA: End of adaptive PA code segment } - gcode += m_writer.set_speed(F, "", comment); + gcode += m_writer->set_speed(F, "", comment); { if (m_enable_cooling_markers) { if (enable_overhang_bridge_fan) { @@ -6889,7 +6723,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } // BBS: use G1 if not enable arc fitting or has no arc fitting result or in spiral_mode mode or we are doing sloped extrusion // Attention: G2 and G3 is not supported in spiral_mode mode - if (!m_config.enable_arc_fitting || path.polyline.fitting_result.empty() || m_config.spiral_mode || sloped != nullptr || m_config.belt_printer) { + if (!m_config.enable_arc_fitting || path.polyline.fitting_result.empty() || m_config.spiral_mode || sloped != nullptr || this->should_disable_arc_fitting()) { double path_length = 0.; double total_length = sloped == nullptr ? 0. : path.polyline.length() * SCALING_FACTOR; for (const Line& line : path.polyline.lines()) { @@ -6909,7 +6743,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } if (sloped == nullptr) { // Normal extrusion - gcode += m_writer.extrude_to_xy( + gcode += m_writer->extrude_to_xy( this->point_to_gcode(line.b), dE, GCodeWriter::full_gcode_comment ? tempDescription : "", path.is_force_no_extrusion()); @@ -6918,7 +6752,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, const auto [z_ratio, e_ratio] = sloped->interpolate(path_length / total_length); Vec2d dest2d = this->point_to_gcode(line.b); Vec3d dest3d(dest2d(0), dest2d(1), get_sloped_z(z_ratio)); - gcode += m_writer.extrude_to_xyz( + gcode += m_writer->extrude_to_xyz( dest3d, dE * e_ratio, GCodeWriter::full_gcode_comment ? tempDescription : "", path.is_force_no_extrusion()); @@ -6948,7 +6782,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, tempDescription += Slic3r::format(" | Old Flow Value: %0.5f Length: %0.5f",oldE, line_length); } } - gcode += m_writer.extrude_to_xy( + gcode += m_writer->extrude_to_xy( this->point_to_gcode(line.b), dE, GCodeWriter::full_gcode_comment ? tempDescription : "", path.is_force_no_extrusion()); @@ -6971,7 +6805,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, tempDescription += Slic3r::format(" | Old Flow Value: %0.5f Length: %0.5f",oldE, arc_length); } } - gcode += m_writer.extrude_arc_to_xy( + gcode += m_writer->extrude_arc_to_xy( this->point_to_gcode(arc.end_point), center_offset, dE, @@ -6999,7 +6833,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, Polyline l(p); total_length = l.length() * SCALING_FACTOR; } - gcode += m_writer.set_speed(last_set_speed, "", comment); + gcode += m_writer->set_speed(last_set_speed, "", comment); Vec2d prev = this->point_to_gcode_quantized(new_points[0].p); bool pre_fan_enabled = false; bool cur_fan_enabled = false; @@ -7049,7 +6883,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer.filament()->id(), + m_writer->filament()->id(), _mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -7064,7 +6898,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer.filament()->id(), + m_writer->filament()->id(), _mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -7081,10 +6915,10 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // Ignore small speed variations - emit speed change if the delta between current and new is greater than 60mm/min / 1mm/sec // Reset speed to F if delta to F is less than 1mm/sec if ((std::abs(last_set_speed - new_speed) > 60)) { - gcode += m_writer.set_speed(new_speed, "", comment); + gcode += m_writer->set_speed(new_speed, "", comment); last_set_speed = new_speed; } else if ((std::abs(F - new_speed) <= 60)) { - gcode += m_writer.set_speed(F, "", comment); + gcode += m_writer->set_speed(F, "", comment); last_set_speed = F; } auto dE = e_per_mm * line_length; @@ -7098,12 +6932,12 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } if (sloped == nullptr) { // Normal extrusion - gcode += m_writer.extrude_to_xy(p, dE, GCodeWriter::full_gcode_comment ? tempDescription : ""); + gcode += m_writer->extrude_to_xy(p, dE, GCodeWriter::full_gcode_comment ? tempDescription : ""); } else { // Sloped extrusion const auto [z_ratio, e_ratio] = sloped->interpolate(path_length / total_length); Vec3d dest3d(p(0), p(1), get_sloped_z(z_ratio)); - gcode += m_writer.extrude_to_xyz(dest3d, dE * e_ratio, GCodeWriter::full_gcode_comment ? tempDescription : ""); + gcode += m_writer->extrude_to_xyz(dest3d, dE * e_ratio, GCodeWriter::full_gcode_comment ? tempDescription : ""); } prev = p; @@ -7244,18 +7078,18 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string } } - if (m_writer.get_gcode_flavor() == gcfKlipper) { - gcode += m_writer.set_accel_and_jerk(acceleration_to_set, jerk_to_set); + if (m_writer->get_gcode_flavor() == gcfKlipper) { + gcode += m_writer->set_accel_and_jerk(acceleration_to_set, jerk_to_set); } else { - gcode += m_writer.set_travel_acceleration(acceleration_to_set); - gcode += m_writer.set_jerk_xy(jerk_to_set); + gcode += m_writer->set_travel_acceleration(acceleration_to_set); + gcode += m_writer->set_jerk_xy(jerk_to_set); } // if a retraction would be needed, try to use reduce_crossing_wall to plan a // multi-hop travel path inside the configuration space if (m_config.reduce_crossing_wall && !m_avoid_crossing_perimeters.disabled_once() - && m_writer.is_current_position_clear()) + && m_writer->is_current_position_clear()) //BBS: don't generate detour travel paths when current position is unclea { travel = m_avoid_crossing_perimeters.travel_to(*this, point, &could_be_wipe_disabled); @@ -7302,7 +7136,7 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string // if needed, write the gcode_label_objects_end then gcode_label_objects_start - m_writer.add_object_change_labels(gcode); + m_writer->add_object_change_labels(gcode); // use G1 because we rely on paths being straight (G0 may make round paths) if (travel.size() >= 2) { @@ -7310,14 +7144,14 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string if (false/*m_spiral_vase*/) { // No lazy z lift for spiral vase mode for (size_t i = 1; i < travel.size(); ++i) { - gcode += m_writer.travel_to_xy(this->point_to_gcode(travel.points[i]), comment); + gcode += m_writer->travel_to_xy(this->point_to_gcode(travel.points[i]), comment); } } else { if (travel.size() == 2) { // No extra movements emitted by avoid_crossing_perimeters, simply move to the end point with z change const auto& dest2d = this->point_to_gcode(travel.points.back()); Vec3d dest3d(dest2d(0), dest2d(1), z == DBL_MAX ? m_nominal_z : z); - gcode += m_writer.travel_to_xyz(dest3d, comment, m_need_change_layer_lift_z); + gcode += m_writer->travel_to_xyz(dest3d, comment, m_need_change_layer_lift_z); m_need_change_layer_lift_z = false; } else { // Extra movements emitted by avoid_crossing_perimeters, lift the z to normal height at the beginning, then apply the z @@ -7327,16 +7161,16 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string // Lift to normal z at beginning Vec2d dest2d = this->point_to_gcode(travel.points[i]); Vec3d dest3d(dest2d(0), dest2d(1), m_nominal_z); - gcode += m_writer.travel_to_xyz(dest3d, comment, m_need_change_layer_lift_z); + gcode += m_writer->travel_to_xyz(dest3d, comment, m_need_change_layer_lift_z); m_need_change_layer_lift_z = false; } else if (z != DBL_MAX && i == travel.size() - 1) { // Apply z_ratio for the very last point Vec2d dest2d = this->point_to_gcode(travel.points[i]); Vec3d dest3d(dest2d(0), dest2d(1), z); - gcode += m_writer.travel_to_xyz(dest3d, comment); + gcode += m_writer->travel_to_xyz(dest3d, comment); } else { // For all points in between, no z change - gcode += m_writer.travel_to_xy(this->point_to_gcode(travel.points[i]), comment); + gcode += m_writer->travel_to_xy(this->point_to_gcode(travel.points[i]), comment); } } } @@ -7439,7 +7273,7 @@ bool GCode::needs_retraction(const Polyline &travel, ExtrusionRole role, LiftTyp if (clipped_travel.length() > travel_len_thresh) clipped_travel.points.back() = clipped_travel.points.front()+(clipped_travel.points.back() - clipped_travel.points.front()) * (travel_len_thresh / clipped_travel.length()); //BBS: translate to current plate coordinate system - clipped_travel.translate(Point::new_scale(double(m_origin.x() - m_writer.get_xy_offset().x()), double(m_origin.y() - m_writer.get_xy_offset().y()))); + clipped_travel.translate(Point::new_scale(double(m_origin.x() - m_writer->get_xy_offset().x()), double(m_origin.y() - m_writer->get_xy_offset().y()))); //BBS: force to retract when leave from external perimeter for a long travel //Better way is judging whether the travel move direction is same with last extrusion move. @@ -7491,13 +7325,13 @@ std::string GCode::retract(bool toolchange, bool is_last_retraction, LiftType li { std::string gcode; - if (m_writer.filament() == nullptr) + if (m_writer->filament() == nullptr) return gcode; // wipe (if it's enabled for this extruder and we have a stored wipe path and no-zero wipe distance) if (FILAMENT_CONFIG(wipe) && m_wipe.has_path() && scale_(FILAMENT_CONFIG(wipe_distance)) > SCALED_EPSILON) { Wipe::RetractionValues wipeRetractions = m_wipe.calculateWipeRetractionLengths(*this, toolchange); - gcode += toolchange ? m_writer.retract_for_toolchange(true,wipeRetractions.retractLengthBeforeWipe) : m_writer.retract(true, wipeRetractions.retractLengthBeforeWipe); + gcode += toolchange ? m_writer->retract_for_toolchange(true,wipeRetractions.retractLengthBeforeWipe) : m_writer->retract(true, wipeRetractions.retractLengthBeforeWipe); gcode += m_wipe.wipe(*this,wipeRetractions.retractLengthDuringWipe, toolchange, is_last_retraction); } @@ -7507,14 +7341,14 @@ std::string GCode::retract(bool toolchange, bool is_last_retraction, LiftType li length is honored in case wipe path was too short. */ if ((!this->on_first_layer() || this->config().bottom_surface_pattern != InfillPattern::ipHilbertCurve) && (role != erTopSolidInfill || this->config().top_surface_pattern != InfillPattern::ipHilbertCurve)) - gcode += toolchange ? m_writer.retract_for_toolchange() : m_writer.retract(); + gcode += toolchange ? m_writer->retract_for_toolchange() : m_writer->retract(); - gcode += m_writer.reset_e(); + gcode += m_writer->reset_e(); // Orca: check if should + can lift (roughly from SuperSlicer) RetractLiftEnforceType retract_lift_type = RetractLiftEnforceType(EXTRUDER_CONFIG(retract_lift_enforce)); bool needs_lift = toolchange - || m_writer.filament()->retraction_length() > 0 + || m_writer->filament()->retraction_length() > 0 || m_config.use_firmware_retraction; bool last_fill_extrusion_role_top_infill = (this->m_last_notgapfill_extrusion_role == ExtrusionRole::erTopSolidInfill || this->m_last_notgapfill_extrusion_role == ExtrusionRole::erIroning); @@ -7537,9 +7371,9 @@ std::string GCode::retract(bool toolchange, bool is_last_retraction, LiftType li if (needs_lift && can_lift) { if (apply_instantly) - gcode += m_writer.eager_lift(lift_type); + gcode += m_writer->eager_lift(lift_type); else - gcode += m_writer.lazy_lift(lift_type, m_spiral_vase != nullptr); + gcode += m_writer->lazy_lift(lift_type, m_spiral_vase != nullptr); } return gcode; @@ -7548,11 +7382,11 @@ std::string GCode::retract(bool toolchange, bool is_last_retraction, LiftType li std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bool by_object, int toolchange_temp_override) { int new_extruder_id = get_extruder_id(new_filament_id); - if (!m_writer.need_toolchange(new_filament_id)) + if (!m_writer->need_toolchange(new_filament_id)) return ""; // if we are running a single-extruder setup, just set the extruder and return nothing - if (!m_writer.multiple_extruders) { + if (!m_writer->multiple_extruders) { this->placeholder_parser().set("current_extruder", new_filament_id); this->placeholder_parser().set("retraction_distance_when_ec", m_config.retraction_distances_when_ec.get_at(new_filament_id)); this->placeholder_parser().set("long_retraction_when_ec", m_config.long_retractions_when_ec.get_at(new_filament_id)); @@ -7575,13 +7409,13 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo check_add_eol(gcode); } if (m_config.enable_pressure_advance.get_at(new_filament_id)) { - gcode += m_writer.set_pressure_advance(m_config.pressure_advance.get_at(new_filament_id)); + gcode += m_writer->set_pressure_advance(m_config.pressure_advance.get_at(new_filament_id)); // Orca: Adaptive PA // Reset Adaptive PA processor last PA value m_pa_processor->resetPreviousPA(m_config.pressure_advance.get_at(new_filament_id)); } - gcode += m_writer.toolchange(new_filament_id); + gcode += m_writer->toolchange(new_filament_id); return gcode; } @@ -7596,18 +7430,18 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo // BBS: insert skip object label before change filament while by object if (by_object) - m_writer.add_object_change_labels(gcode); + m_writer->add_object_change_labels(gcode); bool add_change_filament_624 = false; - if (m_writer.filament() != nullptr) { + if (m_writer->filament() != nullptr) { // Process the custom filament_end_gcode. set_extruder() is only called if there is no wipe tower // so it should not be injected twice. - unsigned int old_filament_id = m_writer.filament()->id(); + unsigned int old_filament_id = m_writer->filament()->id(); const std::string &filament_end_gcode = m_config.filament_end_gcode.get_at(old_filament_id); if (! filament_end_gcode.empty()) { DynamicConfig config; config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index)); - config.set_key_value("layer_z", new ConfigOptionFloat(m_writer.get_position().z() - m_config.z_offset.value)); + config.set_key_value("layer_z", new ConfigOptionFloat(m_writer->get_position().z() - m_config.z_offset.value)); config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); config.set_key_value("filament_extruder_id", new ConfigOptionInt(int(get_extruder_id(old_filament_id)))); if (!m_filament_instances_code.empty()) { @@ -7623,7 +7457,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo // If ooze prevention is enabled, park current extruder in the nearest // standby point and set it to the standby temperature. - if (m_ooze_prevention.enable && m_writer.filament() != nullptr) + if (m_ooze_prevention.enable && m_writer->filament() != nullptr) gcode += m_ooze_prevention.pre_toolchange(*this); // BBS @@ -7636,23 +7470,23 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo if (toolchange_temp_override > 0) new_filament_temp = toolchange_temp_override; - Vec3d nozzle_pos = m_writer.get_position(); + Vec3d nozzle_pos = m_writer->get_position(); float old_retract_length, old_retract_length_toolchange, wipe_volume; int old_filament_temp, old_filament_e_feedrate; float filament_area = float((M_PI / 4.f) * pow(m_config.filament_diameter.get_at(new_filament_id), 2)); //BBS: add handling for filament change in start gcode int old_filament_id = -1; - if (m_writer.filament() != nullptr || m_start_gcode_filament != -1) { + if (m_writer->filament() != nullptr || m_start_gcode_filament != -1) { std::vector flush_matrix(cast(get_flush_volumes_matrix(m_config.flush_volumes_matrix.values, new_extruder_id, m_config.nozzle_diameter.values.size()))); const unsigned int number_of_extruders = (unsigned int) (m_config.filament_colour.values.size()); // if is multi_extruder only use the fist extruder matrix - if (m_writer.filament() != nullptr) - assert(m_writer.filament()->id() < number_of_extruders); + if (m_writer->filament() != nullptr) + assert(m_writer->filament()->id() < number_of_extruders); else assert(m_start_gcode_filament < number_of_extruders); - old_filament_id = m_writer.filament() != nullptr ? m_writer.filament()->id() : m_start_gcode_filament; - int old_extruder_id = m_writer.filament() != nullptr ? m_writer.filament()->extruder_id() : get_extruder_id(m_start_gcode_filament); + old_filament_id = m_writer->filament() != nullptr ? m_writer->filament()->id() : m_start_gcode_filament; + int old_extruder_id = m_writer->filament() != nullptr ? m_writer->filament()->extruder_id() : get_extruder_id(m_start_gcode_filament); old_retract_length = m_config.retraction_length.get_at(old_filament_id); old_retract_length_toolchange = m_config.retract_length_toolchange.get_at(old_filament_id); @@ -7662,7 +7496,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo float grab_purge_volume = m_config.grab_length.get_at(new_extruder_id) * 2.4; if (old_extruder_id != new_extruder_id) { //calc flush volume between the same extruder id - int old_filament_id_in_new_extruder = m_writer.filament(new_extruder_id) != nullptr ? m_writer.filament(new_extruder_id)->id() : -1; + int old_filament_id_in_new_extruder = m_writer->filament(new_extruder_id) != nullptr ? m_writer->filament(new_extruder_id)->id() : -1; if (old_filament_id_in_new_extruder == -1) wipe_volume = 0; else { @@ -7798,13 +7632,13 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo //BBS: gcode writer doesn't know where the extruder is and whether fan speed is changed after inserting tool change gcode //Set this flag so that normal lift will be used the first time after tool change. gcode += ";_FORCE_RESUME_FAN_SPEED\n"; - m_writer.set_current_position_clear(false); + m_writer->set_current_position_clear(false); //BBS: check whether custom gcode changes the z position. Update if changed double temp_z_after_tool_change; if (GCodeProcessor::get_last_z_from_gcode(toolchange_gcode_parsed, temp_z_after_tool_change)) { - Vec3d pos = m_writer.get_position(); + Vec3d pos = m_writer->get_position(); pos(2) = temp_z_after_tool_change; - m_writer.set_position(pos); + m_writer->set_position(pos); } } } @@ -7812,13 +7646,13 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo // BBS. Reset old extruder E-value. // Keep retract length because Custom GCode will guarantee retract length be the same as toolchange if (m_config.single_extruder_multi_material) { - m_writer.reset_e(); + m_writer->reset_e(); } //BBS: don't add T[next extruder] if there is no T cmd on filament change //We inform the writer about what is happening, but we may not use the resulting gcode. - std::string toolchange_command = m_writer.toolchange(new_filament_id); - if (!custom_gcode_changes_tool(toolchange_gcode_parsed, m_writer.toolchange_prefix(), new_filament_id)) + std::string toolchange_command = m_writer->toolchange(new_filament_id); + if (!custom_gcode_changes_tool(toolchange_gcode_parsed, m_writer->toolchange_prefix(), new_filament_id)) gcode += toolchange_command; else { // user provided his own toolchange gcode, no need to do anything @@ -7829,7 +7663,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo int temp = (m_layer_index <= 0 ? m_config.nozzle_temperature_initial_layer.get_at(new_filament_id) : m_config.nozzle_temperature.get_at(new_filament_id)); - gcode += m_writer.set_temperature(temp, false); + gcode += m_writer->set_temperature(temp, false); } this->placeholder_parser().set("current_extruder", new_filament_id); @@ -7873,7 +7707,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo gcode += m_ooze_prevention.post_toolchange(*this); if (m_config.enable_pressure_advance.get_at(new_filament_id)) { - gcode += m_writer.set_pressure_advance(m_config.pressure_advance.get_at(new_filament_id)); + gcode += m_writer->set_pressure_advance(m_config.pressure_advance.get_at(new_filament_id)); // Orca: Adaptive PA // Reset Adaptive PA processor last PA value m_pa_processor->resetPreviousPA(m_config.pressure_advance.get_at(new_filament_id)); @@ -7959,7 +7793,7 @@ Vec2d GCode::point_to_gcode(const Point &point) const Point GCode::gcode_to_point(const Vec2d &point) const { Vec2d pt = point - m_origin; - if (const Extruder *extruder = m_writer.filament(); extruder) + if (const Extruder *extruder = m_writer->filament(); extruder) // This function may be called at the very start from toolchange G-code when the extruder is not assigned yet. pt += m_config.extruder_offset.get_at(extruder->extruder_id()); return scaled(pt); diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index dbec7d0315..9004f30fd3 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -4,6 +4,7 @@ #include "libslic3r.h" #include "ExPolygon.hpp" #include "GCodeWriter.hpp" +#include "BeltGCodeWriter.hpp" #include "Layer.hpp" #include "Point.hpp" #include "PlaceholderParser.hpp" @@ -206,16 +207,18 @@ public: m_last_obj_copy(nullptr, Point(std::numeric_limits::max(), std::numeric_limits::max())), // BBS m_toolchange_count(0), - m_nominal_z(0.) + m_nominal_z(0.), + m_writer(std::make_unique()) {} - ~GCode() = default; + virtual ~GCode() = default; +public: // throws std::runtime_exception on error, // throws CanceledException through print->throw_if_canceled(). void do_export(Print* print, const char* path, GCodeProcessorResult* result = nullptr, ThumbnailsGeneratorCallback thumbnail_cb = nullptr); void export_layer_filaments(GCodeProcessorResult* result); //BBS: set offset for gcode writer - void set_gcode_offset(double x, double y) { m_writer.set_xy_offset(x, y); m_processor.set_xy_offset(x, y);} + void set_gcode_offset(double x, double y) { m_writer->set_xy_offset(x, y); m_processor.set_xy_offset(x, y);} // Exported for the helper classes (OozePrevention, Wipe) and for the Perl binding for unit tests. const Vec2d& origin() const { return m_origin; } @@ -227,8 +230,8 @@ public: Vec2d point_to_gcode_quantized(const Point& point) const; const FullPrintConfig &config() const { return m_config; } const Layer* layer() const { return m_layer; } - GCodeWriter& writer() { return m_writer; } - const GCodeWriter& writer() const { return m_writer; } + GCodeWriter& writer() { return *m_writer; } + const GCodeWriter& writer() const { return *m_writer; } PlaceholderParser& placeholder_parser() { return m_placeholder_parser_integration.parser; } const PlaceholderParser& placeholder_parser() const { return m_placeholder_parser_integration.parser; } // Process a template through the placeholder parser, collect error messages to be reported @@ -248,7 +251,7 @@ public: std::string travel_to(const Point& point, ExtrusionRole role, std::string comment, double z = DBL_MAX); bool needs_retraction(const Polyline& travel, ExtrusionRole role, LiftType& lift_type); std::string retract(bool toolchange = false, bool is_last_retraction = false, LiftType lift_type = LiftType::NormalLift, bool apply_instantly = false, ExtrusionRole role = erNone); - std::string unretract() { return m_writer.unlift() + m_writer.unretract(); } + std::string unretract() { return m_writer->unlift() + m_writer->unretract(); } std::string set_extruder(unsigned int extruder_id, double print_z, bool by_object=false, int toolchange_temp_override = -1); bool is_BBL_Printer(); WipeTowerType wipe_tower_type(); @@ -300,7 +303,7 @@ public: } }; -private: +protected: class GCodeOutputStream { public: GCodeOutputStream(FILE *f, GCodeProcessor &processor) : f(f), m_processor(processor) {} @@ -328,9 +331,17 @@ private: FILE *f = nullptr; GCodeProcessor &m_processor; }; + + // Virtual hooks for belt printer subclass (BeltGCode). + // No-ops in base GCode; overridden in BeltGCode. + virtual void init_belt_writer(Print &print, bool is_bbl_printers) {} + virtual void write_belt_header(GCodeOutputStream &file, const Print &print) {} + virtual void on_set_origin(const PrintObject *obj, const Point &inst_shift) {} + virtual bool should_disable_arc_fitting() const { return false; } + void _do_export(Print &print, GCodeOutputStream &file, ThumbnailsGeneratorCallback thumbnail_cb); - static std::vector collect_layers_to_print(const PrintObject &object); + static std::vector collect_layers_to_print(const PrintObject &object, bool skip_empty_first_layer = false); static std::vector>> collect_layers_to_print(const Print &print); std::string generate_skirt(const Print &print, @@ -493,16 +504,11 @@ private: This affects the input arguments supplied to the extrude*() and travel_to() methods. */ Vec2d m_origin; - // Per-axis origin snap: shift G-code so each object's bbox min = offset. - bool m_origin_snap[3] = {false, false, false}; - double m_origin_snap_offset[3] = {0., 0., 0.}; - // Called when switching instances to recompute the writer's snap for this instance. - void update_origin_snap(const PrintObject *obj, const Point &inst_shift); FullPrintConfig m_config; DynamicConfig m_calib_config; // scaled G-code resolution double m_scaled_resolution; - GCodeWriter m_writer; + std::unique_ptr m_writer; struct PlaceholderParserIntegration { void reset(); diff --git a/src/libslic3r/GCode/BeltBackTransform.cpp b/src/libslic3r/GCode/BeltBackTransform.cpp index 7cd5bc6b53..2e2cd26f75 100644 --- a/src/libslic3r/GCode/BeltBackTransform.cpp +++ b/src/libslic3r/GCode/BeltBackTransform.cpp @@ -1,40 +1,8 @@ #include "BeltBackTransform.hpp" -#include "../Geometry.hpp" -#include +#include "../BeltTransform.hpp" namespace Slic3r { -// Keep in sync with PrintObjectSlice.cpp compute_shear_factor (lines ~147-157). -static double compute_shear_factor(BeltShearMode mode, double angle_deg) -{ - double angle_rad = Geometry::deg2rad(angle_deg); - double sin_a = std::sin(angle_rad); - double cos_a = std::cos(angle_rad); - switch (mode) { - case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; - case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; - case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; - case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; - default: return 0.; - } -} - -// Keep in sync with PrintObjectSlice.cpp compute_scale_factor (lines ~180-192). -static double compute_scale_factor(BeltScaleMode mode, double angle_deg) -{ - if (mode == BeltScaleMode::None) return 1.; - double angle_rad = Geometry::deg2rad(angle_deg); - double sin_a = std::sin(angle_rad); - double cos_a = std::cos(angle_rad); - switch (mode) { - case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; - case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; - case BeltScaleMode::Sin: return sin_a; - case BeltScaleMode::Cos: return cos_a; - default: return 1.; - } -} - bool BeltBackTransform::init_from_config(const PrintConfig &config) { m_active = false; @@ -43,99 +11,19 @@ bool BeltBackTransform::init_from_config(const PrintConfig &config) if (!config.belt_printer.value || !config.belt_gcode_back_transform.value) return false; - // --- Pre-slice axis remap (same as PrintObjectSlice.cpp) --- - int pre_rx = int(config.belt_preslice_remap_x.value); - int pre_ry = int(config.belt_preslice_remap_y.value); - int pre_rz = int(config.belt_preslice_remap_z.value); - - bool has_preslice_remap = (pre_rx != int(BeltRemapAxis::PosX) || - pre_ry != int(BeltRemapAxis::PosY) || - pre_rz != int(BeltRemapAxis::PosZ)); - // Require at least one active transform to proceed. bool has_global_shear = config.belt_shear_x_global.value || config.belt_shear_y_global.value || config.belt_shear_z_global.value; - if (!has_global_shear && !has_preslice_remap) + if (!has_global_shear && !BeltTransformPipeline::has_preslice_remap(config)) return false; - // Build pre-slice remap matrix. - Transform3d pre_remap = Transform3d::Identity(); - if (has_preslice_remap) { - auto remap_column = [](int r) -> Vec3d { - int axis = r % 3; - Vec3d col = Vec3d::Zero(); - if (r < 3) col[axis] = 1.0; - else if (r < 6) col[axis] = -1.0; - else col[axis] = -1.0; // Rev: max - pos - 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; - - // Rev mode translation (needs build volume extents). - Vec3d remap_trans = Vec3d::Zero(); - 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); - 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; - } - - // Build per-axis shear matrix (same as PrintObjectSlice.cpp). - struct AxisShear { BeltShearMode mode; double angle; int from; }; - AxisShear axes[3] = { - { config.belt_shear_x.value, config.belt_shear_x_angle.value, int(config.belt_shear_x_from.value) }, - { config.belt_shear_y.value, config.belt_shear_y_angle.value, int(config.belt_shear_y_from.value) }, - { config.belt_shear_z.value, config.belt_shear_z_angle.value, int(config.belt_shear_z_from.value) }, - }; - - Matrix3d shear = Matrix3d::Identity(); - bool has_shear = false; - for (int row = 0; row < 3; ++row) { - if (axes[row].mode != BeltShearMode::None) { - double factor = compute_shear_factor(axes[row].mode, axes[row].angle); - if (std::abs(factor) > EPSILON) { - shear(row, axes[row].from) += factor; - has_shear = true; - } - } - } - - // Build per-axis scale diagonal matrix (same as PrintObjectSlice.cpp). - double sx = compute_scale_factor(config.belt_scale_x.value, config.belt_scale_x_angle.value); - double sy = compute_scale_factor(config.belt_scale_y.value, config.belt_scale_y_angle.value); - double sz = compute_scale_factor(config.belt_scale_z.value, config.belt_scale_z_angle.value); - - Matrix3d scale = Matrix3d::Identity(); - bool has_scale = (std::abs(sx - 1.) > EPSILON || - std::abs(sy - 1.) > EPSILON || - std::abs(sz - 1.) > EPSILON); - if (has_scale) { - scale(0, 0) = sx; - scale(1, 1) = sy; - scale(2, 2) = sz; - } - - if (!has_shear && !has_scale && !has_preslice_remap) + // Build the forward pipeline (scale * shear * pre_remap) and store its inverse. + Transform3d forward = BeltTransformPipeline::build_forward_transform(config); + if (forward.isApprox(Transform3d::Identity())) return false; - // Forward pipeline: scale * shear * pre_remap (same order as PrintObjectSlice.cpp). - Transform3d combined = Transform3d::Identity(); - combined.linear() = scale * shear; - combined = combined * pre_remap; - m_inverse = combined.inverse(); + m_inverse = forward.inverse(); m_active = true; return true; } diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index 8c2f7122b8..fea5509bc3 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -20,59 +20,6 @@ namespace Slic3r { bool GCodeWriter::full_gcode_comment = true; -void GCodeWriter::set_belt_angle(double angle_deg) -{ - m_belt_angle_rad = Geometry::deg2rad(angle_deg); -} - -void GCodeWriter::set_axis_remap(int rx, int ry, int rz) -{ - m_remap_x = rx; - m_remap_y = ry; - m_remap_z = rz; -} - -void GCodeWriter::set_build_volume_max(const Vec3d &max) -{ - m_build_vol_max = max; -} - -void GCodeWriter::set_belt_back_transform(const PrintConfig &config) -{ - m_belt_back_transform.init_from_config(config); -} - -void GCodeWriter::set_origin_snap(int axis, bool enable, double offset, double bbox_min) -{ - if (axis >= 0 && axis < 3) { - m_origin_snap[axis] = enable; - m_origin_offset[axis] = offset; - m_origin_bbox_min[axis] = bbox_min; - } -} - -Vec3d GCodeWriter::to_machine_coords(const Vec3d &pos) const -{ - if (!is_belt_printer()) - return pos; - // Step 1: Undo the shear/scale applied during slicing. - Vec3d p = m_belt_back_transform.apply(pos); - // Step 2: Apply axis remapping for the machine's coordinate convention. - // BeltRemapAxis: 0-2 = +X/+Y/+Z, 3-5 = -X/-Y/-Z, 6-8 = Rev X/Y/Z - auto remap = [this, &p](int r) -> double { - int axis = r % 3; - if (r < 3) return p[axis]; - if (r < 6) return -p[axis]; - return m_build_vol_max[axis] - p[axis]; - }; - Vec3d result = { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) }; - // Per-axis origin snap: shift so bbox min on each enabled axis = offset. - for (int i = 0; i < 3; ++i) - if (m_origin_snap[i]) - result[i] -= (m_origin_bbox_min[i] - m_origin_offset[i]); - return result; -} - bool GCodeWriter::supports_separate_travel_acceleration(GCodeFlavor flavor) { return (flavor == gcfRepetier || flavor == gcfMarlinFirmware || flavor == gcfRepRapFirmware); @@ -601,13 +548,7 @@ std::string GCodeWriter::travel_to_xy(const Vec2d &point, const std::string &com Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; GCodeG1Formatter w; - if (is_belt_printer()) { - // Belt printer: transform to machine coordinates (XY travel also needs Z due to YZ rotation) - Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); - w.emit_xyz(machine); - } else { - w.emit_xy(point_on_plate); - } + w.emit_xy(point_on_plate); auto speed = m_is_first_layer ? this->config.get_abs_value("initial_layer_travel_speed") : this->config.travel_speed.value; w.emit_f(speed * 60.0); @@ -621,11 +562,6 @@ it will not perform subsequent lifts, even if Z was raised manually (i.e. with travel_to_z()) and thus _lifted was reduced. */ std::string GCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase) { - // Belt printer: force NormalLift since SpiralLift and SlopeLift compute slope angles - // that don't account for the YZ coordinate rotation. - if (is_belt_printer()) - lift_type = LiftType::NormalLift; - // check whether the above/below conditions are met double target_lift = 0; { @@ -654,8 +590,7 @@ std::string GCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase) // BBS: immediately execute an undelayed lift move with a spiral lift pattern // designed specifically for subsequent gcode injection (e.g. timelapse) std::string GCodeWriter::eager_lift(const LiftType type) { - // Belt printer: force NormalLift (SpiralLift/SlopeLift don't account for YZ rotation). - const LiftType effective_type = is_belt_printer() ? LiftType::NormalLift : type; + const LiftType effective_type = type; std::string lift_move; double target_lift = 0; { @@ -742,8 +677,6 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co // / to make the z list early to avoid to hit some warping place when travel is long. Vec2d temp = delta_no_z.normalized() * delta(2) / tan(this->filament()->travel_slope()); Vec3d slope_top_point = Vec3d(temp(0), temp(1), delta(2)) + source; - if (is_belt_printer()) - slope_top_point = to_machine_coords(slope_top_point); GCodeG1Formatter w0; w0.emit_xyz(slope_top_point); w0.emit_f(travel_speed * 60.0); @@ -758,27 +691,18 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co std::string xy_z_move; { - Vec3d emit_target = is_belt_printer() ? to_machine_coords(target) : target; GCodeG1Formatter w0; if (this->is_current_position_clear()) { - w0.emit_xyz(emit_target); + w0.emit_xyz(target); w0.emit_f(travel_speed * 60.0); w0.emit_comment(GCodeWriter::full_gcode_comment, comment); xy_z_move = w0.string(); } else { - if (is_belt_printer()) { - // Belt mode: can't split XY and Z moves independently, emit full XYZ - w0.emit_xyz(emit_target); - w0.emit_f(travel_speed * 60.0); - w0.emit_comment(GCodeWriter::full_gcode_comment, comment); - xy_z_move = w0.string(); - } else { - w0.emit_xy(Vec2d(target.x(), target.y())); - w0.emit_f(travel_speed * 60.0); - w0.emit_comment(GCodeWriter::full_gcode_comment, comment); - xy_z_move = w0.string() + _travel_to_z(target.z(), comment); - } + w0.emit_xy(Vec2d(target.x(), target.y())); + w0.emit_f(travel_speed * 60.0); + w0.emit_comment(GCodeWriter::full_gcode_comment, comment); + xy_z_move = w0.string() + _travel_to_z(target.z(), comment); } } m_pos = dest_point; @@ -804,25 +728,15 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co //BBS: take plate offset into consider Vec3d point_on_plate = { dest_point(0) - m_x_offset, dest_point(1) - m_y_offset, dest_point(2) }; - if (is_belt_printer()) - point_on_plate = to_machine_coords(point_on_plate); std::string out_string; GCodeG1Formatter w; if (!this->is_current_position_clear()) { - if (is_belt_printer()) { - // Belt mode: emit full XYZ since Y and Z are coupled - w.emit_xyz(point_on_plate); - w.emit_f(this->config.travel_speed.value * 60.0); - w.emit_comment(GCodeWriter::full_gcode_comment, comment); - out_string = w.string(); - } else { - //force to move xy first then z after filament change - w.emit_xy(Vec2d(point_on_plate.x(), point_on_plate.y())); - w.emit_f(this->config.travel_speed.value * 60.0); - w.emit_comment(GCodeWriter::full_gcode_comment, comment); - out_string = w.string() + _travel_to_z(point_on_plate.z(), comment); - } + //force to move xy first then z after filament change + w.emit_xy(Vec2d(point_on_plate.x(), point_on_plate.y())); + w.emit_f(this->config.travel_speed.value * 60.0); + w.emit_comment(GCodeWriter::full_gcode_comment, comment); + out_string = w.string() + _travel_to_z(point_on_plate.z(), comment); } else { GCodeG1Formatter w; w.emit_xyz(point_on_plate); @@ -866,13 +780,7 @@ std::string GCodeWriter::_travel_to_z(double z, const std::string &comment) } GCodeG1Formatter w; - if (is_belt_printer()) { - // Belt printer: a Z-only move in slicing frame needs to emit both Y and Z in machine coords. - Vec3d machine = to_machine_coords(Vec3d(m_pos.x() - m_x_offset, m_pos.y() - m_y_offset, z)); - w.emit_xyz(machine); - } else { - w.emit_z(z); - } + w.emit_z(z); w.emit_f(speed * 60.0); //BBS w.emit_comment(GCodeWriter::full_gcode_comment, comment); @@ -977,12 +885,7 @@ std::string GCodeWriter::extrude_to_xy(const Vec2d &point, double dE, const std: Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; GCodeG1Formatter w; - if (is_belt_printer()) { - Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); - w.emit_xyz(machine); - } else { - w.emit_xy(point_on_plate); - } + w.emit_xy(point_on_plate); if (!force_no_extrusion) w.emit_e(filament()->E()); //BBS @@ -1023,8 +926,6 @@ std::string GCodeWriter::extrude_to_xyz(const Vec3d &point, double dE, const std Vec3d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset, point(2) }; GCodeG1Formatter w; - if (is_belt_printer()) - point_on_plate = to_machine_coords(point_on_plate); w.emit_xyz(point_on_plate); if (!force_no_extrusion) w.emit_e(filament()->E()); diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index 2334f5d416..c96c3ca2c1 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -8,12 +8,11 @@ #include "Point.hpp" #include "PrintConfig.hpp" #include "GCode/CoolingBuffer.hpp" -#include "GCode/BeltBackTransform.hpp" - namespace Slic3r { class GCodeWriter { public: + virtual ~GCodeWriter() = default; GCodeConfig config; bool multiple_extruders; @@ -74,21 +73,21 @@ public: std::string set_speed(double F, const std::string &comment = std::string(), const std::string &cooling_marker = std::string()); // SoftFever NOTE: the returned speed is mm/minute double get_current_speed() const { return m_current_speed;} - std::string travel_to_xy(const Vec2d &point, const std::string &comment = std::string()); - std::string travel_to_xyz(const Vec3d &point, const std::string &comment = std::string(), bool force_z = false); + virtual std::string travel_to_xy(const Vec2d &point, const std::string &comment = std::string()); + virtual std::string travel_to_xyz(const Vec3d &point, const std::string &comment = std::string(), bool force_z = false); std::string travel_to_z(double z, const std::string &comment = std::string(), bool force = false); bool will_move_z(double z) const; - std::string extrude_to_xy(const Vec2d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false); + virtual std::string extrude_to_xy(const Vec2d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false); //BBS: generate G2 or G3 extrude which moves by arc std::string extrude_arc_to_xy(const Vec2d &point, const Vec2d ¢er_offset, double dE, const bool is_ccw, const std::string &comment = std::string(), bool force_no_extrusion = false); - std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false); + virtual std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false); std::string retract(bool before_wipe = false, double retract_length = 0); std::string retract_for_toolchange(bool before_wipe = false, double retract_length = 0); std::string unretract(); // do lift instantly - std::string eager_lift(const LiftType type); + virtual std::string eager_lift(const LiftType type); // record a lift request, do realy lift in next travel - std::string lazy_lift(LiftType lift_type = LiftType::NormalLift, bool spiral_vase = false); + virtual std::string lazy_lift(LiftType lift_type = LiftType::NormalLift, bool spiral_vase = false); std::string unlift(); const Vec3d& get_position() const { return m_pos; } Vec3d& get_position() { return m_pos; } @@ -126,23 +125,23 @@ public: void set_is_first_layer(bool bval) { m_is_first_layer = bval; } GCodeFlavor get_gcode_flavor() const { return config.gcode_flavor; } - // Belt printer: set the belt angle and precompute sin/cos for coordinate transformation. - void set_belt_angle(double angle_deg); - bool is_belt_printer() const { return m_belt_angle_rad != 0.; } - // Set axis remap for G-code output (BeltRemapAxis enum values). - void set_axis_remap(int rx, int ry, int rz); - // Set build volume extents for Rev remap mode (max X, Y, Z). - void set_build_volume_max(const Vec3d &max); - // Initialize the belt back-transform that undoes slicing shear/scale. - void set_belt_back_transform(const PrintConfig &config); - // Set per-axis origin snap: shifts G-code so bbox min on this axis = offset. - void set_origin_snap(int axis, bool enable, double offset, double bbox_min); - // Transform a point from the slicing frame to machine coordinates. - Vec3d to_machine_coords(const Vec3d &pos) const; - // Returns whether this flavor supports separate print and travel acceleration. static bool supports_separate_travel_acceleration(GCodeFlavor flavor); - private: +protected: + // Position/lift/offset state — accessible to subclasses (e.g. BeltGCodeWriter) + Vec3d m_pos = Vec3d::Zero(); + double m_x_offset{ 0 }; + double m_y_offset{ 0 }; + double m_lifted; + double m_to_lift; + LiftType m_to_lift_type; + bool m_is_first_layer = true; + bool m_is_current_pos_clear = false; + double m_current_speed; + + virtual std::string _travel_to_z(double z, const std::string &comment); + +private: // Extruders are sorted by their ID, so that binary search is possible. std::vector m_filament_extruders; bool m_single_extruder_multi_material; @@ -170,48 +169,21 @@ public: unsigned int m_last_additional_fan_speed; int m_last_bed_temperature; bool m_last_bed_temperature_reached; - double m_lifted; - - // BBS - double m_to_lift; - LiftType m_to_lift_type; - Vec3d m_pos = Vec3d::Zero(); - //BBS: this flag is used to indicate whether the m_pos is real. - //A example that of the first move, the m_pos is zero, but the real position of extruder doesn't - //Pos must be clear after the first xyz travel move - bool m_is_current_pos_clear = false; - //BBS: x, y offset for gcode generated - double m_x_offset{ 0 }; - double m_y_offset{ 0 }; // Orca: slicing resolution in mm double m_resolution = 0.01; - + std::string m_gcode_label_objects_start; std::string m_gcode_label_objects_end; //SoftFever bool m_is_bbl_printers = false; - // Belt printer state - double m_belt_angle_rad = 0.; - int m_remap_x = 0; - int m_remap_y = 1; - int m_remap_z = 2; - Vec3d m_build_vol_max = Vec3d::Zero(); - BeltBackTransform m_belt_back_transform; - bool m_origin_snap[3] = {false, false, false}; - double m_origin_offset[3] = {0., 0., 0.}; // target coord for bbox min - double m_origin_bbox_min[3] = {0., 0., 0.}; // computed bbox min in machine space - double m_current_speed; - bool m_is_first_layer = true; - enum class Acceleration { Travel, Print }; - std::string _travel_to_z(double z, const std::string &comment); std::string _spiral_travel_to_z(double z, const Vec2d &ij_offset, const std::string &comment); std::string _retract(double length, double restart_extra, const std::string &comment); std::string set_acceleration_internal(Acceleration type, unsigned int acceleration); diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 0b7190e8d1..1f787a70cf 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -12,6 +12,7 @@ #include "Thread.hpp" #include "Time.hpp" #include "GCode.hpp" +#include "BeltGCode.hpp" #include "GCode/WipeTower.hpp" #include "GCode/WipeTower2.hpp" #include "Utils.hpp" @@ -2551,12 +2552,17 @@ std::string Print::export_gcode(const std::string& path_template, GCodeProcessor this->set_status(80, message); // The following line may die for multiple reasons. - GCode gcode; + // Factory: use BeltGCode for belt printers, plain GCode otherwise. + std::unique_ptr gcode; + if (m_config.belt_printer.value) + gcode = std::make_unique(); + else + gcode = std::make_unique(); //BBS: compute plate offset for gcode-generator const Vec3d origin = this->get_plate_origin(); - gcode.set_gcode_offset(origin(0), origin(1)); - gcode.do_export(this, path.c_str(), result, thumbnail_cb); - gcode.export_layer_filaments(result); + gcode->set_gcode_offset(origin(0), origin(1)); + gcode->do_export(this, path.c_str(), result, thumbnail_cb); + gcode->export_layer_filaments(result); //BBS result->conflict_result = m_conflict_result; return path.c_str(); diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index c13a1407c0..9a5d000e1c 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -17,6 +17,7 @@ #include "GCode/ThumbnailData.hpp" #include "GCode/GCodeProcessor.hpp" #include "MultiMaterialSegmentation.hpp" +#include "BeltTransform.hpp" #include "libslic3r.h" #include @@ -190,30 +191,7 @@ class ConstSupportLayerPtrsAdaptor : public ConstVectorOfPtrsAdaptor double { - int axis = r % 3; - if (r < 3) return v[axis]; - return -v[axis]; - }; - Vec3d mn = bb.min.cast(), mx = bb.max.cast(); - 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; + return BeltTransformPipeline::remap_bbox(model_object, config); } // Single instance of a PrintObject. diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index ba2ed408ce..be65c21c59 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1,5 +1,6 @@ #include "Exception.hpp" #include "Print.hpp" +#include "BeltTransform.hpp" #include "BoundingBox.hpp" #include "ClipperUtils.hpp" #include "ElephantFootCompensation.hpp" @@ -3398,76 +3399,22 @@ void PrintObject::update_slicing_parameters() // Orca: updated function call for XYZ shrinkage compensation if (!m_slicing_params.valid) { coordf_t object_height = this->model_object()->max_z(); - // Belt floor parameters for support clipping (populated below if belt Z-shear is active). - double belt_floor_shear_factor_out = 0.0; - int belt_floor_from_axis_out = 1; - double belt_floor_z_shift_out = 0.0; - // Belt shear/scale/pre-remap may change the effective Z height. + BeltTransformPipeline::BeltFloorParams belt_floor; const auto &pcfg = this->print()->config(); if (pcfg.belt_printer.value) { - BoundingBoxf3 bb = belt_remapped_bbox(*this->model_object(), pcfg); - bool has_preslice_remap = (int(pcfg.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || - int(pcfg.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || - int(pcfg.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ)); - if (has_preslice_remap) + BoundingBoxf3 bb = BeltTransformPipeline::remap_bbox(*this->model_object(), pcfg); + if (BeltTransformPipeline::has_preslice_remap(pcfg)) object_height = bb.size().z(); - - bool has_z_shear = pcfg.belt_shear_z.value != BeltShearMode::None; - bool has_z_scale = pcfg.belt_scale_z.value != BeltScaleMode::None; - if (has_z_shear || has_z_scale) { - auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { - double angle_rad = Geometry::deg2rad(angle_deg); - double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad); - switch (mode) { - case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; - case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; - case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; - case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; - default: return 0.; - } - }; - auto compute_scale_factor = [](BeltScaleMode mode, double angle_deg) -> double { - if (mode == BeltScaleMode::None) return 1.; - double angle_rad = Geometry::deg2rad(angle_deg); - double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad); - switch (mode) { - case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; - case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; - case BeltScaleMode::Sin: return sin_a; - case BeltScaleMode::Cos: return cos_a; - default: return 1.; - } - }; - double shear_factor = has_z_shear ? compute_shear_factor(pcfg.belt_shear_z.value, pcfg.belt_shear_z_angle.value) : 0.; - double scale_z = compute_scale_factor(pcfg.belt_scale_z.value, pcfg.belt_scale_z_angle.value); - if (has_z_shear && std::abs(shear_factor) > EPSILON) { - int from = int(pcfg.belt_shear_z_from.value); - double min_rz = std::numeric_limits::max(); - double max_rz = std::numeric_limits::lowest(); - for (double vz : {bb.min.z(), bb.max.z()}) - for (double vs : {bb.min(from), bb.max(from)}) { - double new_z = scale_z * (vz + shear_factor * vs); - min_rz = std::min(min_rz, new_z); - max_rz = std::max(max_rz, new_z); - } - object_height = max_rz - min_rz; - belt_floor_shear_factor_out = shear_factor; - belt_floor_from_axis_out = from; - // Belt contact surface starts at bb.min.z() pre-shear; add the - // slicing Z-shift that keeps the mesh above Z=0. - // Exact value is patched after slice_volumes() in posSlice. - belt_floor_z_shift_out = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.); - } else { - object_height *= scale_z; - } - } + auto hr = BeltTransformPipeline::compute_belt_height_and_floor(pcfg, bb, object_height); + object_height = hr.object_height; + belt_floor = hr.floor_params; } m_slicing_params = SlicingParameters::create_from_config(pcfg, m_config, object_height, this->object_extruders(), this->print()->shrinkage_compensation()); // Populate belt floor parameters into slicing params for support clipping. - m_slicing_params.belt_floor_shear_factor = belt_floor_shear_factor_out; - m_slicing_params.belt_floor_from_axis = belt_floor_from_axis_out; - m_slicing_params.belt_floor_z_shift = belt_floor_z_shift_out; + m_slicing_params.belt_floor_shear_factor = belt_floor.shear_factor; + m_slicing_params.belt_floor_from_axis = belt_floor.from_axis; + m_slicing_params.belt_floor_z_shift = belt_floor.z_shift; } } @@ -3505,75 +3452,23 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full sort_remove_duplicates(object_extruders); //FIXME add painting extruders - // Belt floor parameters for support clipping (populated below if belt Z-shear is active). - double belt_floor_shear_factor_out = 0.0; - int belt_floor_from_axis_out = 1; - double belt_floor_z_shift_out = 0.0; - + BeltTransformPipeline::BeltFloorParams belt_floor; if (object_max_z <= 0.f) { BoundingBoxf3 bb = model_object.raw_bounding_box(); object_max_z = (float)bb.size().z(); - // Belt pre-remap/shear/scale may change the effective Z height. if (print_config.belt_printer.value) { - bb = belt_remapped_bbox(model_object, print_config); - bool has_preslice_remap = (int(print_config.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || - int(print_config.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || - int(print_config.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ)); - if (has_preslice_remap) + bb = BeltTransformPipeline::remap_bbox(model_object, print_config); + if (BeltTransformPipeline::has_preslice_remap(print_config)) object_max_z = (float)bb.size().z(); - - bool has_z_shear = print_config.belt_shear_z.value != BeltShearMode::None; - bool has_z_scale = print_config.belt_scale_z.value != BeltScaleMode::None; - if (has_z_shear || has_z_scale) { - auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { - double angle_rad = Geometry::deg2rad(angle_deg); - double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad); - switch (mode) { - case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; - case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; - case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; - case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; - default: return 0.; - } - }; - auto compute_scale_factor = [](BeltScaleMode mode, double angle_deg) -> double { - if (mode == BeltScaleMode::None) return 1.; - double angle_rad = Geometry::deg2rad(angle_deg); - double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad); - switch (mode) { - case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; - case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; - case BeltScaleMode::Sin: return sin_a; - case BeltScaleMode::Cos: return cos_a; - default: return 1.; - } - }; - double shear_factor = has_z_shear ? compute_shear_factor(print_config.belt_shear_z.value, print_config.belt_shear_z_angle.value) : 0.; - double scale_z = compute_scale_factor(print_config.belt_scale_z.value, print_config.belt_scale_z_angle.value); - if (has_z_shear && std::abs(shear_factor) > EPSILON) { - int from = int(print_config.belt_shear_z_from.value); - double min_rz = std::numeric_limits::max(); - double max_rz = std::numeric_limits::lowest(); - for (double vz : {bb.min.z(), bb.max.z()}) - for (double vs : {bb.min(from), bb.max(from)}) { - double new_z = scale_z * (vz + shear_factor * vs); - min_rz = std::min(min_rz, new_z); - max_rz = std::max(max_rz, new_z); - } - object_max_z = (float)(max_rz - min_rz); - belt_floor_shear_factor_out = shear_factor; - belt_floor_from_axis_out = from; - belt_floor_z_shift_out = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.); - } else { - object_max_z *= (float)scale_z; - } - } + 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; } } SlicingParameters params = SlicingParameters::create_from_config(print_config, object_config, object_max_z, object_extruders, object_shrinkage_compensation); - params.belt_floor_shear_factor = belt_floor_shear_factor_out; - params.belt_floor_from_axis = belt_floor_from_axis_out; - params.belt_floor_z_shift = belt_floor_z_shift_out; + params.belt_floor_shear_factor = belt_floor.shear_factor; + params.belt_floor_from_axis = belt_floor.from_axis; + params.belt_floor_z_shift = belt_floor.z_shift; return params; } diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 4c767ea0ed..3067772f09 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -9,6 +9,8 @@ #include "Layer.hpp" #include "MultiMaterialSegmentation.hpp" #include "Print.hpp" +#include "BeltTransform.hpp" +#include "BeltSliceStrategy.hpp" #include "Geometry.hpp" //BBS #include "ShortestPath.hpp" @@ -142,147 +144,11 @@ static std::vector slice_volumes_inner( params_base.closing_radius = print_object_config.slice_closing_radius.value; params_base.extra_offset = 0; params_base.trafo = object_trafo; - if (print_config.belt_printer.value) { - // --- Pre-slice axis remap --- - // Permutes/negates model axes before slicing so the slicer's coordinate - // system matches the physical bed orientation (e.g. XZ bed instead of XY). - int pre_rx = int(print_config.belt_preslice_remap_x.value); - int pre_ry = int(print_config.belt_preslice_remap_y.value); - int pre_rz = int(print_config.belt_preslice_remap_z.value); - - bool has_preslice_remap = (pre_rx != int(BeltRemapAxis::PosX) || - pre_ry != int(BeltRemapAxis::PosY) || - pre_rz != int(BeltRemapAxis::PosZ)); - - if (has_preslice_remap) { - // Build volume extents for Rev mode. - BoundingBoxf bbox_bed(print_config.printable_area.values); - Vec3d vol_max(bbox_bed.max.x(), bbox_bed.max.y(), - print_config.printable_height.value); - - // Each remap value selects a source axis and sign. - // The column vector tells the matrix which input axis feeds this output. - 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); - - // Translation for Rev modes: output = max[src] - input[src]. - Vec3d remap_trans = Vec3d::Zero(); - auto add_rev_offset = [&](int r, int out_axis) { - if (r >= 6) { - int src_axis = r % 3; - remap_trans[out_axis] = vol_max[src_axis]; - } - }; - add_rev_offset(pre_rx, 0); - add_rev_offset(pre_ry, 1); - add_rev_offset(pre_rz, 2); - - Transform3d pre_remap = Transform3d::Identity(); - pre_remap.linear() = remap_lin; - pre_remap.translation() = remap_trans; - - params_base.trafo = pre_remap * params_base.trafo; - } - - // Build per-axis shear matrix from 3 independent axis configs. - auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { - double angle_rad = Geometry::deg2rad(angle_deg); - double sin_a = std::sin(angle_rad); - double cos_a = std::cos(angle_rad); - switch (mode) { - case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; - case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; - case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; - case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; - default: return 0.; - } - }; - - struct AxisShear { BeltShearMode mode; double angle; int from; }; - AxisShear axes[3] = { - { print_config.belt_shear_x.value, print_config.belt_shear_x_angle.value, int(print_config.belt_shear_x_from.value) }, - { print_config.belt_shear_y.value, print_config.belt_shear_y_angle.value, int(print_config.belt_shear_y_from.value) }, - { print_config.belt_shear_z.value, print_config.belt_shear_z_angle.value, int(print_config.belt_shear_z_from.value) }, - }; - - Transform3d belt_shear = Transform3d::Identity(); - bool has_shear = false; - for (int row = 0; row < 3; ++row) { - if (axes[row].mode != BeltShearMode::None) { - double factor = compute_shear_factor(axes[row].mode, axes[row].angle); - if (std::abs(factor) > EPSILON) { - belt_shear.matrix()(row, axes[row].from) += factor; - has_shear = true; - } - } - } - - // Build per-axis scale matrix. - auto compute_scale_factor = [](BeltScaleMode mode, double angle_deg) -> double { - if (mode == BeltScaleMode::None) return 1.; - double angle_rad = Geometry::deg2rad(angle_deg); - double sin_a = std::sin(angle_rad); - double cos_a = std::cos(angle_rad); - switch (mode) { - case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; - case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; - case BeltScaleMode::Sin: return sin_a; - case BeltScaleMode::Cos: return cos_a; - default: return 1.; - } - }; - - Transform3d belt_scale = Transform3d::Identity(); - bool has_scale = false; - double sx = compute_scale_factor(print_config.belt_scale_x.value, print_config.belt_scale_x_angle.value); - double sy = compute_scale_factor(print_config.belt_scale_y.value, print_config.belt_scale_y_angle.value); - double sz = compute_scale_factor(print_config.belt_scale_z.value, print_config.belt_scale_z_angle.value); - if (std::abs(sx - 1.) > EPSILON || std::abs(sy - 1.) > EPSILON || std::abs(sz - 1.) > EPSILON) { - belt_scale.matrix()(0, 0) = sx; - belt_scale.matrix()(1, 1) = sy; - belt_scale.matrix()(2, 2) = sz; - has_scale = true; - } - - // Apply: scale * shear * trafo (shear first, then scale). - if (has_shear || has_scale) - params_base.trafo = belt_scale * belt_shear * params_base.trafo; - - // After pre-remap/shear/scale, the mesh may clip through the build - // plate (Z < 0). Detect this and shift the mesh up along slicer Z. - if (has_preslice_remap || has_shear || has_scale) { - Transform3d combined = params_base.trafo; - double min_z = std::numeric_limits::max(); - for (const ModelVolume *mv : model_volumes) { - if (!mv->is_model_part()) continue; - for (const stl_vertex &v : mv->mesh().its.vertices) { - Vec3d pt = combined * v.cast(); - min_z = std::min(min_z, pt.z()); - } - } - double belt_z_shift_val = (min_z < 0. && min_z != std::numeric_limits::max()) ? -min_z : 0.; - BOOST_LOG_TRIVIAL(warning) << "Belt Z-shift: min_z=" << min_z - << " z_shift=" << belt_z_shift_val - << " trafo_z=" << object_trafo.matrix()(2, 3); - if (belt_z_shift_val > 0.) { - Transform3d z_shift = Transform3d::Identity(); - z_shift.matrix()(2, 3) = belt_z_shift_val; - params_base.trafo = z_shift * params_base.trafo; - } - if (out_belt_min_z) - *out_belt_min_z = (min_z != std::numeric_limits::max()) ? min_z : 0.; - } + { + // Belt printer: apply pre-slice transforms (remap, shear, scale, z-shift) via strategy. + auto belt_strategy = BeltSliceStrategy::create(print_config); + if (belt_strategy) + belt_strategy->apply_to_trafo(params_base.trafo, model_volumes, out_belt_min_z); } //BBS: 0.0025mm is safe enough to simplify the data to speed slicing up for high-resolution model. //Also has on influence on arc fitting which has default resolution 0.0125mm. @@ -968,11 +834,8 @@ void PrintObject::slice() // 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(); - bool has_preslice_remap = (int(pcfg.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || - int(pcfg.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || - int(pcfg.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ)); - double belt_surface_z = has_preslice_remap - ? belt_remapped_bbox(*this->model_object(), pcfg).min.z() : 0.; + 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; } @@ -1025,18 +888,6 @@ void PrintObject::slice() << " belt_shear_z_global=" << pcfg.belt_shear_z_global.value << " object=" << this->model_object()->name; if (pcfg.belt_printer.value) { - auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { - double angle_rad = Geometry::deg2rad(angle_deg); - double sin_a = std::sin(angle_rad); - double cos_a = std::cos(angle_rad); - switch (mode) { - case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; - case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; - case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; - case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; - default: return 0.; - } - }; Point inst_shift = this->instances().empty() ? Point(0, 0) : this->instances().front().shift - this->center_offset(); @@ -1058,7 +909,7 @@ void PrintObject::slice() // PrintObjects so the lowest-positioned object stays at Z=0. const auto &za = gaxes[2]; // Z row if (za.global && za.mode != BeltShearMode::None && za.from < 2) { - double factor = compute_shear_factor(za.mode, za.angle); + double factor = BeltTransformPipeline::compute_shear_factor(za.mode, za.angle); // The global Z offset accounts for the instance's position- // dependent shear contribution. m_belt_min_z is the minimum Z // of the mesh after pre_remap + shear + trafo_centered, which @@ -1066,12 +917,8 @@ void PrintObject::slice() // Subtract the belt surface's centered Z position so we get // only the shear-induced contribution (same correction as the // belt_floor_z_shift fix). - // Same pre-remap guard as belt_floor_z_shift above. - bool has_preslice_remap2 = (int(pcfg.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || - int(pcfg.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || - int(pcfg.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ)); - double belt_surface_z = has_preslice_remap2 - ? belt_remapped_bbox(*this->model_object(), this->print()->config()).min.z() : 0.; + double belt_surface_z = BeltTransformPipeline::has_preslice_remap(pcfg) + ? BeltTransformPipeline::remap_bbox(*this->model_object(), pcfg).min.z() : 0.; double shear_min_z = m_belt_min_z - belt_surface_z; Point phys = inst_shift; // already has center_offset subtracted double center_on_axis = (za.from == 0) ? unscale(phys.x()) : unscale(phys.y()); diff --git a/src/libslic3r/Support/BeltFloorContext.cpp b/src/libslic3r/Support/BeltFloorContext.cpp new file mode 100644 index 0000000000..d2ffcfd397 --- /dev/null +++ b/src/libslic3r/Support/BeltFloorContext.cpp @@ -0,0 +1,124 @@ +#include "BeltFloorContext.hpp" + +#include +#include + +namespace Slic3r { + +bool BeltFloorContext::init(const SlicingParameters &sp, const PrintConfig &pcfg) +{ + m_active = false; + m_shear_factor = sp.belt_floor_shear_factor; + m_from_axis = sp.belt_floor_from_axis; + m_z_shift = sp.belt_floor_z_shift; + m_floor_offset = pcfg.belt_support_floor_offset.value; + + if (std::abs(m_shear_factor) < EPSILON) + return false; + + m_active = true; + return true; +} + +bool BeltFloorContext::init_local(const SlicingParameters &sp, const PrintConfig &pcfg, + double global_z_offset) +{ + if (!init(sp, pcfg)) + return false; + // Local Z: subtract the global Z offset so polygon computation + // works in the object's local coordinate space. + m_z_shift -= global_z_offset; + return true; +} + +Polygons BeltFloorContext::surface_polygon(coordf_t print_z) const +{ + return half_plane(print_z, /*belt_surface=*/true); +} + +Polygons BeltFloorContext::valid_region_polygon(coordf_t print_z) const +{ + return half_plane(print_z, /*belt_surface=*/false); +} + +double BeltFloorContext::floor_print_z(const Point &pos_slicing) const +{ + if (!m_active) + return -std::numeric_limits::infinity(); + double pos = unscale(m_from_axis == 0 ? pos_slicing.x() : pos_slicing.y()); + return m_shear_factor * pos + m_floor_offset + m_z_shift; +} + +std::vector BeltFloorContext::compute_per_layer_floors( + size_t num_layers, + const std::function &layer_print_z) const +{ + std::vector result(num_layers); + if (!m_active) + return result; + for (size_t i = 0; i < num_layers; ++i) + result[i] = surface_polygon(layer_print_z(i)); + return result; +} + +Polygons BeltFloorContext::half_plane(coordf_t print_z, bool belt_surface) const +{ + if (!m_active) + return {}; + + const double cutoff = (print_z - m_z_shift - m_floor_offset) / m_shear_factor; + const coord_t cutoff_scaled = scale_(cutoff); + const coord_t large_bound = scale_(1e3); + + // The belt surface is on one side of the cutoff line; the valid region + // is on the other side. Which side depends on shear_factor sign. + // + // belt_surface=true → the belt side (where support should NOT exist) + // belt_surface=false → the valid side (where support IS allowed) + // + // For shear_factor > 0: belt surface is from_axis >= cutoff + // For shear_factor < 0: belt surface is from_axis <= cutoff + bool high_side = (m_shear_factor > 0) == belt_surface; + + Polygon poly; + if (m_from_axis == 0) { + if (high_side) { + // X >= cutoff + poly.points = { + Point(cutoff_scaled, -large_bound), + Point(large_bound, -large_bound), + Point(large_bound, large_bound), + Point(cutoff_scaled, large_bound) + }; + } else { + // X < cutoff + poly.points = { + Point(-large_bound, -large_bound), + Point(cutoff_scaled, -large_bound), + Point(cutoff_scaled, large_bound), + Point(-large_bound, large_bound) + }; + } + } else { + if (high_side) { + // Y >= cutoff + poly.points = { + Point(-large_bound, cutoff_scaled), + Point( large_bound, cutoff_scaled), + Point( large_bound, large_bound), + Point(-large_bound, large_bound) + }; + } else { + // Y < cutoff + poly.points = { + Point(-large_bound, -large_bound), + Point( large_bound, -large_bound), + Point( large_bound, cutoff_scaled), + Point(-large_bound, cutoff_scaled) + }; + } + } + return { poly }; +} + +} // namespace Slic3r diff --git a/src/libslic3r/Support/BeltFloorContext.hpp b/src/libslic3r/Support/BeltFloorContext.hpp new file mode 100644 index 0000000000..7b8b704ef6 --- /dev/null +++ b/src/libslic3r/Support/BeltFloorContext.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include "../libslic3r.h" +#include "../Point.hpp" +#include "../Polygon.hpp" +#include "../Slicing.hpp" +#include "../PrintConfig.hpp" + +namespace Slic3r { + +class PrintObject; + +// Belt floor context: encapsulates the parameters and polygon computation +// for belt printer floor clipping in support generation. +// +// All belt floor code across SupportMaterial, TreeSupport, TreeSupport3D, +// and TreeModelVolumes uses the same 4 parameters and the same 4-case +// polygon construction. This class consolidates that logic. +// +// Construct once per PrintObject, then call surface_polygon() or +// valid_region_polygon() per layer with the layer's print_z. +class BeltFloorContext +{ +public: + BeltFloorContext() = default; + + // Initialize from slicing parameters and print config. + // Uses global Z coordinates (for SupportMaterial, non-organic TreeSupport). + bool init(const SlicingParameters &sp, const PrintConfig &pcfg); + + // Initialize with a Z offset subtracted from z_shift. + // Uses local Z coordinates (for TreeSupport3D, TreeModelVolumes organic pipeline). + bool init_local(const SlicingParameters &sp, const PrintConfig &pcfg, + double global_z_offset); + + bool is_active() const { return m_active; } + + // Compute the belt-side half-plane polygon at a given print_z. + // This is the region where the belt surface exists. + Polygons surface_polygon(coordf_t print_z) const; + + // Compute the valid-region half-plane polygon at a given print_z. + // This is the complement: the region where support is allowed. + Polygons valid_region_polygon(coordf_t print_z) const; + + // Compute the belt floor Z position at a given XY position (in slicing coords). + // Returns -infinity if not active. + double floor_print_z(const Point &pos_slicing) const; + + // Pre-compute belt floor polygons for a range of layers. + // layer_print_z(i) returns the print_z for layer index i. + std::vector compute_per_layer_floors( + size_t num_layers, + const std::function &layer_print_z) const; + + // Accessors + double shear_factor() const { return m_shear_factor; } + int from_axis() const { return m_from_axis; } + double z_shift() const { return m_z_shift; } + double floor_offset() const { return m_floor_offset; } + +private: + bool m_active = false; + double m_shear_factor = 0.0; + int m_from_axis = 1; // 0=X, 1=Y + double m_z_shift = 0.0; + double m_floor_offset = 0.0; + + // Internal: compute the raw half-plane polygon. + // If belt_surface=true, returns the belt side; otherwise the valid (complement) side. + Polygons half_plane(coordf_t print_z, bool belt_surface) const; +}; + +} // namespace Slic3r diff --git a/src/libslic3r/Support/SupportMaterial.cpp b/src/libslic3r/Support/SupportMaterial.cpp index d70153a7dd..c1f732bc6d 100644 --- a/src/libslic3r/Support/SupportMaterial.cpp +++ b/src/libslic3r/Support/SupportMaterial.cpp @@ -5,6 +5,7 @@ #include "Print.hpp" #include "SupportMaterial.hpp" #include "SupportCommon.hpp" +#include "BeltFloorContext.hpp" #include "Geometry.hpp" #include "Point.hpp" #include "MutablePolygon.hpp" @@ -625,64 +626,10 @@ Polygons belt_floor_surface_polygon( const PrintObject &object, coordf_t print_z) { - const double shear_factor = slicing_params.belt_floor_shear_factor; - if (std::abs(shear_factor) < EPSILON) + BeltFloorContext ctx; + if (!ctx.init(slicing_params, print_config)) return {}; - - const int from_axis = slicing_params.belt_floor_from_axis; // 0=X, 1=Y - const double floor_offset = print_config.belt_support_floor_offset.value; - - // Belt floor line in slicing coordinates: Z = sf * Y + z_shift. - // z_shift accounts for the upward shift applied when post-shear geometry - // extends below the bed (overhangs). Solving for Y: - // cutoff = (print_z - z_shift - floor_offset) / sf - const double z_shift = slicing_params.belt_floor_z_shift; - const double cutoff = (print_z - z_shift - floor_offset) / shear_factor; - const coord_t cutoff_scaled = scale_(cutoff); - const coord_t large_bound = scale_(1e4); - - // Build the belt-side half-plane (inverted from the valid region). - // If shear_factor > 0: valid region is from_axis < cutoff, so belt surface is from_axis >= cutoff. - // If shear_factor < 0: valid region is from_axis > cutoff, so belt surface is from_axis <= cutoff. - Polygon belt_poly; - if (from_axis == 0) { - if (shear_factor > 0) { - // Belt surface: X >= cutoff - belt_poly.points = { - Point(cutoff_scaled, -large_bound), - Point(large_bound, -large_bound), - Point(large_bound, large_bound), - Point(cutoff_scaled, large_bound) - }; - } else { - // Belt surface: X <= cutoff - belt_poly.points = { - Point(-large_bound, -large_bound), - Point(cutoff_scaled, -large_bound), - Point(cutoff_scaled, large_bound), - Point(-large_bound, large_bound) - }; - } - } else { - if (shear_factor > 0) { - // Belt surface: Y >= cutoff - belt_poly.points = { - Point(-large_bound, cutoff_scaled), - Point( large_bound, cutoff_scaled), - Point( large_bound, large_bound), - Point(-large_bound, large_bound) - }; - } else { - // Belt surface: Y <= cutoff - belt_poly.points = { - Point(-large_bound, -large_bound), - Point( large_bound, -large_bound), - Point( large_bound, cutoff_scaled), - Point(-large_bound, cutoff_scaled) - }; - } - } - return { belt_poly }; + return ctx.surface_polygon(print_z); } // Belt printer: compute the valid-region half-plane polygon at a given print_z. @@ -694,58 +641,10 @@ static Polygons belt_floor_valid_region_polygon( const PrintObject &object, coordf_t print_z) { - const double shear_factor = slicing_params.belt_floor_shear_factor; - if (std::abs(shear_factor) < EPSILON) + BeltFloorContext ctx; + if (!ctx.init(slicing_params, print_config)) return {}; - - const int from_axis = slicing_params.belt_floor_from_axis; - const double floor_offset = print_config.belt_support_floor_offset.value; - - const double z_shift = slicing_params.belt_floor_z_shift; - const double cutoff = (print_z - z_shift - floor_offset) / shear_factor; - const coord_t cutoff_scaled = scale_(cutoff); - const coord_t large_bound = scale_(1e4); - - // Valid region: the complement of the belt surface polygon. - Polygon valid_poly; - if (from_axis == 0) { - if (shear_factor > 0) { - // Valid: X < cutoff - valid_poly.points = { - Point(-large_bound, -large_bound), - Point(cutoff_scaled, -large_bound), - Point(cutoff_scaled, large_bound), - Point(-large_bound, large_bound) - }; - } else { - // Valid: X > cutoff - valid_poly.points = { - Point(cutoff_scaled, -large_bound), - Point(large_bound, -large_bound), - Point(large_bound, large_bound), - Point(cutoff_scaled, large_bound) - }; - } - } else { - if (shear_factor > 0) { - // Valid: Y < cutoff - valid_poly.points = { - Point(-large_bound, -large_bound), - Point( large_bound, -large_bound), - Point( large_bound, cutoff_scaled), - Point(-large_bound, cutoff_scaled) - }; - } else { - // Valid: Y > cutoff - valid_poly.points = { - Point(-large_bound, cutoff_scaled), - Point( large_bound, cutoff_scaled), - Point( large_bound, large_bound), - Point(-large_bound, large_bound) - }; - } - } - return { valid_poly }; + return ctx.valid_region_polygon(print_z); } // Collect outer contours of all slices of this layer. @@ -3373,22 +3272,18 @@ static void trim_support_layers_by_belt_floor( const PrintObject &object, SupportGeneratorLayersPtr &support_layers) { - if (std::abs(slicing_params.belt_floor_shear_factor) < EPSILON) + 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(0, support_layers.size()), [&](const tbb::blocked_range &range) { - for (size_t i = range.begin(); i < range.end(); ++ i) { - SupportGeneratorLayer *layer = support_layers[i]; - if (layer->polygons.empty()) - continue; - Polygons belt_surface = belt_floor_surface_polygon( - slicing_params, print_config, object, layer->print_z); - if (! belt_surface.empty()) - layer->polygons = diff(layer->polygons, belt_surface); - } + for (size_t i = range.begin(); i < range.end(); ++i) + if (support_layers[i]) + support_layers[i]->polygons = diff(support_layers[i]->polygons, + ctx.surface_polygon(support_layers[i]->print_z)); }); } diff --git a/src/libslic3r/Support/TreeModelVolumes.cpp b/src/libslic3r/Support/TreeModelVolumes.cpp index ec38ffed73..ebc3d2c958 100644 --- a/src/libslic3r/Support/TreeModelVolumes.cpp +++ b/src/libslic3r/Support/TreeModelVolumes.cpp @@ -8,6 +8,7 @@ #include "TreeModelVolumes.hpp" #include "TreeSupportCommon.hpp" +#include "BeltFloorContext.hpp" #include "../BuildVolume.hpp" #include "../ClipperUtils.hpp" @@ -101,13 +102,11 @@ TreeModelVolumes::TreeModelVolumes( { const auto &sp = print_object.slicing_parameters(); const auto &pcfg = print_object.print()->config(); - const double sf = sp.belt_floor_shear_factor; - if (std::abs(sf) > EPSILON + 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) { - const int from_axis = sp.belt_floor_from_axis; - const double floor_off = pcfg.belt_support_floor_offset.value; - const double z_shift = sp.belt_floor_z_shift - print_object.belt_global_z_offset(); size_t num_layers_needed = print_object.layer_count(); // Ensure m_anti_overhang is large enough. if (m_anti_overhang.size() < num_layers_needed) @@ -115,22 +114,7 @@ TreeModelVolumes::TreeModelVolumes( for (size_t layer_idx = 0; layer_idx < num_layers_needed; ++layer_idx) { double print_z = print_object.get_layer(layer_idx)->print_z - print_object.belt_global_z_offset(); - double cutoff = (print_z - z_shift - floor_off) / sf; - coord_t cutoff_sc = scale_(cutoff); - coord_t big = scale_(1e4); - Polygon belt_poly; - if (from_axis == 0) { - if (sf > 0) - belt_poly.points = {{cutoff_sc,-big},{big,-big},{big,big},{cutoff_sc,big}}; - else - belt_poly.points = {{-big,-big},{cutoff_sc,-big},{cutoff_sc,big},{-big,big}}; - } else { - if (sf > 0) - belt_poly.points = {{-big,cutoff_sc},{big,cutoff_sc},{big,big},{-big,big}}; - else - belt_poly.points = {{-big,-big},{big,-big},{big,cutoff_sc},{-big,cutoff_sc}}; - } - append(m_anti_overhang[layer_idx], Polygons{belt_poly}); + append(m_anti_overhang[layer_idx], ctx.surface_polygon(print_z)); } } } @@ -180,40 +164,20 @@ TreeModelVolumes::TreeModelVolumes( // Branches grow toward the belt and their slices are clipped at the belt // surface in organic_draw_branches(). The organic pipeline works in LOCAL // Z (no global_z_offset), so use local z_shift and local print_z. - const auto &slicing_params = print_object.slicing_parameters(); - const auto &pcfg = print_object.print()->config(); - const double sf = slicing_params.belt_floor_shear_factor; - if (std::abs(sf) > EPSILON - && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { - const int from_axis = slicing_params.belt_floor_from_axis; - const double floor_off = pcfg.belt_support_floor_offset.value; - // Subtract global_z_offset to get the LOCAL z_shift — the organic - // pipeline's Z coordinates don't include the global offset. - const double z_shift = slicing_params.belt_floor_z_shift - - print_object.belt_global_z_offset(); - m_belt_floor.assign(num_layers, Polygons{}); - for (size_t layer_idx = 0; layer_idx < num_layers; ++layer_idx) { - // Use local print_z (subtract global offset from object layer). - double print_z = (layer_idx >= num_raft_layers) - ? print_object.get_layer(layer_idx - num_raft_layers)->print_z - - print_object.belt_global_z_offset() - : 0.; - double cutoff = (print_z - z_shift - floor_off) / sf; - coord_t cutoff_sc = scale_(cutoff); - coord_t big = scale_(1e4); - Polygon belt_poly; - if (from_axis == 0) { - if (sf > 0) - belt_poly.points = {{cutoff_sc,-big},{big,-big},{big,big},{cutoff_sc,big}}; - else - belt_poly.points = {{-big,-big},{cutoff_sc,-big},{cutoff_sc,big},{-big,big}}; - } else { - if (sf > 0) - belt_poly.points = {{-big,cutoff_sc},{big,cutoff_sc},{big,big},{-big,big}}; - else - belt_poly.points = {{-big,-big},{big,-big},{big,cutoff_sc},{-big,cutoff_sc}}; - } - m_belt_floor[layer_idx] = { belt_poly }; + { + const auto &slicing_params = print_object.slicing_parameters(); + 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) { + m_belt_floor = ctx.compute_per_layer_floors(num_layers, [&](size_t layer_idx) -> double { + // Use local print_z (subtract global offset from object layer). + return (layer_idx >= num_raft_layers) + ? print_object.get_layer(layer_idx - num_raft_layers)->print_z + - print_object.belt_global_z_offset() + : 0.; + }); } } } diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 60e681add9..6c359115da 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -14,6 +14,7 @@ #include "TreeSupportCommon.hpp" #include "TreeSupport.hpp" #include "TreeSupport3D.hpp" +#include "BeltFloorContext.hpp" #include #include @@ -639,16 +640,10 @@ TreeSupport::TreeSupport(PrintObject& object, const SlicingParameters &slicing_p double TreeSupport::belt_floor_print_z(const Point &pos_slicing) const { - double sf = m_slicing_params.belt_floor_shear_factor; - if (std::abs(sf) < EPSILON) - return -std::numeric_limits::max(); // no belt floor - int from = m_slicing_params.belt_floor_from_axis; - // Belt floor in slicing coords: Z = sf * Y + z_shift + floor_offset. - // Inverse of cutoff = (Z - z_shift - floor_offset) / sf. - double pos = unscale(from == 0 ? pos_slicing.x() : pos_slicing.y()); - double floor_offset = m_print_config->belt_support_floor_offset.value; - double z_shift = m_slicing_params.belt_floor_z_shift; - return sf * pos + floor_offset + z_shift; + BeltFloorContext ctx; + if (!ctx.init(m_slicing_params, *m_print_config)) + return -std::numeric_limits::infinity(); + return ctx.floor_print_z(pos_slicing); } #define SUPPORT_SURFACES_OFFSET_PARAMETERS ClipperLib::jtSquare, 0. @@ -1738,18 +1733,11 @@ void TreeSupport::generate() // layer and clipped at the belt surface. These layers bypass the tree // algorithm entirely — they're pure geometry added after draw_circles(). { - const auto &sp = m_slicing_params; - const auto &pcfg = *m_print_config; - const double sf = sp.belt_floor_shear_factor; - if (std::abs(sf) > EPSILON - && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly + BeltFloorContext ctx; + if (ctx.init(m_slicing_params, *m_print_config) + && m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly && m_object->support_layer_count() > 0) { - const int from_axis = sp.belt_floor_from_axis; - const double floor_off = pcfg.belt_support_floor_offset.value; - // Support layer print_z values are in GLOBAL Z (non-organic inherits - // from object layers which include global_z_offset). Use the GLOBAL - // belt_floor_z_shift to match. - const double z_shift = sp.belt_floor_z_shift; + const auto &sp = m_slicing_params; // Find the lowest non-empty, non-brim support layer. ExPolygons source_areas; double source_z = 0; @@ -1778,7 +1766,7 @@ void TreeSupport::generate() } if (!source_areas.empty()) { BoundingBoxf3 bb = belt_remapped_bbox(*m_object->model_object(), m_object->print()->config()); - double from_extent = std::abs(bb.min(from_axis)); + double from_extent = std::abs(bb.min(ctx.from_axis())); double bb_min_z = std::abs(bb.min.z()); double first_z = m_object->get_support_layer(0)->print_z; // Depth = from-axis extent + pre-shear bbox Z offset (ensure_on_bed @@ -1792,18 +1780,8 @@ void TreeSupport::generate() 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; - double cutoff = (print_z - z_shift - floor_off) / sf; - coord_t cutoff_sc = scale_(cutoff); - coord_t big = scale_(1e3); - Polygon belt_poly; - if (from_axis == 0) { - if (sf > 0) belt_poly.points = {{cutoff_sc,-big},{big,-big},{big,big},{cutoff_sc,big}}; - else belt_poly.points = {{-big,-big},{cutoff_sc,-big},{cutoff_sc,big},{-big,big}}; - } else { - if (sf > 0) belt_poly.points = {{-big,cutoff_sc},{big,cutoff_sc},{big,big},{-big,big}}; - else belt_poly.points = {{-big,-big},{big,-big},{big,cutoff_sc},{-big,cutoff_sc}}; - } - ExPolygons clipped = diff_ex(source_areas, Polygons{belt_poly}); + 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; @@ -2250,37 +2228,19 @@ void TreeSupport::draw_circles() // Belt floor: clip tree support polygons by the belt surface plane. // ts_layer->print_z is at LOCAL Z (global offset applied later in - // _generate_support_material), but belt_floor_z_shift includes - // global_z_offset — subtract it to get the cutoff in local coords. - if (std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON - && m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { - const double sf = m_slicing_params.belt_floor_shear_factor; - const int from_axis = m_slicing_params.belt_floor_from_axis; - const double floor_off = m_print_config->belt_support_floor_offset.value; - const double z_shift_local = m_slicing_params.belt_floor_z_shift - - m_object->belt_global_z_offset(); - const double cutoff = (ts_layer->print_z - z_shift_local - floor_off) / sf; - const coord_t cutoff_sc = scale_(cutoff); - const coord_t big = scale_(1e4); - - Polygon belt_poly; - if (from_axis == 0) { - if (sf > 0) - belt_poly.points = { {cutoff_sc,-big}, {big,-big}, {big,big}, {cutoff_sc,big} }; - else - belt_poly.points = { {-big,-big}, {cutoff_sc,-big}, {cutoff_sc,big}, {-big,big} }; - } else { - if (sf > 0) - belt_poly.points = { {-big,cutoff_sc}, {big,cutoff_sc}, {big,big}, {-big,big} }; - else - belt_poly.points = { {-big,-big}, {big,-big}, {big,cutoff_sc}, {-big,cutoff_sc} }; + // _generate_support_material), so use init_local to subtract + // belt_global_z_offset from z_shift. + { + BeltFloorContext ctx; + if (ctx.init_local(m_slicing_params, *m_print_config, m_object->belt_global_z_offset()) + && m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + 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); + roof_1st_layer = diff_ex(roof_1st_layer, belt_surface); + floor_areas = diff_ex(floor_areas, belt_surface); + roof_gap_areas = diff_ex(roof_gap_areas, belt_surface); } - Polygons belt_surface = { belt_poly }; - base_areas = diff_ex(base_areas, belt_surface); - roof_areas = diff_ex(roof_areas, belt_surface); - roof_1st_layer = diff_ex(roof_1st_layer, belt_surface); - floor_areas = diff_ex(floor_areas, belt_surface); - roof_gap_areas = diff_ex(roof_gap_areas, belt_surface); } if (SQUARE_SUPPORT) { @@ -3593,6 +3553,20 @@ TreeSupportData::TreeSupportData(const PrintObject &object, coordf_t xy_distance poly.simplify(scale_(m_radius_sample_resolution), &outline); } + // Belt floor: add belt surface polygon to layer outlines so the + // collision system treats the belt as a physical surface. + { + 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) { + Polygons belt_surface = ctx.surface_polygon(local_print_z); + for (auto &p : belt_surface) + outline.emplace_back(ExPolygon(p)); + } + } + if (layer_nr == 0) m_layer_outlines_below.push_back(outline); else diff --git a/src/libslic3r/Support/TreeSupport3D.cpp b/src/libslic3r/Support/TreeSupport3D.cpp index 9d35cb2dd8..8a07609930 100644 --- a/src/libslic3r/Support/TreeSupport3D.cpp +++ b/src/libslic3r/Support/TreeSupport3D.cpp @@ -19,6 +19,7 @@ #include "Polygon.hpp" #include "Polyline.hpp" #include "MutablePolygon.hpp" +#include "BeltFloorContext.hpp" #include "SupportCommon.hpp" #include "TriangleMeshSlicer.hpp" #include "TreeSupport.hpp" @@ -3373,8 +3374,9 @@ static void generate_support_areas(Print &print, TreeSupport* tree_support, cons PrintObject &po = *print.get_object(processing.second.front()); const auto &sp = po.slicing_parameters(); const auto &pcfg = po.print()->config(); - const double sf = sp.belt_floor_shear_factor; - if (std::abs(sf) > EPSILON && std::abs(po.belt_global_z_offset()) > EPSILON + 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 @@ -3598,32 +3600,16 @@ static void generate_support_areas(Print &print, TreeSupport* tree_support, cons // Compute the belt floor polygon directly from each layer's print_z // rather than mapping to a layer index (avoids index mismatch issues). { - const auto &sp = print_object.slicing_parameters(); - const double sf = sp.belt_floor_shear_factor; - const double z_shift = sp.belt_floor_z_shift - print_object.belt_global_z_offset(); - const double floor_off = print_object.print()->config().belt_support_floor_offset.value; - const int from_axis = sp.belt_floor_from_axis; - if (std::abs(sf) > EPSILON - && print_object.print()->config().belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + 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() + && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { tbb::parallel_for_each(layers_sorted.begin(), layers_sorted.end(), [&](SupportGeneratorLayer *layer) { if (!layer || layer->polygons.empty()) return; - double cutoff = (layer->print_z - z_shift - floor_off) / sf; - coord_t cutoff_sc = scale_(cutoff); - coord_t big = scale_(1e4); - Polygon belt_poly; - if (from_axis == 0) { - if (sf > 0) - belt_poly.points = {{cutoff_sc,-big},{big,-big},{big,big},{cutoff_sc,big}}; - else - belt_poly.points = {{-big,-big},{cutoff_sc,-big},{cutoff_sc,big},{-big,big}}; - } else { - if (sf > 0) - belt_poly.points = {{-big,cutoff_sc},{big,cutoff_sc},{big,big},{-big,big}}; - else - belt_poly.points = {{-big,-big},{big,-big},{big,cutoff_sc},{-big,cutoff_sc}}; - } - layer->polygons = diff(layer->polygons, Polygons{belt_poly}); + layer->polygons = diff(layer->polygons, ctx.surface_polygon(layer->print_z)); }); } } From 44eebdb8ad0d0869c1aa4a58b5a27425ae46d81f Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 28 Mar 2026 02:13:15 -0500 Subject: [PATCH 008/224] Part 3.2: decouple axis remapping, enable viewing settings in Developer mode or when Belt mode is active --- src/libslic3r/BeltGCode.cpp | 14 ++---- src/libslic3r/BeltGCodeWriter.cpp | 25 ++-------- src/libslic3r/BeltGCodeWriter.hpp | 8 +--- src/libslic3r/BeltSliceStrategy.cpp | 15 ++---- src/libslic3r/BeltSliceStrategy.hpp | 10 ++-- src/libslic3r/BeltTransform.cpp | 18 ++++---- src/libslic3r/BeltTransform.hpp | 14 +++--- src/libslic3r/GCode.cpp | 13 ++++++ src/libslic3r/GCode/BeltBackTransform.cpp | 2 +- src/libslic3r/GCode/BeltBackTransform.hpp | 2 +- src/libslic3r/GCode/GCodeProcessor.cpp | 34 +++++++------- src/libslic3r/GCode/GCodeProcessor.hpp | 12 ++--- src/libslic3r/GCodeWriter.cpp | 56 +++++++++++++++++++++-- src/libslic3r/GCodeWriter.hpp | 15 ++++++ src/libslic3r/Preset.cpp | 4 +- src/libslic3r/Print.cpp | 12 ++--- src/libslic3r/PrintConfig.cpp | 52 ++++++++++----------- src/libslic3r/PrintConfig.hpp | 18 ++++---- src/libslic3r/PrintObjectSlice.cpp | 24 +++++++++- src/slic3r/GUI/Tab.cpp | 21 +++++---- 20 files changed, 215 insertions(+), 154 deletions(-) diff --git a/src/libslic3r/BeltGCode.cpp b/src/libslic3r/BeltGCode.cpp index dc1b6ca439..40dabf98dc 100644 --- a/src/libslic3r/BeltGCode.cpp +++ b/src/libslic3r/BeltGCode.cpp @@ -15,13 +15,7 @@ void BeltGCode::init_belt_writer(Print &print, bool is_bbl_printers) auto belt_writer = std::make_unique(); belt_writer->set_is_bbl_machine(is_bbl_printers); belt_writer->set_belt_angle(print.config().belt_printer_angle.value); - belt_writer->set_axis_remap( - int(print.config().belt_gcode_remap_x.value), - int(print.config().belt_gcode_remap_y.value), - int(print.config().belt_gcode_remap_z.value)); - BoundingBoxf bbox_bed(print.config().printable_area.values); - belt_writer->set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(), - print.config().printable_height.value)); + // Axis remap and build volume max are set by base GCode after init_belt_writer returns. belt_writer->set_belt_back_transform(print.config()); m_writer = std::move(belt_writer); @@ -59,9 +53,9 @@ void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print) file.write_format("; belt_scale_z = %s\n", full_cfg.opt_serialize("belt_scale_z").c_str()); file.write_format("; belt_scale_z_angle = %.1f\n", print.config().belt_scale_z_angle.value); // Pre-slice remap configs - file.write_format("; belt_preslice_remap_x = %s\n", full_cfg.opt_serialize("belt_preslice_remap_x").c_str()); - file.write_format("; belt_preslice_remap_y = %s\n", full_cfg.opt_serialize("belt_preslice_remap_y").c_str()); - file.write_format("; belt_preslice_remap_z = %s\n", full_cfg.opt_serialize("belt_preslice_remap_z").c_str()); + file.write_format("; preslice_remap_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()); } void BeltGCode::on_set_origin(const PrintObject *obj, const Point &inst_shift) diff --git a/src/libslic3r/BeltGCodeWriter.cpp b/src/libslic3r/BeltGCodeWriter.cpp index 645bf85a5b..db2bc46fe1 100644 --- a/src/libslic3r/BeltGCodeWriter.cpp +++ b/src/libslic3r/BeltGCodeWriter.cpp @@ -10,18 +10,6 @@ void BeltGCodeWriter::set_belt_angle(double angle_deg) m_belt_angle_rad = Geometry::deg2rad(angle_deg); } -void BeltGCodeWriter::set_axis_remap(int rx, int ry, int rz) -{ - m_remap_x = rx; - m_remap_y = ry; - m_remap_z = rz; -} - -void BeltGCodeWriter::set_build_volume_max(const Vec3d &max) -{ - m_build_vol_max = max; -} - void BeltGCodeWriter::set_belt_back_transform(const PrintConfig &config) { m_belt_back_transform.init_from_config(config); @@ -40,16 +28,9 @@ Vec3d BeltGCodeWriter::to_machine_coords(const Vec3d &pos) const { // Step 1: Undo the shear/scale applied during slicing. Vec3d p = m_belt_back_transform.apply(pos); - // Step 2: Apply axis remapping for the machine's coordinate convention. - // BeltRemapAxis: 0-2 = +X/+Y/+Z, 3-5 = -X/-Y/-Z, 6-8 = Rev X/Y/Z - auto remap = [this, &p](int r) -> double { - int axis = r % 3; - if (r < 3) return p[axis]; - if (r < 6) return -p[axis]; - return m_build_vol_max[axis] - p[axis]; - }; - Vec3d result = { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) }; - // Per-axis origin snap: shift so bbox min on each enabled axis = offset. + // Step 2: Apply axis remap (uses inherited base class method). + Vec3d result = apply_axis_remap(p); + // Step 3: Per-axis origin snap. for (int i = 0; i < 3; ++i) if (m_origin_snap[i]) result[i] -= (m_origin_bbox_min[i] - m_origin_offset[i]); diff --git a/src/libslic3r/BeltGCodeWriter.hpp b/src/libslic3r/BeltGCodeWriter.hpp index 727135b578..a41f890503 100644 --- a/src/libslic3r/BeltGCodeWriter.hpp +++ b/src/libslic3r/BeltGCodeWriter.hpp @@ -15,11 +15,9 @@ class BeltGCodeWriter : public GCodeWriter public: BeltGCodeWriter() : GCodeWriter() {} - // Belt configuration + // Belt configuration (axis remap is inherited from GCodeWriter) void set_belt_angle(double angle_deg); bool is_belt_printer() const { return m_belt_angle_rad != 0.; } - void set_axis_remap(int rx, int ry, int rz); - void set_build_volume_max(const Vec3d &max); void set_belt_back_transform(const PrintConfig &config); void set_origin_snap(int axis, bool enable, double offset, double bbox_min); Vec3d to_machine_coords(const Vec3d &pos) const; @@ -37,10 +35,6 @@ protected: private: double m_belt_angle_rad = 0.; - int m_remap_x = 0; - int m_remap_y = 1; - int m_remap_z = 2; - Vec3d m_build_vol_max = Vec3d::Zero(); BeltBackTransform m_belt_back_transform; bool m_origin_snap[3] = {false, false, false}; double m_origin_offset[3] = {0., 0., 0.}; diff --git a/src/libslic3r/BeltSliceStrategy.cpp b/src/libslic3r/BeltSliceStrategy.cpp index b3adeec64a..c1a300869e 100644 --- a/src/libslic3r/BeltSliceStrategy.cpp +++ b/src/libslic3r/BeltSliceStrategy.cpp @@ -13,31 +13,24 @@ std::unique_ptr BeltSliceStrategy::create(const PrintConfig & BeltSliceStrategy::BeltSliceStrategy(const PrintConfig &config) { - m_has_remap = BeltTransformPipeline::has_preslice_remap(config); - if (m_has_remap) - m_pre_remap = BeltTransformPipeline::build_preslice_remap(config); - m_shear = BeltTransformPipeline::build_shear_matrix(config, &m_has_shear); m_scale = BeltTransformPipeline::build_scale_matrix(config, &m_has_scale); } void BeltSliceStrategy::apply_to_trafo(Transform3d &trafo, const ModelVolumePtrs &model_volumes, + bool has_remap, double *out_belt_min_z) const { - // Step 1: Pre-slice axis remap. - if (m_has_remap) - trafo = m_pre_remap * trafo; - - // Step 2: Shear + scale. + // Shear + scale (belt-only; pre-slice remap is handled separately). if (m_has_shear || m_has_scale) { Transform3d belt_xform = Transform3d::Identity(); belt_xform.linear() = m_scale * m_shear; trafo = belt_xform * trafo; } - // Step 3: Z-shift — detect if mesh clips below build plate after transforms. - if (m_has_remap || m_has_shear || m_has_scale) { + // Z-shift — detect if mesh clips below build plate after transforms. + if (has_remap || m_has_shear || m_has_scale) { double min_z = std::numeric_limits::max(); for (const ModelVolume *mv : model_volumes) { if (!mv->is_model_part()) continue; diff --git a/src/libslic3r/BeltSliceStrategy.hpp b/src/libslic3r/BeltSliceStrategy.hpp index bf19243fd2..ef6e038ee7 100644 --- a/src/libslic3r/BeltSliceStrategy.hpp +++ b/src/libslic3r/BeltSliceStrategy.hpp @@ -23,21 +23,19 @@ public: // Create a strategy if belt_printer is enabled; returns nullptr otherwise. static std::unique_ptr create(const PrintConfig &config); - // Apply belt transforms to the slicing trafo. - // Modifies trafo in-place: trafo = z_shift * scale * shear * pre_remap * trafo - // Scans all model_part volumes to detect minimum Z and add z-shift if needed. - // Sets *out_belt_min_z to the minimum Z of the mesh after transforms. + // Apply belt-specific transforms (shear + scale + z-shift) to the slicing trafo. + // Pre-slice remap is handled separately (standalone feature). + // has_remap: whether pre-slice remap was already applied (affects z-shift detection). void apply_to_trafo(Transform3d &trafo, const ModelVolumePtrs &model_volumes, + bool has_remap, double *out_belt_min_z) const; private: explicit BeltSliceStrategy(const PrintConfig &config); - bool m_has_remap = false; bool m_has_shear = false; bool m_has_scale = false; - Transform3d m_pre_remap = Transform3d::Identity(); Matrix3d m_shear = Matrix3d::Identity(); Matrix3d m_scale = Matrix3d::Identity(); }; diff --git a/src/libslic3r/BeltTransform.cpp b/src/libslic3r/BeltTransform.cpp index db7eea7d80..9bb66fcfb6 100644 --- a/src/libslic3r/BeltTransform.cpp +++ b/src/libslic3r/BeltTransform.cpp @@ -13,9 +13,9 @@ Transform3d BeltTransformPipeline::build_preslice_remap(const PrintConfig &confi if (!has_preslice_remap(config)) return pre_remap; - int pre_rx = int(config.belt_preslice_remap_x.value); - int pre_ry = int(config.belt_preslice_remap_y.value); - int pre_rz = int(config.belt_preslice_remap_z.value); + 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 { @@ -114,13 +114,13 @@ Transform3d BeltTransformPipeline::build_forward_transform(const PrintConfig &co BoundingBoxf3 BeltTransformPipeline::remap_bbox(const BoundingBoxf3 &bb, const PrintConfig &config) { - int pre_rx = int(config.belt_preslice_remap_x.value); - int pre_ry = int(config.belt_preslice_remap_y.value); - int pre_rz = int(config.belt_preslice_remap_z.value); + 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); - if (pre_rx == int(BeltRemapAxis::PosX) && - pre_ry == int(BeltRemapAxis::PosY) && - pre_rz == int(BeltRemapAxis::PosZ)) + if (pre_rx == int(RemapAxis::PosX) && + pre_ry == int(RemapAxis::PosY) && + pre_rz == int(RemapAxis::PosZ)) return bb; // Identity remap. auto remap_coord = [](int r, const Vec3d &v) -> double { diff --git a/src/libslic3r/BeltTransform.hpp b/src/libslic3r/BeltTransform.hpp index b9a60473e5..3c0665c252 100644 --- a/src/libslic3r/BeltTransform.hpp +++ b/src/libslic3r/BeltTransform.hpp @@ -58,21 +58,21 @@ public: static bool has_preslice_remap(const PrintConfig &config) { - return int(config.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || - int(config.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || - int(config.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ); + return 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) { auto get_int = [&](const char *key) -> int { - auto *opt = config.option>(key); + auto *opt = config.option>(key); return opt ? int(opt->value) : 0; }; - return get_int("belt_preslice_remap_x") != int(BeltRemapAxis::PosX) || - get_int("belt_preslice_remap_y") != int(BeltRemapAxis::PosY) || - get_int("belt_preslice_remap_z") != int(BeltRemapAxis::PosZ); + 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_shear(const PrintConfig &config) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index c8b6e540cf..514efc4b52 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2425,6 +2425,19 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Belt printer: initialize belt-specific writer via virtual hook. this->init_belt_writer(print, is_bbl_printers); + // Standalone axis remap (works with or without belt mode). + { + int rx = int(print.config().gcode_remap_x.value); + int ry = int(print.config().gcode_remap_y.value); + int rz = int(print.config().gcode_remap_z.value); + if (rx != 0 || ry != 1 || rz != 2) { + m_writer->set_axis_remap(rx, ry, rz); + BoundingBoxf bbox_bed(print.config().printable_area.values); + m_writer->set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(), + print.config().printable_height.value)); + } + } + // How many times will be change_layer() called? // change_layer() in turn increments the progress bar status. m_layer_count = 0; diff --git a/src/libslic3r/GCode/BeltBackTransform.cpp b/src/libslic3r/GCode/BeltBackTransform.cpp index 2e2cd26f75..b9cdac779f 100644 --- a/src/libslic3r/GCode/BeltBackTransform.cpp +++ b/src/libslic3r/GCode/BeltBackTransform.cpp @@ -8,7 +8,7 @@ bool BeltBackTransform::init_from_config(const PrintConfig &config) m_active = false; m_inverse = Transform3d::Identity(); - if (!config.belt_printer.value || !config.belt_gcode_back_transform.value) + if (!config.belt_printer.value || !config.gcode_back_transform.value) return false; // Require at least one active transform to proceed. diff --git a/src/libslic3r/GCode/BeltBackTransform.hpp b/src/libslic3r/GCode/BeltBackTransform.hpp index 5a75d94dc8..b98770c688 100644 --- a/src/libslic3r/GCode/BeltBackTransform.hpp +++ b/src/libslic3r/GCode/BeltBackTransform.hpp @@ -15,7 +15,7 @@ namespace Slic3r { // Initialized once from PrintConfig, then applied per-point in // GCodeWriter::to_machine_coords() before axis remapping. // -// Active when belt_gcode_back_transform is true AND at least one of: +// Active when gcode_back_transform is true AND at least one of: // - a shear axis has global mode enabled, or // - a pre-slice axis remap is non-identity. class BeltBackTransform diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 4c630bb6de..5e1bad0b69 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -3129,26 +3129,26 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers try { m_result.belt_scale_z_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; } // Pre-slice axis remap - auto parse_remap_axis = [](const std::string &s) -> BeltRemapAxis { - if (s == "pos_x") return BeltRemapAxis::PosX; - if (s == "pos_y") return BeltRemapAxis::PosY; - if (s == "pos_z") return BeltRemapAxis::PosZ; - if (s == "neg_x") return BeltRemapAxis::NegX; - if (s == "neg_y") return BeltRemapAxis::NegY; - if (s == "neg_z") return BeltRemapAxis::NegZ; - if (s == "rev_x") return BeltRemapAxis::RevX; - if (s == "rev_y") return BeltRemapAxis::RevY; - if (s == "rev_z") return BeltRemapAxis::RevZ; - return BeltRemapAxis::PosX; + 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, " belt_preslice_remap_x = ")) { - m_result.belt_preslice_remap_x = parse_remap_axis(trim(std::string(comment.substr(25)))); return; + if (boost::starts_with(comment, " preslice_remap_x = ")) { + m_result.preslice_remap_x = parse_remap_axis(trim(std::string(comment.substr(25)))); return; } - if (boost::starts_with(comment, " belt_preslice_remap_y = ")) { - m_result.belt_preslice_remap_y = parse_remap_axis(trim(std::string(comment.substr(25)))); return; + if (boost::starts_with(comment, " preslice_remap_y = ")) { + m_result.preslice_remap_y = parse_remap_axis(trim(std::string(comment.substr(25)))); return; } - if (boost::starts_with(comment, " belt_preslice_remap_z = ")) { - m_result.belt_preslice_remap_z = parse_remap_axis(trim(std::string(comment.substr(25)))); return; + if (boost::starts_with(comment, " preslice_remap_z = ")) { + m_result.preslice_remap_z = parse_remap_axis(trim(std::string(comment.substr(25)))); return; } } // wipe start tag diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index 7d08a9cdfd..4c6e4f6618 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -246,9 +246,9 @@ class Print; float belt_scale_y_angle{ 45.f }; BeltScaleMode belt_scale_z{ BeltScaleMode::None }; float belt_scale_z_angle{ 45.f }; - BeltRemapAxis belt_preslice_remap_x{ BeltRemapAxis::PosX }; - BeltRemapAxis belt_preslice_remap_y{ BeltRemapAxis::PosY }; - BeltRemapAxis belt_preslice_remap_z{ BeltRemapAxis::PosZ }; + 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; @@ -324,9 +324,9 @@ class Print; belt_scale_y_angle = other.belt_scale_y_angle; belt_scale_z = other.belt_scale_z; belt_scale_z_angle = other.belt_scale_z_angle; - belt_preslice_remap_x = other.belt_preslice_remap_x; - belt_preslice_remap_y = other.belt_preslice_remap_y; - belt_preslice_remap_z = other.belt_preslice_remap_z; + preslice_remap_x = other.preslice_remap_x; + preslice_remap_y = other.preslice_remap_y; + preslice_remap_z = other.preslice_remap_z; #if ENABLE_GCODE_VIEWER_STATISTICS time = other.time; #endif diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index fea5509bc3..9b78ef1ea5 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -20,6 +20,36 @@ namespace Slic3r { bool GCodeWriter::full_gcode_comment = true; +void GCodeWriter::set_axis_remap(int rx, int ry, int rz) +{ + m_remap_x = rx; + m_remap_y = ry; + m_remap_z = rz; +} + +void GCodeWriter::set_build_volume_max(const Vec3d &max) +{ + m_build_vol_max = max; +} + +bool GCodeWriter::has_axis_remap() const +{ + return m_remap_x != 0 || m_remap_y != 1 || m_remap_z != 2; +} + +Vec3d GCodeWriter::apply_axis_remap(const Vec3d &pos) const +{ + if (!has_axis_remap()) + return pos; + auto remap = [this, &pos](int r) -> double { + int axis = r % 3; + if (r < 3) return pos[axis]; + if (r < 6) return -pos[axis]; + return m_build_vol_max[axis] - pos[axis]; + }; + return { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) }; +} + bool GCodeWriter::supports_separate_travel_acceleration(GCodeFlavor flavor) { return (flavor == gcfRepetier || flavor == gcfMarlinFirmware || flavor == gcfRepRapFirmware); @@ -548,7 +578,13 @@ std::string GCodeWriter::travel_to_xy(const Vec2d &point, const std::string &com Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; GCodeG1Formatter w; - w.emit_xy(point_on_plate); + if (has_axis_remap()) { + // Axis remap may couple XY with Z; emit full XYZ in machine coordinates. + Vec3d machine = apply_axis_remap(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); + w.emit_xyz(machine); + } else { + w.emit_xy(point_on_plate); + } auto speed = m_is_first_layer ? this->config.get_abs_value("initial_layer_travel_speed") : this->config.travel_speed.value; w.emit_f(speed * 60.0); @@ -780,7 +816,13 @@ std::string GCodeWriter::_travel_to_z(double z, const std::string &comment) } GCodeG1Formatter w; - w.emit_z(z); + if (has_axis_remap()) { + // Remap may couple Z with other axes; emit full XYZ. + Vec3d machine = apply_axis_remap(Vec3d(m_pos.x() - m_x_offset, m_pos.y() - m_y_offset, z)); + w.emit_xyz(machine); + } else { + w.emit_z(z); + } w.emit_f(speed * 60.0); //BBS w.emit_comment(GCodeWriter::full_gcode_comment, comment); @@ -885,7 +927,12 @@ std::string GCodeWriter::extrude_to_xy(const Vec2d &point, double dE, const std: Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; GCodeG1Formatter w; - w.emit_xy(point_on_plate); + if (has_axis_remap()) { + Vec3d machine = apply_axis_remap(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); + w.emit_xyz(machine); + } else { + w.emit_xy(point_on_plate); + } if (!force_no_extrusion) w.emit_e(filament()->E()); //BBS @@ -925,6 +972,9 @@ std::string GCodeWriter::extrude_to_xyz(const Vec3d &point, double dE, const std //BBS: take plate offset into consider Vec3d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset, point(2) }; + if (has_axis_remap()) + point_on_plate = apply_axis_remap(point_on_plate); + GCodeG1Formatter w; w.emit_xyz(point_on_plate); if (!force_no_extrusion) diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index c96c3ca2c1..6aa2ee8ffd 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -125,6 +125,12 @@ public: void set_is_first_layer(bool bval) { m_is_first_layer = bval; } GCodeFlavor get_gcode_flavor() const { return config.gcode_flavor; } + // Axis remap: permute/negate/reverse axes in G-code output. + // Works standalone (without belt mode) for printers with non-standard axis conventions. + void set_axis_remap(int rx, int ry, int rz); + void set_build_volume_max(const Vec3d &max); + bool has_axis_remap() const; + // Returns whether this flavor supports separate print and travel acceleration. static bool supports_separate_travel_acceleration(GCodeFlavor flavor); protected: @@ -141,6 +147,15 @@ protected: virtual std::string _travel_to_z(double z, const std::string &comment); + // Axis remap state — accessible to subclasses. + int m_remap_x = 0; // RemapAxis: 0=+X, 1=+Y, 2=+Z, 3=-X, etc. + int m_remap_y = 1; + int m_remap_z = 2; + Vec3d m_build_vol_max = Vec3d::Zero(); + + // Apply axis remap to a point. Returns pos unchanged if remap is identity. + Vec3d apply_axis_remap(const Vec3d &pos) const; + private: // Extruders are sorted by their ID, so that binary search is possible. std::vector m_filament_extruders; diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 9cc5e03d40..cd3af59dae 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1014,8 +1014,8 @@ static std::vector s_Preset_printer_options { "belt_shear_y", "belt_shear_y_angle", "belt_shear_y_from", "belt_shear_y_global", "belt_shear_z", "belt_shear_z_angle", "belt_shear_z_from", "belt_shear_z_global", "belt_scale_x", "belt_scale_x_angle", "belt_scale_y", "belt_scale_y_angle", "belt_scale_z", "belt_scale_z_angle", - "belt_preslice_remap_x", "belt_preslice_remap_y", "belt_preslice_remap_z", - "belt_gcode_remap_x", "belt_gcode_remap_y", "belt_gcode_remap_z", "belt_gcode_back_transform", + "preslice_remap_x", "preslice_remap_y", "preslice_remap_z", + "gcode_remap_x", "gcode_remap_y", "gcode_remap_z", "gcode_back_transform", "belt_origin_snap_x", "belt_origin_offset_x", "belt_origin_snap_y", "belt_origin_offset_y", "belt_origin_snap_z", "belt_origin_offset_z", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 1f787a70cf..aca40e4952 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -101,9 +101,9 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n // or they are only notes not influencing the generated G-code. static std::unordered_set steps_gcode = { // Belt printer G-code axis remap (only affects G-code output, not slicing). - "belt_gcode_remap_x", - "belt_gcode_remap_y", - "belt_gcode_remap_z", + "gcode_remap_x", + "gcode_remap_y", + "gcode_remap_z", "belt_origin_snap_x", "belt_origin_offset_x", "belt_origin_snap_y", "belt_origin_offset_y", "belt_origin_snap_z", "belt_origin_offset_z", @@ -308,9 +308,9 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "belt_scale_y_angle" || opt_key == "belt_scale_z" || opt_key == "belt_scale_z_angle" - || opt_key == "belt_preslice_remap_x" - || opt_key == "belt_preslice_remap_y" - || opt_key == "belt_preslice_remap_z") { + || opt_key == "preslice_remap_x" + || opt_key == "preslice_remap_y" + || opt_key == "preslice_remap_z") { osteps.emplace_back(posSlice); } else if ( opt_key == "belt_support_floor_offset" diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 506bd17a29..a1dbcf8845 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -314,18 +314,18 @@ static t_config_enum_values s_keys_map_BeltScaleMode { }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltScaleMode) -static t_config_enum_values s_keys_map_BeltRemapAxis { - { "pos_x", int(BeltRemapAxis::PosX) }, - { "pos_y", int(BeltRemapAxis::PosY) }, - { "pos_z", int(BeltRemapAxis::PosZ) }, - { "neg_x", int(BeltRemapAxis::NegX) }, - { "neg_y", int(BeltRemapAxis::NegY) }, - { "neg_z", int(BeltRemapAxis::NegZ) }, - { "rev_x", int(BeltRemapAxis::RevX) }, - { "rev_y", int(BeltRemapAxis::RevY) }, - { "rev_z", int(BeltRemapAxis::RevZ) }, +static t_config_enum_values s_keys_map_RemapAxis { + { "pos_x", int(RemapAxis::PosX) }, + { "pos_y", int(RemapAxis::PosY) }, + { "pos_z", int(RemapAxis::PosZ) }, + { "neg_x", int(RemapAxis::NegX) }, + { "neg_y", int(RemapAxis::NegY) }, + { "neg_z", int(RemapAxis::NegZ) }, + { "rev_x", int(RemapAxis::RevX) }, + { "rev_y", int(RemapAxis::RevY) }, + { "rev_z", int(RemapAxis::RevZ) }, }; -CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltRemapAxis) +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(RemapAxis) static t_config_enum_values s_keys_map_BeltSupportFloorMode { { "none", int(BeltSupportFloorMode::None) }, @@ -6120,49 +6120,49 @@ void PrintConfigDef::init_fff_params() add_belt_scale_angle("belt_scale_z_angle", "Angle"); // G-code axis remap with sign - auto add_belt_remap = [this](const char *key, const char *label, const char *tooltip, BeltRemapAxis default_axis) { + auto add_belt_remap = [this](const char *key, const char *label, const char *tooltip, RemapAxis default_axis) { auto def = this->add(key, coEnum); def->label = L(label); def->category = L("Printable space"); def->tooltip = L(tooltip); - def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); def->enum_values = {"pos_x", "pos_y", "pos_z", "neg_x", "neg_y", "neg_z", "rev_x", "rev_y", "rev_z"}; def->enum_labels = {L("+X"), L("+Y"), L("+Z"), L("-X"), L("-Y"), L("-Z"), L("Rev X"), L("Rev Y"), L("Rev Z")}; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionEnum(default_axis)); + def->mode = comSimple; // Visibility controlled by toggle_line in Tab.cpp + def->set_default_value(new ConfigOptionEnum(default_axis)); }; - add_belt_remap("belt_preslice_remap_x", "X", + add_belt_remap("preslice_remap_x", "X", "Before slicing, which model-space axis becomes the slicer's X axis. " "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.", - BeltRemapAxis::PosX); - add_belt_remap("belt_preslice_remap_y", "Y", + RemapAxis::PosX); + 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.", - BeltRemapAxis::PosY); - add_belt_remap("belt_preslice_remap_z", "Z", + RemapAxis::PosY); + 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.", - BeltRemapAxis::PosZ); + RemapAxis::PosZ); - add_belt_remap("belt_gcode_remap_x", "X", "Which slicing axis maps to machine X in G-code output. Applied AFTER slicing, during G-code generation.", BeltRemapAxis::PosX); - add_belt_remap("belt_gcode_remap_y", "Y", "Which slicing axis maps to machine Y in G-code output. Applied AFTER slicing, during G-code generation.", BeltRemapAxis::PosY); - add_belt_remap("belt_gcode_remap_z", "Z", "Which slicing axis maps to machine Z in G-code output. Applied AFTER slicing, during G-code generation.", BeltRemapAxis::PosZ); + add_belt_remap("gcode_remap_x", "X", "Which slicing axis maps to machine X in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosX); + add_belt_remap("gcode_remap_y", "Y", "Which slicing axis maps to machine Y in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosY); + add_belt_remap("gcode_remap_z", "Z", "Which slicing axis maps to machine Z in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosZ); - def = this->add("belt_gcode_back_transform", coBool); + def = this->add("gcode_back_transform", coBool); def->label = L("G-code back-transform"); def->category = L("Printable space"); def->tooltip = L("Reverse the shear/scale transform applied during slicing so G-code " "coordinates are in the machine's physical coordinate space. " "Requires at least one shear axis with global mode enabled."); - def->mode = comAdvanced; + def->mode = comSimple; // Visibility controlled by toggle_line in Tab.cpp def->set_default_value(new ConfigOptionBool(false)); auto add_belt_origin_snap = [this](const char *key_snap, const char *key_offset, diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 8b469c4dc6..459e9eb26f 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -182,7 +182,7 @@ enum class BeltAxis Z = 2, }; -enum class BeltRemapAxis +enum class RemapAxis { PosX = 0, PosY = 1, PosZ = 2, NegX = 3, NegY = 4, NegZ = 5, @@ -550,7 +550,7 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SlicingMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltShearMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltScaleMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltAxis) -CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltRemapAxis) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(RemapAxis) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltSupportFloorMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltSupportZOffsetMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialPattern) @@ -1488,13 +1488,13 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionFloat, belt_scale_y_angle)) ((ConfigOptionEnum, belt_scale_z)) ((ConfigOptionFloat, belt_scale_z_angle)) - ((ConfigOptionEnum, belt_preslice_remap_x)) - ((ConfigOptionEnum, belt_preslice_remap_y)) - ((ConfigOptionEnum, belt_preslice_remap_z)) - ((ConfigOptionEnum, belt_gcode_remap_x)) - ((ConfigOptionEnum, belt_gcode_remap_y)) - ((ConfigOptionEnum, belt_gcode_remap_z)) - ((ConfigOptionBool, belt_gcode_back_transform)) + ((ConfigOptionEnum, preslice_remap_x)) + ((ConfigOptionEnum, preslice_remap_y)) + ((ConfigOptionEnum, preslice_remap_z)) + ((ConfigOptionEnum, gcode_remap_x)) + ((ConfigOptionEnum, gcode_remap_y)) + ((ConfigOptionEnum, gcode_remap_z)) + ((ConfigOptionBool, gcode_back_transform)) ((ConfigOptionBool, belt_origin_snap_x)) ((ConfigOptionFloat, belt_origin_offset_x)) ((ConfigOptionBool, belt_origin_snap_y)) diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 3067772f09..7279b83d14 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -144,11 +144,31 @@ static std::vector slice_volumes_inner( params_base.closing_radius = print_object_config.slice_closing_radius.value; params_base.extra_offset = 0; params_base.trafo = object_trafo; + // Standalone pre-slice axis remap (works without belt mode). + bool has_remap = BeltTransformPipeline::has_preslice_remap(print_config); + if (has_remap) + params_base.trafo = BeltTransformPipeline::build_preslice_remap(print_config) * params_base.trafo; { - // Belt printer: apply pre-slice transforms (remap, shear, scale, z-shift) via strategy. + // Belt-specific transforms (shear, scale, z-shift) via strategy. auto belt_strategy = BeltSliceStrategy::create(print_config); if (belt_strategy) - belt_strategy->apply_to_trafo(params_base.trafo, model_volumes, out_belt_min_z); + belt_strategy->apply_to_trafo(params_base.trafo, model_volumes, has_remap, out_belt_min_z); + } + // If only remap (no belt), still need z-shift detection. + if (has_remap && !print_config.belt_printer.value) { + double min_z = std::numeric_limits::max(); + for (const ModelVolume *mv : model_volumes) { + if (!mv->is_model_part()) continue; + for (const stl_vertex &v : mv->mesh().its.vertices) { + Vec3d pt = params_base.trafo * v.cast(); + min_z = std::min(min_z, pt.z()); + } + } + if (min_z < 0. && min_z != std::numeric_limits::max()) { + Transform3d z_shift = Transform3d::Identity(); + z_shift.matrix()(2, 3) = -min_z; + params_base.trafo = z_shift * params_base.trafo; + } } //BBS: 0.0025mm is safe enough to simplify the data to speed slicing up for high-resolution model. //Also has on influence on arc fitting which has default resolution 0.0125mm. diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index d5a6b38ebc..6202185440 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4435,19 +4435,19 @@ void TabPrinter::build_fff() 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.append_option(optgroup->get_option("belt_preslice_remap_x")); - line.append_option(optgroup->get_option("belt_preslice_remap_y")); - line.append_option(optgroup->get_option("belt_preslice_remap_z")); + line.append_option(optgroup->get_option("preslice_remap_x")); + line.append_option(optgroup->get_option("preslice_remap_y")); + line.append_option(optgroup->get_option("preslice_remap_z")); optgroup->append_line(line); } { Line line = { L("G-code axis remap (post-slice)"), L("Remap slicing-frame axes to machine axes in G-code output. Applied AFTER slicing, during G-code generation.") }; - line.append_option(optgroup->get_option("belt_gcode_remap_x")); - line.append_option(optgroup->get_option("belt_gcode_remap_y")); - line.append_option(optgroup->get_option("belt_gcode_remap_z")); + line.append_option(optgroup->get_option("gcode_remap_x")); + line.append_option(optgroup->get_option("gcode_remap_y")); + line.append_option(optgroup->get_option("gcode_remap_z")); optgroup->append_line(line); } - optgroup->append_single_option_line("belt_gcode_back_transform"); + optgroup->append_single_option_line("gcode_back_transform"); { Line line = { L("Origin snap X"), L("Snap object bbox min X to offset in G-code output") }; line.append_option(optgroup->get_option("belt_origin_snap_x")); @@ -5320,11 +5320,14 @@ void TabPrinter::toggle_options() toggle_line("belt_printer_infinite_y", is_belt); for (auto el : {"belt_shear_x", "belt_shear_y", "belt_shear_z", "belt_scale_x", "belt_scale_y", "belt_scale_z", - "belt_preslice_remap_x", - "belt_gcode_remap_x", "belt_gcode_back_transform", "belt_origin_snap_x", "belt_origin_snap_y", "belt_origin_snap_z"}) toggle_line(el, is_belt); + // Remap options: visible when belt mode is on OR when UI is in developer mode. + bool show_remap = is_belt || (m_mode >= comDevelop); + for (auto el : {"preslice_remap_x", "gcode_remap_x", "gcode_back_transform"}) + toggle_line(el, show_remap); + // Gray out angle/from sub-options when their parent shear/scale mode is None. auto sx = m_config->option>("belt_shear_x")->value; toggle_option("belt_shear_x_angle", is_belt && sx != BeltShearMode::None); From 372139c770b9112d27c76c27e3253e29de2f5bb0 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 28 Mar 2026 04:03:39 -0500 Subject: [PATCH 009/224] revert CLAUDE.md --- CLAUDE.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9bd162c42c..25aa516a56 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,7 +23,27 @@ cmake --build build/arm64 --config RelWithDebInfo --target all -- ### Building on Linux **Always use this command to build the project when testing build issues on Linux.** ```bash -systemd-run --user --scope -p MemoryMax=48G cmake --build build --config RelWithDebInfo --target all -- -j18 -l 24 +cmake --build build/arm64 --config RelWithDebInfo --target all -- + +``` +### Build test: + +**Always use this command to build the project when testing build issues on Windows.** +```bash +cmake --build . --config %build_type% --target ALL_BUILD -- -m +``` + +### Building on macOS +**Always use this command to build the project when testing build issues on macOS.** +```bash +cmake --build build/arm64 --config RelWithDebInfo --target all -- +``` + +### Building on Linux + **Always use this command to build the project when testing build issues on Linux.** +```bash +cmake --build build --config RelWithDebInfo --target all -- + ``` From 4634a5dfd79e234439b244e43f528f3e33a1c9cb Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sun, 29 Mar 2026 13:14:25 -0500 Subject: [PATCH 010/224] switch default shear axis, swap to tan(a) instead of cot(a) --- src/libslic3r/PrintConfig.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index a1dbcf8845..0ae4173911 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -6062,13 +6062,13 @@ void PrintConfigDef::init_fff_params() def->set_default_value(new ConfigOptionEnum(default_axis)); }; - auto add_belt_shear_global = [this](const char *key, const char *label) { + auto add_belt_shear_global = [this](const char *key, const char *label, bool default_val = false) { auto def = this->add(key, coBool); def->label = L(label); def->category = L("Printable space"); def->tooltip = L("Apply shear in global coordinates (position-aware) rather than object-local coordinates."); def->mode = comAdvanced; - def->set_default_value(new ConfigOptionBool(false)); + def->set_default_value(new ConfigOptionBool(default_val)); }; add_belt_shear_mode("belt_shear_x", "Function", BeltShearMode::None); @@ -6076,15 +6076,15 @@ void PrintConfigDef::init_fff_params() add_belt_axis_enum("belt_shear_x_from", "From", "Source axis for X shear.", BeltAxis::Z); add_belt_shear_global("belt_shear_x_global", "Global"); - add_belt_shear_mode("belt_shear_y", "Function", BeltShearMode::PosCot); + add_belt_shear_mode("belt_shear_y", "Function", BeltShearMode::None); add_belt_shear_angle("belt_shear_y_angle", "Angle"); add_belt_axis_enum("belt_shear_y_from", "From", "Source axis for Y shear.", BeltAxis::Z); add_belt_shear_global("belt_shear_y_global", "Global"); - add_belt_shear_mode("belt_shear_z", "Function", BeltShearMode::None); + add_belt_shear_mode("belt_shear_z", "Function", BeltShearMode::PosTan); add_belt_shear_angle("belt_shear_z_angle", "Angle"); add_belt_axis_enum("belt_shear_z_from", "From", "Source axis for Z shear.", BeltAxis::Y); - add_belt_shear_global("belt_shear_z_global", "Global"); + add_belt_shear_global("belt_shear_z_global", "Global", true); // Per-axis scale controls for belt printer auto add_belt_scale_mode = [this](const char *key, const char *label, BeltScaleMode default_mode) { From 3bc13e5cfd91f2949bca14466423214c6c1ce111 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Tue, 7 Apr 2026 10:37:34 +0800 Subject: [PATCH 011/224] add a generic belt printer --- resources/profiles/Custom.json | 26 ++++- .../Custom/Generic Belt Printer_cover.png | Bin 0 -> 30205 bytes .../machine/MyBeltPrinter 0.2 nozzle.json | 26 +++++ .../machine/MyBeltPrinter 0.4 nozzle.json | 20 ++++ .../machine/MyBeltPrinter 0.6 nozzle.json | 26 +++++ .../machine/MyBeltPrinter 0.8 nozzle.json | 26 +++++ .../Custom/machine/MyBeltPrinter.json | 12 +++ .../Custom/machine/fdm_belt_common.json | 98 ++++++++++++++++++ 8 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 resources/profiles/Custom/Generic Belt Printer_cover.png create mode 100644 resources/profiles/Custom/machine/MyBeltPrinter 0.2 nozzle.json create mode 100644 resources/profiles/Custom/machine/MyBeltPrinter 0.4 nozzle.json create mode 100644 resources/profiles/Custom/machine/MyBeltPrinter 0.6 nozzle.json create mode 100644 resources/profiles/Custom/machine/MyBeltPrinter 0.8 nozzle.json create mode 100644 resources/profiles/Custom/machine/MyBeltPrinter.json create mode 100644 resources/profiles/Custom/machine/fdm_belt_common.json diff --git a/resources/profiles/Custom.json b/resources/profiles/Custom.json index 37927a90fe..4c64f0a205 100644 --- a/resources/profiles/Custom.json +++ b/resources/profiles/Custom.json @@ -1,9 +1,13 @@ { "name": "Custom Printer", - "version": "02.03.02.60", + "version": "02.03.02.61", "force_update": "0", "description": "My configurations", "machine_model_list": [ + { + "name": "Generic Belt Printer", + "sub_path": "machine/MyBeltPrinter.json" + }, { "name": "Generic Klipper Printer", "sub_path": "machine/MyKlipper.json" @@ -246,6 +250,10 @@ "name": "MyKlipper 0.8 nozzle", "sub_path": "machine/MyKlipper 0.8 nozzle.json" }, + { + "name": "fdm_belt_common", + "sub_path": "machine/fdm_belt_common.json" + }, { "name": "fdm_toolchanger_common", "sub_path": "machine/fdm_toolchanger_common.json" @@ -254,6 +262,22 @@ "name": "MyRRF 0.4 nozzle", "sub_path": "machine/MyRRF 0.4 nozzle.json" }, + { + "name": "MyBeltPrinter 0.2 nozzle", + "sub_path": "machine/MyBeltPrinter 0.2 nozzle.json" + }, + { + "name": "MyBeltPrinter 0.4 nozzle", + "sub_path": "machine/MyBeltPrinter 0.4 nozzle.json" + }, + { + "name": "MyBeltPrinter 0.6 nozzle", + "sub_path": "machine/MyBeltPrinter 0.6 nozzle.json" + }, + { + "name": "MyBeltPrinter 0.8 nozzle", + "sub_path": "machine/MyBeltPrinter 0.8 nozzle.json" + }, { "name": "MyToolChanger 0.2 nozzle", "sub_path": "machine/MyToolChanger 0.2 nozzle.json" diff --git a/resources/profiles/Custom/Generic Belt Printer_cover.png b/resources/profiles/Custom/Generic Belt Printer_cover.png new file mode 100644 index 0000000000000000000000000000000000000000..d42f2ef228b2cfcdb0d16a525b4fde4d68b3010d GIT binary patch literal 30205 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 Date: Wed, 11 Mar 2026 00:27:14 -0500 Subject: [PATCH 012/224] stage in changes from off-plate-gravity and remove stuff I didn't need --- resources/shaders/110/gouraud.fs | 1 + resources/shaders/110/gouraud.vs | 5 +- resources/shaders/110/mm_gouraud.fs | 3 +- resources/shaders/110/mm_gouraud.vs | 1 + resources/shaders/140/gouraud.fs | 1 + resources/shaders/140/gouraud.vs | 5 +- resources/shaders/140/mm_gouraud.fs | 3 +- resources/shaders/140/mm_gouraud.vs | 1 + src/libslic3r/ExPolygon.hpp | 5 ++ src/libslic3r/Preset.cpp | 2 +- src/libslic3r/PrintConfig.cpp | 24 ++++++++ src/libslic3r/PrintConfig.hpp | 3 + src/libslic3r/Support/SupportMaterial.cpp | 22 ++++++-- src/libslic3r/Support/TreeSupport.cpp | 18 +++++- src/libslic3r/Support/TreeSupport3D.cpp | 16 +++++- src/libslic3r/TriangleSelector.cpp | 12 ++-- src/libslic3r/TriangleSelector.hpp | 6 +- src/slic3r/GUI/3DBed.cpp | 59 ++++++++++++++++++++ src/slic3r/GUI/3DBed.hpp | 4 +- src/slic3r/GUI/3DScene.cpp | 18 +++++- src/slic3r/GUI/GUI_Factories.cpp | 4 +- src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp | 20 +++++-- src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp | 1 + src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp | 28 ++++++++-- src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp | 3 + src/slic3r/GUI/Tab.cpp | 2 + 26 files changed, 230 insertions(+), 37 deletions(-) diff --git a/resources/shaders/110/gouraud.fs b/resources/shaders/110/gouraud.fs index e602d6067d..12f4feca3e 100644 --- a/resources/shaders/110/gouraud.fs +++ b/resources/shaders/110/gouraud.fs @@ -26,6 +26,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform vec4 uniform_color; diff --git a/resources/shaders/110/gouraud.vs b/resources/shaders/110/gouraud.vs index ff3a190c79..e2de2b63e5 100644 --- a/resources/shaders/110/gouraud.vs +++ b/resources/shaders/110/gouraud.vs @@ -23,6 +23,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform mat4 view_model_matrix; @@ -71,8 +72,8 @@ void main() // Point in homogenous coordinates. world_pos = volume_world_matrix * vec4(v_position, 1.0); - // z component of normal vector in world coordinate used for slope shading - world_normal_z = slope.actived ? (normalize(slope.volume_world_normal_matrix * v_normal)).z : 0.0; + // dot product of world normal with up direction, used for slope shading + world_normal_z = slope.actived ? dot(normalize(slope.volume_world_normal_matrix * v_normal), slope.up_direction) : 0.0; gl_Position = projection_matrix * position; // Fill in the scalars for fragment shader clipping. Fragments with any of these components lower than zero are discarded. diff --git a/resources/shaders/110/mm_gouraud.fs b/resources/shaders/110/mm_gouraud.fs index 821af13f03..431c936e9c 100644 --- a/resources/shaders/110/mm_gouraud.fs +++ b/resources/shaders/110/mm_gouraud.fs @@ -37,6 +37,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform SlopeDetection slope; @@ -85,7 +86,7 @@ void main() color = LightBlue; alpha = 1.0; } - else if( transformed_normal.z < slope.normal_z - EPSILON) + else if( dot(transformed_normal, slope.up_direction) < slope.normal_z - EPSILON) { color = color * 0.5 + LightRed * 0.5; alpha = 1.0; diff --git a/resources/shaders/110/mm_gouraud.vs b/resources/shaders/110/mm_gouraud.vs index b0cea9cfd6..c54ae4bff5 100644 --- a/resources/shaders/110/mm_gouraud.vs +++ b/resources/shaders/110/mm_gouraud.vs @@ -24,6 +24,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform SlopeDetection slope; void main() diff --git a/resources/shaders/140/gouraud.fs b/resources/shaders/140/gouraud.fs index bbfb76f7a1..0d9fa021fe 100644 --- a/resources/shaders/140/gouraud.fs +++ b/resources/shaders/140/gouraud.fs @@ -26,6 +26,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform vec4 uniform_color; diff --git a/resources/shaders/140/gouraud.vs b/resources/shaders/140/gouraud.vs index 72dd4ff1bf..c69dcd11df 100644 --- a/resources/shaders/140/gouraud.vs +++ b/resources/shaders/140/gouraud.vs @@ -23,6 +23,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform mat4 view_model_matrix; @@ -71,8 +72,8 @@ void main() // Point in homogenous coordinates. world_pos = volume_world_matrix * vec4(v_position, 1.0); - // z component of normal vector in world coordinate used for slope shading - world_normal_z = slope.actived ? (normalize(slope.volume_world_normal_matrix * v_normal)).z : 0.0; + // dot product of world normal with up direction, used for slope shading + world_normal_z = slope.actived ? dot(normalize(slope.volume_world_normal_matrix * v_normal), slope.up_direction) : 0.0; gl_Position = projection_matrix * position; // Fill in the scalars for fragment shader clipping. Fragments with any of these components lower than zero are discarded. diff --git a/resources/shaders/140/mm_gouraud.fs b/resources/shaders/140/mm_gouraud.fs index c5fe86efc5..7c77d5565e 100644 --- a/resources/shaders/140/mm_gouraud.fs +++ b/resources/shaders/140/mm_gouraud.fs @@ -37,6 +37,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform SlopeDetection slope; @@ -87,7 +88,7 @@ void main() color = LightBlue; alpha = 1.0; } - else if( transformed_normal.z < slope.normal_z - EPSILON) + else if( dot(transformed_normal, slope.up_direction) < slope.normal_z - EPSILON) { color = color * 0.5 + LightRed * 0.5; alpha = 1.0; diff --git a/resources/shaders/140/mm_gouraud.vs b/resources/shaders/140/mm_gouraud.vs index b191e35fa8..191613f957 100644 --- a/resources/shaders/140/mm_gouraud.vs +++ b/resources/shaders/140/mm_gouraud.vs @@ -24,6 +24,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform SlopeDetection slope; void main() diff --git a/src/libslic3r/ExPolygon.hpp b/src/libslic3r/ExPolygon.hpp index 87a1146d41..7a51f70ef7 100644 --- a/src/libslic3r/ExPolygon.hpp +++ b/src/libslic3r/ExPolygon.hpp @@ -377,6 +377,11 @@ inline void translate(ExPolygons &expolys, const Point &p) { expoly.translate(p); } +inline void translate(Polygons &polys, const Point &p) { + for (Polygon &poly : polys) + poly.translate(p); +} + inline void polygons_append(Polygons &dst, const ExPolygon &src) { dst.reserve(dst.size() + src.holes.size() + 1); diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index f8001d2806..8317eca70a 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1010,7 +1010,7 @@ static std::vector s_Preset_machine_limits_options { static std::vector s_Preset_printer_options { "printer_technology", - "printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "gcode_flavor", + "printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "build_plate_tilt_x", "build_plate_tilt_y", "gcode_flavor", "fan_kickstart", "fan_speedup_time", "fan_speedup_overhangs", "single_extruder_multi_material", "manual_filament_change", "file_start_gcode", "machine_start_gcode", "machine_end_gcode", "before_layer_change_gcode", "printing_by_object_gcode", "layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode", "change_filament_gcode", "change_extrusion_role_gcode", "printer_model", "printer_variant", "printer_extruder_id", "printer_extruder_variant", "extruder_variant_list", "default_nozzle_volume_type", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index d4711a0da9..c9d2f588da 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -5922,6 +5922,30 @@ void PrintConfigDef::init_fff_params() def->mode = comSimple; def->set_default_value(new ConfigOptionFloatOrPercent(50., true)); + def = this->add("build_plate_tilt_x", coFloat); + def->label = L("Build plate tilt X"); + def->category = L("Support"); + def->tooltip = L("Tilt angle of the build plate along the X axis. " + "A positive value tilts the plate so the +X side is higher, shifting gravity toward -X and increasing overhangs on the +X side. " + "A negative value tilts the -X side higher. Set to 0 for no X-axis tilt."); + def->sidetext = u8"\u00B0"; + def->min = -45; + def->max = 45; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0.)); + + def = this->add("build_plate_tilt_y", coFloat); + def->label = L("Build plate tilt Y"); + def->category = L("Support"); + def->tooltip = L("Tilt angle of the build plate along the Y axis. " + "A positive value tilts the plate so the +Y side is higher, shifting gravity toward -Y and increasing overhangs on the +Y side. " + "A negative value tilts the -Y side higher. Set to 0 for no Y-axis tilt."); + def->sidetext = u8"\u00B0"; + def->min = -45; + def->max = 45; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0.)); + def = this->add("tree_support_branch_angle", coFloat); def->label = L("Tree support branch angle"); def->category = L("Support"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 7d40ef0706..6b9ad65185 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1409,6 +1409,9 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( PrintConfig, (MachineEnvelopeConfig, GCodeConfig), + // Build plate tilt for off-axis gravity support generation (printer-level setting). + ((ConfigOptionFloat, build_plate_tilt_x)) + ((ConfigOptionFloat, build_plate_tilt_y)) //BBS ((ConfigOptionInts, additional_cooling_fan_speed)) ((ConfigOptionBool, reduce_crossing_wall)) diff --git a/src/libslic3r/Support/SupportMaterial.cpp b/src/libslic3r/Support/SupportMaterial.cpp index d1fa5d8627..c230924ac8 100644 --- a/src/libslic3r/Support/SupportMaterial.cpp +++ b/src/libslic3r/Support/SupportMaterial.cpp @@ -1392,6 +1392,10 @@ static inline ExPolygons detect_overhangs( const double threshold_rad = Geometry::deg2rad(thresh_angle); const bool bridge_no_support = object_config.bridge_no_support.value; const coordf_t xy_expansion = scale_(object_config.support_expansion.value); + // Build plate tilt: compute per-layer XY shift for tilted gravity direction + const double tilt_x_rad = Geometry::deg2rad(print_config.build_plate_tilt_x.value); + const double tilt_y_rad = Geometry::deg2rad(print_config.build_plate_tilt_y.value); + const bool has_tilt = std::abs(tilt_x_rad) > EPSILON || std::abs(tilt_y_rad) > EPSILON; float lower_layer_offset = 0; if (layer_id == 0) @@ -1441,9 +1445,17 @@ static inline ExPolygons detect_overhangs( // Overhang polygons for this layer and region. Polygons diff_polygons; Polygons layerm_polygons = to_polygons(layerm->slices.surfaces); + // Apply build plate tilt: shift lower layer polygons to simulate tilted gravity + Polygons effective_lower = lower_layer_polygons; + if (has_tilt) { + const double lh = lower_layer.height; + Point tilt_shift(coord_t(scale_(lh * tan(tilt_y_rad))), + coord_t(scale_(lh * tan(tilt_x_rad)))); + translate(effective_lower, tilt_shift); + } if (lower_layer_offset == 0.f) { // Support everything. - diff_polygons = diff(layerm_polygons, lower_layer_polygons); + diff_polygons = diff(layerm_polygons, effective_lower); if (buildplate_only) { // Don't support overhangs above the top surfaces. // This step is done before the contact surface is calculated by growing the overhang region. @@ -1452,9 +1464,9 @@ static inline ExPolygons detect_overhangs( } else if (auto_normal_support) { // Get the regions needing a suport, collapse very tiny spots. //FIXME cache the lower layer offset if this layer has multiple regions. - diff_polygons = + diff_polygons = diff(layerm_polygons, - expand(lower_layer_polygons, lower_layer_offset, SUPPORT_SURFACES_OFFSET_PARAMETERS)); + expand(effective_lower, lower_layer_offset, SUPPORT_SURFACES_OFFSET_PARAMETERS)); if (buildplate_only && ! annotations.buildplate_covered[layer_id].empty()) { // Don't support overhangs above the top surfaces. // This step is done before the contact surface is calculated by growing the overhang region. @@ -1462,9 +1474,9 @@ static inline ExPolygons detect_overhangs( } if (! diff_polygons.empty()) { // Offset the support regions back to a full overhang, restrict them to the full overhang. - // This is done to increase size of the supporting columns below, as they are calculated by + // This is done to increase size of the supporting columns below, as they are calculated by // propagating these contact surfaces downwards. - diff_polygons = diff(intersection(expand(diff_polygons, lower_layer_offset, SUPPORT_SURFACES_OFFSET_PARAMETERS), layerm_polygons), lower_layer_polygons); + diff_polygons = diff(intersection(expand(diff_polygons, lower_layer_offset, SUPPORT_SURFACES_OFFSET_PARAMETERS), layerm_polygons), effective_lower); } //FIXME add user defined filtering here based on minimal area or minimum radius or whatever. diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 50272009cd..864a0eb0e0 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -671,6 +671,11 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) double thresh_angle = config.support_threshold_angle.value > EPSILON ? config.support_threshold_angle.value + 1 : 30; thresh_angle = std::min(thresh_angle, 89.); // should be smaller than 90 const double threshold_rad = Geometry::deg2rad(thresh_angle); + // Build plate tilt: compute per-layer XY shift for tilted gravity direction + const PrintConfig& print_cfg = m_object->print()->config(); + const double tilt_x_rad = Geometry::deg2rad(print_cfg.build_plate_tilt_x.value); + const double tilt_y_rad = Geometry::deg2rad(print_cfg.build_plate_tilt_y.value); + const bool has_tilt = std::abs(tilt_x_rad) > EPSILON || std::abs(tilt_y_rad) > EPSILON; // FIXME this is a fudge constant! double support_tree_tip_diameter = 0.8; auto enforcer_overhang_offset = scaled(support_tree_tip_diameter); @@ -813,8 +818,19 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) ExPolygons& curr_polys = layer->lslices_extrudable; ExPolygons& lower_polys = lower_layer->lslices_extrudable; + // Apply build plate tilt: shift lower layer polygons to simulate tilted gravity + ExPolygons shifted_lower; + if (has_tilt) { + shifted_lower = lower_polys; // copy + const double lh = lower_layer->height; + Point tilt_shift(coord_t(scale_(lh * tan(tilt_y_rad))), + coord_t(scale_(lh * tan(tilt_x_rad)))); + translate(shifted_lower, tilt_shift); + } + const ExPolygons &effective_lower = has_tilt ? shifted_lower : lower_polys; + // normal overhang - ExPolygons lower_layer_offseted = offset_ex(lower_polys, support_offset_scaled, SUPPORT_SURFACES_OFFSET_PARAMETERS); + ExPolygons lower_layer_offseted = offset_ex(effective_lower, support_offset_scaled, SUPPORT_SURFACES_OFFSET_PARAMETERS); overhangs_all_layers[layer_nr] = std::move(diff_ex(curr_polys, lower_layer_offseted)); double duration{ std::chrono::duration_cast(clock_::now() - t0).count() }; diff --git a/src/libslic3r/Support/TreeSupport3D.cpp b/src/libslic3r/Support/TreeSupport3D.cpp index 307e4d314d..2c24b933b2 100644 --- a/src/libslic3r/Support/TreeSupport3D.cpp +++ b/src/libslic3r/Support/TreeSupport3D.cpp @@ -208,6 +208,10 @@ static std::vector>> group_me const bool support_threshold_auto = support_threshold == 0; // +1 makes the threshold inclusive double tan_threshold = support_threshold_auto ? 0. : tan(M_PI * double(support_threshold + 1) / 180.); + // Build plate tilt: compute per-layer XY shift for tilted gravity direction + const double tilt_x_rad = Geometry::deg2rad(print_config.build_plate_tilt_x.value); + const double tilt_y_rad = Geometry::deg2rad(print_config.build_plate_tilt_y.value); + const bool has_tilt = std::abs(tilt_x_rad) > EPSILON || std::abs(tilt_y_rad) > EPSILON; //FIXME this is a fudge constant! auto enforcer_overhang_offset = scaled(config.tree_support_tip_diameter.value); const coordf_t radius_sample_resolution = g_config_tree_support_collision_resolution; @@ -229,7 +233,7 @@ static std::vector>> group_me size_t num_overhang_layers = support_auto ? num_object_layers : std::min(num_object_layers, std::max(size_t(support_enforce_layers), enforcers_layers.size())); tbb::parallel_for(tbb::blocked_range(1, num_overhang_layers), [&print_object, &config, &print_config, &enforcers_layers, &blockers_layers, - support_auto, support_enforce_layers, support_threshold_auto, tan_threshold, enforcer_overhang_offset, num_raft_layers, radius_sample_resolution, &throw_on_cancel, &out] + support_auto, support_enforce_layers, support_threshold_auto, tan_threshold, enforcer_overhang_offset, num_raft_layers, radius_sample_resolution, has_tilt, tilt_x_rad, tilt_y_rad, &throw_on_cancel, &out] (const tbb::blocked_range &range) { for (LayerIndex layer_id = range.begin(); layer_id < range.end(); ++ layer_id) { const Layer ¤t_layer = *print_object.get_layer(layer_id); @@ -253,7 +257,15 @@ static std::vector>> group_me lower_layer_offset = external_perimeter_width - float(scale_(config.support_threshold_overlap.get_abs_value(unscale_(external_perimeter_width)))); } else lower_layer_offset = scaled(lower_layer.height / tan_threshold); - Polygons lower_layer_offseted = offset(lower_layer.lslices_extrudable, lower_layer_offset); + // Apply build plate tilt: shift lower layer polygons to simulate tilted gravity + Polygons lower_src = to_polygons(lower_layer.lslices_extrudable); + if (has_tilt) { + const double lh = lower_layer.height; + Point tilt_shift(coord_t(scale_(lh * tan(tilt_y_rad))), + coord_t(scale_(lh * tan(tilt_x_rad)))); + translate(lower_src, tilt_shift); + } + Polygons lower_layer_offseted = offset(lower_src, lower_layer_offset); overhangs = diff(current_layer.lslices_extrudable, lower_layer_offseted); if (lower_layer_offset == 0) { raw_overhangs = overhangs; diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp index a6d19f505c..695ac4c2ba 100644 --- a/src/libslic3r/TriangleSelector.cpp +++ b/src/libslic3r/TriangleSelector.cpp @@ -233,7 +233,7 @@ int TriangleSelector::select_unsplit_triangle(const Vec3f &hit, int facet_idx) c return this->select_unsplit_triangle(hit, facet_idx, neighbors); } -void TriangleSelector::select_patch(int facet_start, std::unique_ptr &&cursor, EnforcerBlockerType new_state, const Transform3d& trafo_no_translate, bool triangle_splitting, float highlight_by_angle_deg) +void TriangleSelector::select_patch(int facet_start, std::unique_ptr &&cursor, EnforcerBlockerType new_state, const Transform3d& trafo_no_translate, bool triangle_splitting, float highlight_by_angle_deg, const Vec3f &up_direction) { assert(facet_start < m_orig_size_indices); @@ -294,8 +294,8 @@ void TriangleSelector::select_patch(int facet_start, std::unique_ptr &&c int facet = facets_to_check[facet_idx]; const Vec3f& facet_normal = m_face_normals[m_triangles[facet].source_triangle]; Matrix3f normal_matrix = static_cast(trafo_no_translate.matrix().block(0, 0, 3, 3).inverse().transpose().cast()); - float world_normal_z = (normal_matrix* facet_normal).normalized().z(); - if (!visited[facet] && (highlight_by_angle_deg == 0.f || world_normal_z < highlight_angle_limit)) { + float world_normal_dot = (normal_matrix * facet_normal).normalized().dot(up_direction); + if (!visited[facet] && (highlight_by_angle_deg == 0.f || world_normal_dot < highlight_angle_limit)) { if (select_triangle(facet, new_state, triangle_splitting)) { // add neighboring facets to list to be processed later for (int neighbor_idx : m_neighbors[facet]) @@ -320,7 +320,7 @@ bool TriangleSelector::is_facet_clipped(int facet_idx, const ClippingPlane &clp) void TriangleSelector::seed_fill_select_triangles(const Vec3f &hit, int facet_start, const Transform3d& trafo_no_translate, const ClippingPlane &clp, float seed_fill_angle, float highlight_by_angle_deg, - bool force_reselection) + bool force_reselection, const Vec3f &up_direction) { assert(facet_start < m_orig_size_indices); @@ -344,8 +344,8 @@ void TriangleSelector::seed_fill_select_triangles(const Vec3f &hit, int facet_st const Vec3f &facet_normal = m_face_normals[m_triangles[current_facet].source_triangle]; Matrix3f normal_matrix = static_cast(trafo_no_translate.matrix().block(0, 0, 3, 3).inverse().transpose().cast()); - float world_normal_z = (normal_matrix * facet_normal).normalized().z(); - if (!visited[current_facet] && (highlight_by_angle_deg == 0.f || world_normal_z < highlight_angle_limit)) { + float world_normal_dot = (normal_matrix * facet_normal).normalized().dot(up_direction); + if (!visited[current_facet] && (highlight_by_angle_deg == 0.f || world_normal_dot < highlight_angle_limit)) { if (m_triangles[current_facet].is_split()) { for (int split_triangle_idx = 0; split_triangle_idx <= m_triangles[current_facet].number_of_split_sides(); ++split_triangle_idx) { assert(split_triangle_idx < int(m_triangles[current_facet].children.size())); diff --git a/src/libslic3r/TriangleSelector.hpp b/src/libslic3r/TriangleSelector.hpp index 50bbdd4ed0..5b20647b84 100644 --- a/src/libslic3r/TriangleSelector.hpp +++ b/src/libslic3r/TriangleSelector.hpp @@ -308,7 +308,8 @@ public: EnforcerBlockerType new_state, // enforcer or blocker? const Transform3d &trafo_no_translate, // matrix to get from mesh to world without translation bool triangle_splitting, // If triangles will be split base on the cursor or not - float highlight_by_angle_deg = 0.f); // The maximal angle of overhang. If it is set to a non-zero value, it is possible to paint only the triangles of overhang defined by this angle in degrees. + float highlight_by_angle_deg = 0.f, // The maximal angle of overhang. If it is set to a non-zero value, it is possible to paint only the triangles of overhang defined by this angle in degrees. + const Vec3f &up_direction = Vec3f::UnitZ()); // Up direction for overhang detection (accounts for build plate tilt) void seed_fill_select_triangles(const Vec3f &hit, // point where to start int facet_start, // facet of the original mesh (unsplit) that the hit point belongs to @@ -316,7 +317,8 @@ public: const ClippingPlane &clp, // Clipping plane to limit painting to not clipped facets only float seed_fill_angle, // the maximal angle between two facets to be painted by the same color float highlight_by_angle_deg = 0.f, // The maximal angle of overhang. If it is set to a non-zero value, it is possible to paint only the triangles of overhang defined by this angle in degrees. - bool force_reselection = false); // force reselection of the triangle mesh even in cases that mouse is pointing on the selected triangle + bool force_reselection = false, // force reselection of the triangle mesh even in cases that mouse is pointing on the selected triangle + const Vec3f &up_direction = Vec3f::UnitZ()); // Up direction for overhang detection (accounts for build plate tilt) void bucket_fill_select_triangles(const Vec3f &hit, // point where to start int facet_start, // facet of the original mesh (unsplit) that the hit point belongs to diff --git a/src/slic3r/GUI/3DBed.cpp b/src/slic3r/GUI/3DBed.cpp index 544a3f8f12..3f5cd306ad 100644 --- a/src/slic3r/GUI/3DBed.cpp +++ b/src/slic3r/GUI/3DBed.cpp @@ -394,6 +394,8 @@ void Bed3D::render_internal(GLCanvas3D& canvas, const Transform3d& view_matrix, case Type::Custom: { render_custom(canvas, view_matrix, projection_matrix, bottom); break; } } + render_gravity_arrow(view_matrix, projection_matrix); + glsafe(::glDisable(GL_DEPTH_TEST)); } @@ -731,6 +733,63 @@ void Bed3D::render_custom(GLCanvas3D& canvas, const Transform3d& view_matrix, co render_texture(bottom, canvas);*/ } +void Bed3D::render_gravity_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix) +{ + const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + double tilt_x_deg = cfg.opt_float("build_plate_tilt_x"); + double tilt_y_deg = cfg.opt_float("build_plate_tilt_y"); + if (tilt_x_deg == 0. && tilt_y_deg == 0.) { + m_gravity_arrow.reset(); + return; + } + + // Gravity direction (matching the slicer's tilt convention) + double tilt_x_rad = Geometry::deg2rad(tilt_x_deg); + double tilt_y_rad = Geometry::deg2rad(tilt_y_deg); + Vec3d gravity_dir = Vec3d(-tan(tilt_y_rad), -tan(tilt_x_rad), -1.0).normalized(); + + // 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)); + } + + // 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)) + Vec3d from = Vec3d::UnitZ(); + Vec3d to = gravity_dir; + double dot = from.dot(to); + Transform3d rot = Transform3d::Identity(); + if (dot < -0.9999) { + // Nearly opposite — rotate 180° around X + rot = Eigen::AngleAxisd(M_PI, Vec3d::UnitX()) * rot; + } else if (dot < 0.9999) { + Vec3d axis = from.cross(to).normalized(); + double angle = std::acos(std::clamp(dot, -1.0, 1.0)); + rot = Eigen::AngleAxisd(angle, axis) * rot; + } + + GLShaderProgram* shader = wxGetApp().get_shader("flat"); + if (shader == nullptr) + return; + + glsafe(::glEnable(GL_DEPTH_TEST)); + shader->start_using(); + + const Camera& camera = wxGetApp().plater()->get_camera(); + Transform3d model_matrix = rot; + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * model_matrix); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + + m_gravity_arrow.set_color({ 1.0f, 0.85f, 0.0f, 1.0f }); // yellow + m_gravity_arrow.render(); + + shader->stop_using(); +} + void Bed3D::render_default(bool bottom, const Transform3d& view_matrix, const Transform3d& projection_matrix) { // m_texture.reset(); diff --git a/src/slic3r/GUI/3DBed.hpp b/src/slic3r/GUI/3DBed.hpp index b791635fd3..b16ead575b 100644 --- a/src/slic3r/GUI/3DBed.hpp +++ b/src/slic3r/GUI/3DBed.hpp @@ -110,6 +110,7 @@ private: //GLTexture m_temp_texture; GLModel m_model; Vec3d m_model_offset{ Vec3d::Zero() }; + GLModel m_gravity_arrow; Axes m_axes; float m_scale_factor{ 1.0f }; @@ -177,7 +178,8 @@ private: void render_model(const Transform3d& view_matrix, const Transform3d& projection_matrix); void render_custom(GLCanvas3D& canvas, const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom); void render_default(bool bottom, const Transform3d& view_matrix, const Transform3d& projection_matrix); - + void render_gravity_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix); + // BBS: remove the bed picking logic // void register_raycasters_for_picking(const GLModel::Geometry& geometry, const Transform3d& trafo); }; diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index 1a1f652e71..e36f053667 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -1078,13 +1078,27 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type, bool enable_support; int support_threshold_angle = get_selection_support_threshold_angle(enable_support); - + float normal_z = -::cos(Geometry::deg2rad((float) support_threshold_angle)); - + + // Compute up direction accounting for build plate tilt + Vec3f up_direction = Vec3f::UnitZ(); + { + const DynamicPrintConfig& prt_cfg = GUI::wxGetApp().preset_bundle->printers.get_edited_preset().config; + double tilt_x_deg = prt_cfg.opt_float("build_plate_tilt_x"); + double tilt_y_deg = prt_cfg.opt_float("build_plate_tilt_y"); + if (tilt_x_deg != 0. || tilt_y_deg != 0.) { + double tilt_x_rad = Geometry::deg2rad(tilt_x_deg); + double tilt_y_rad = Geometry::deg2rad(tilt_y_deg); + up_direction = Vec3f(float(tan(tilt_y_rad)), float(tan(tilt_x_rad)), 1.f).normalized(); + } + } + shader->set_uniform("volume_world_matrix", volume.first->world_matrix()); shader->set_uniform("slope.actived", m_slope.isGlobalActive && !volume.first->is_modifier && !volume.first->is_wipe_tower); shader->set_uniform("slope.volume_world_normal_matrix", static_cast(volume.first->world_matrix().matrix().block(0, 0, 3, 3).inverse().transpose().cast())); shader->set_uniform("slope.normal_z", normal_z); + shader->set_uniform("slope.up_direction", up_direction); #if ENABLE_ENVIRONMENT_MAP unsigned int environment_texture_id = GUI::wxGetApp().plater()->get_environment_texture_id(); diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index 5c3c0267ee..3cc39fe1ce 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -64,6 +64,7 @@ static SettingsFactory::Bundle FREQ_SETTINGS_BUNDLE_FFF = { L("Support") , { "enable_support", "support_type", "support_threshold_angle", "support_threshold_overlap", "support_base_pattern", "support_on_build_plate_only","support_critical_regions_only", "support_remove_small_overhang", + "build_plate_tilt_x", "build_plate_tilt_y", "support_base_pattern_spacing", "support_expansion"}}, //BBS { L("Flush options") , { "flush_into_infill", "flush_into_objects", "flush_into_support"} } @@ -94,7 +95,8 @@ std::map> SettingsFactory::OBJECT_C {"support_bottom_z_distance", "",19},{"support_top_z_distance", "",20},{"support_base_pattern", "",21},{"support_base_pattern_spacing", "",22}, {"support_interface_top_layers", "",23},{"support_interface_bottom_layers", "",24},{"support_interface_spacing", "",25},{"support_bottom_interface_spacing", "",26}, {"support_object_xy_distance", "",27}, {"bridge_no_support", "",28},{"max_bridge_length", "",29},{"support_critical_regions_only", "",30},{"support_remove_small_overhang","",31}, - {"support_object_first_layer_gap","",32} + {"build_plate_tilt_x","",32},{"build_plate_tilt_y","",33}, + {"support_object_first_layer_gap","",34} }}, { L("Speed"), {{"support_speed", "",12}, {"support_interface_speed", "",13} }} diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp index 02f1b4be5f..88781f6006 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp @@ -537,6 +537,14 @@ int GLGizmoFdmSupports::get_selection_support_threshold_angle() return auto_support ? support_threshold_angle : 0; } +std::pair GLGizmoFdmSupports::get_build_plate_tilt() +{ + const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + double tilt_x = cfg.opt_float("build_plate_tilt_x"); + double tilt_y = cfg.opt_float("build_plate_tilt_y"); + return {tilt_x, tilt_y}; +} + void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block) { float threshold = (float(M_PI)/180.f)*threshold_deg; @@ -544,6 +552,12 @@ void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block) const ModelObject* mo = m_c->selection_info()->model_object(); const ModelInstance* mi = mo->instances[selection.get_instance_idx()]; + // Compute gravity direction accounting for build plate tilt + auto [tilt_x_deg, tilt_y_deg] = get_build_plate_tilt(); + double tilt_x_rad = tilt_x_deg * M_PI / 180.0; + double tilt_y_rad = tilt_y_deg * M_PI / 180.0; + Vec3d gravity_dir = Vec3d(-tan(tilt_y_rad), -tan(tilt_x_rad), -1.0).normalized(); + int mesh_id = -1; for (const ModelVolume* mv : mo->volumes) { if (! mv->is_model_part()) @@ -552,10 +566,8 @@ void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block) ++mesh_id; const Transform3d trafo_matrix = mi->get_matrix_no_offset() * mv->get_matrix_no_offset(); - Vec3f down = (trafo_matrix.inverse() * (-Vec3d::UnitZ())).cast().normalized(); - Vec3f limit = (trafo_matrix.inverse() * Vec3d(std::sin(threshold), 0, -std::cos(threshold))).cast().normalized(); - - float dot_limit = limit.dot(down); + Vec3f down = (trafo_matrix.inverse() * gravity_dir).cast().normalized(); + float dot_limit = std::cos(threshold); // Now calculate dot product of vert_direction and facets' normals. int idx = 0; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp index ec0faeab80..0afdba7f19 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp @@ -59,6 +59,7 @@ private: void select_facets_by_angle(float threshold, bool block); // BBS int get_selection_support_threshold_angle(); + std::pair get_build_plate_tilt(); int m_support_threshold_angle = -1; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp index 7d3f40bd12..71e4c45f4a 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp @@ -73,6 +73,18 @@ GLGizmoPainterBase::ClippingPlaneDataWrapper GLGizmoPainterBase::get_clipping_pl return clp_data_out; } +Vec3f GLGizmoPainterBase::get_tilt_up_direction() const +{ + const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + double tilt_x_deg = cfg.opt_float("build_plate_tilt_x"); + double tilt_y_deg = cfg.opt_float("build_plate_tilt_y"); + if (tilt_x_deg == 0. && tilt_y_deg == 0.) + return Vec3f::UnitZ(); + double tilt_x_rad = Geometry::deg2rad(tilt_x_deg); + double tilt_y_rad = Geometry::deg2rad(tilt_y_deg); + return Vec3f(float(tan(tilt_y_rad)), float(tan(tilt_x_rad)), 1.f).normalized(); +} + void GLGizmoPainterBase::render_triangles(const Selection& selection) const { auto* shader = wxGetApp().get_shader("mm_gouraud"); @@ -119,11 +131,15 @@ void GLGizmoPainterBase::render_triangles(const Selection& selection) const float normal_z = -::cos(Geometry::deg2rad(m_highlight_by_angle_threshold_deg)); Matrix3f normal_matrix = static_cast(trafo_matrix.matrix().block(0, 0, 3, 3).inverse().transpose().cast()); + // Compute up direction accounting for build plate tilt + Vec3f up_direction = get_tilt_up_direction(); + shader->set_uniform("volume_world_matrix", trafo_matrix); shader->set_uniform("volume_mirrored", is_left_handed); shader->set_uniform("slope.actived", m_parent.is_using_slope()); shader->set_uniform("slope.volume_world_normal_matrix", normal_matrix); shader->set_uniform("slope.normal_z", normal_z); + shader->set_uniform("slope.up_direction", up_direction); m_triangle_selectors[mesh_id]->render(m_imgui, trafo_matrix); if (is_left_handed) @@ -691,7 +707,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous mi->get_assemble_transformation().get_matrix() * mo->volumes[m_rr.mesh_id]->get_matrix() : mi->get_transformation().get_matrix() * mo->volumes[m_rr.mesh_id]->get_matrix(); m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, int(m_rr.facet), trafo_matrix_not_translate, this->get_clipping_plane_in_volume_coordinates(trafo_matrix), m_smart_fill_angle, - m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, true); + m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, true, get_tilt_up_direction()); m_triangle_selectors[m_rr.mesh_id]->request_update_render_data(); m_seed_fill_last_mesh_id = m_rr.mesh_id; } @@ -803,7 +819,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous std::unique_ptr cursor = TriangleSelector::SinglePointCursor::cursor_factory(phr.z_world, camera_pos, m_cursor_height, trafo_matrix, clp); m_triangle_selectors[mesh_idx]->select_patch(int(phr.first_facet_idx), std::move(cursor), new_state, trafo_matrix_not_translate, - m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f); + m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, get_tilt_up_direction()); m_triangle_selectors[mesh_idx]->request_update_render_data(true); m_last_mouse_click = _mouse_position; @@ -855,7 +871,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous m_triangle_selectors[mesh_idx]->seed_fill_apply_on_triangles(new_state); if (m_tool_type == ToolType::SMART_FILL) m_triangle_selectors[mesh_idx]->seed_fill_select_triangles(mesh_hit, facet_idx, trafo_matrix_not_translate, clp, m_smart_fill_angle, - m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, true); + m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, true, get_tilt_up_direction()); else if (m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER) // BBS: add infill_angle parameter m_triangle_selectors[mesh_idx]->bucket_fill_select_triangles(mesh_hit, facet_idx, clp, -1.f, false, true); @@ -874,12 +890,12 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous camera_pos, m_cursor_radius, m_cursor_type, trafo_matrix, clp); m_triangle_selectors[mesh_idx]->select_patch(int(first_position.facet_idx), std::move(cursor), new_state, trafo_matrix_not_translate, - m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f); + m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, get_tilt_up_direction()); } else { for (auto first_position_it = projected_mouse_positions.cbegin(); first_position_it != projected_mouse_positions.cend() - 1; ++first_position_it) { auto second_position_it = first_position_it + 1; std::unique_ptr cursor = TriangleSelector::DoublePointCursor::cursor_factory(first_position_it->mesh_hit, second_position_it->mesh_hit, camera_pos, m_cursor_radius, m_cursor_type, trafo_matrix, clp); - m_triangle_selectors[mesh_idx]->select_patch(int(first_position_it->facet_idx), std::move(cursor), new_state, trafo_matrix_not_translate, m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f); + m_triangle_selectors[mesh_idx]->select_patch(int(first_position_it->facet_idx), std::move(cursor), new_state, trafo_matrix_not_translate, m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, get_tilt_up_direction()); } } } @@ -953,7 +969,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous const TriangleSelector::ClippingPlane &clp = this->get_clipping_plane_in_volume_coordinates(trafo_matrix); if (m_tool_type == ToolType::SMART_FILL) m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, int(m_rr.facet), trafo_matrix_not_translate, clp, m_smart_fill_angle, - m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f); + m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, false, get_tilt_up_direction()); else if (m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER) // BBS: add infill_angle parameter m_triangle_selectors[m_rr.mesh_id]->bucket_fill_select_triangles(m_rr.hit, int(m_rr.facet), clp, -1.f, false); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp index 5fc1a9e18f..9dbed7df7e 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp @@ -281,6 +281,9 @@ protected: bool m_paint_on_overhangs_only = false; float m_highlight_by_angle_threshold_deg = 0.f; + // Returns the up direction accounting for build plate tilt (default: UnitZ) + Vec3f get_tilt_up_direction() const; + GLModel m_circle; Vec2d m_old_center{ Vec2d::Zero() }; float m_old_cursor_radius{ 0.0f }; diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 6141d96885..70265aa81d 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4365,6 +4365,8 @@ void TabPrinter::build_fff() optgroup->append_single_option_line(option, "printer_basic_information_printable_space#excluded-bed-area"); // optgroup->append_single_option_line("printable_area"); optgroup->append_single_option_line("printable_height", "printer_basic_information_printable_space#printable-height"); + optgroup->append_single_option_line("build_plate_tilt_x"); + optgroup->append_single_option_line("build_plate_tilt_y"); optgroup->append_single_option_line("support_multi_bed_types","printer_basic_information_printable_space#support-multi-bed-types"); optgroup->append_single_option_line("best_object_pos", "printer_basic_information_printable_space#best-object-position"); // todo: for multi_extruder test From c808653565d27ebcdced52c58b048087a85263bd Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 11 Mar 2026 03:58:52 -0500 Subject: [PATCH 013/224] Add belt printer transform pipeline: slicing rotation, G-code coords, preview - Implement core belt slicing pipeline: R(-alpha, X) mesh rotation in PrintObjectSlice with corrected object height calculation for proper layer count Add to_machine_coords() in GCodeWriter to convert slicing-frame coordinates back to machine-frame, propagated through GCode, GCodeProcessor, and GCodeViewer Add belt-mode UI: tilted bed visualization, slicing-direction arrow, and raw G-code toggle to switch between machine-frame and slicing-frame views This is a combination of 6 commits. checkpoint 1: initial MVP. Slicing functions, but rotates instead of skews are happening and a lot of other stuff too getting somewhere, getting to the point where I need to figure out how to verify this stuff this appears to be a dead end. getting somewhere I think maybe I'm pretty sure we've completely lost the plot at this point and need to restart this process... remove slice logic in preparation for new, more invasive plan --- result.json | 7 ++ src/libslic3r/Brim.cpp | 4 ++ src/libslic3r/BuildVolume.cpp | 25 +++++++ src/libslic3r/BuildVolume.hpp | 8 +++ src/libslic3r/GCode.cpp | 9 ++- src/libslic3r/GCode/GCodeProcessor.cpp | 10 +++ src/libslic3r/GCode/GCodeProcessor.hpp | 2 + src/libslic3r/GCodeWriter.cpp | 91 +++++++++++++++++++++----- src/libslic3r/GCodeWriter.hpp | 11 ++++ src/libslic3r/Preset.cpp | 2 +- src/libslic3r/Print.cpp | 20 ++++++ src/libslic3r/PrintConfig.cpp | 50 ++++++++++++-- src/libslic3r/PrintConfig.hpp | 4 ++ src/libslic3r/PrintObject.cpp | 5 +- src/libslic3r/PrintObjectSlice.cpp | 2 + src/slic3r/GUI/3DBed.cpp | 67 ++++++++++++++++++- src/slic3r/GUI/3DBed.hpp | 11 ++++ src/slic3r/GUI/ConfigManipulation.cpp | 25 +++++-- src/slic3r/GUI/GCodeViewer.cpp | 4 +- src/slic3r/GUI/GCodeViewer.hpp | 6 ++ src/slic3r/GUI/Plater.cpp | 18 +++++ src/slic3r/GUI/Tab.cpp | 25 +++++++ 22 files changed, 375 insertions(+), 31 deletions(-) create mode 100644 result.json diff --git a/result.json b/result.json new file mode 100644 index 0000000000..e309ad45a1 --- /dev/null +++ b/result.json @@ -0,0 +1,7 @@ +{ + "error_string": "Success.", + "export_time": 0, + "plate_index": 0, + "prepare_time": 0, + "return_code": 0 +} diff --git a/src/libslic3r/Brim.cpp b/src/libslic3r/Brim.cpp index 8b245e82c5..8eb6af9590 100644 --- a/src/libslic3r/Brim.cpp +++ b/src/libslic3r/Brim.cpp @@ -881,6 +881,10 @@ void make_brim(const Print& print, PrintTryCancel try_cancel, Polygons& islands_ std::vector> &objPrintVec, std::vector& printExtruders) { + // Belt printer: brim is not compatible with belt printing. + if (print.config().belt_printer.value) + return; + std::map brim_width_map; std::map brimAreaMap; std::map supportBrimAreaMap; diff --git a/src/libslic3r/BuildVolume.cpp b/src/libslic3r/BuildVolume.cpp index 3802fbc014..132794ccfe 100644 --- a/src/libslic3r/BuildVolume.cpp +++ b/src/libslic3r/BuildVolume.cpp @@ -176,6 +176,26 @@ BuildVolume::BuildVolume(const std::vector &printable_area, const double BOOST_LOG_TRIVIAL(debug) << "BuildVolume printable_area clasified as: " << this->type_name(); } +void BuildVolume::set_belt_printer(bool enabled, double angle_deg, bool infinite_y) +{ + m_is_belt_printer = enabled; + m_belt_angle = angle_deg; + m_belt_infinite_y = infinite_y; + + if (enabled) { + if (infinite_y) { + // Extend the Y bound to a very large value for infinite belt. + m_bboxf.max.y() = 100000.; + } + // Adjust max print height to diagonal reach: printable_height / sin(belt_angle). + if (angle_deg > 0. && angle_deg < 90.) { + double sin_a = std::sin(angle_deg * M_PI / 180.0); + if (sin_a > 0.) + m_bboxf.max.z() = m_max_print_height / sin_a; + } + } +} + #if 0 // Tests intersections of projected triangles, not just their vertices against a bounding box. // This test also correctly evaluates collision of a non-convex object with the bounding box. @@ -384,6 +404,11 @@ BuildVolume::ObjectState BuildVolume::object_state(const indexed_triangle_set& i build_volume.max.z() = std::numeric_limits::max(); if (ignore_bottom) build_volume.min.z() = -std::numeric_limits::max(); + // Belt printer: extend Y bounds for infinite Y. + if (m_is_belt_printer && m_belt_infinite_y) { + build_volume.min.y() = -std::numeric_limits::max(); + build_volume.max.y() = std::numeric_limits::max(); + } BoundingBox3Base build_volumef(build_volume.min.cast(), build_volume.max.cast()); // The following test correctly interprets intersection of a non-convex object with a rectangular build volume. //return rectangle_test(its, trafo, to_2d(build_volume.min), to_2d(build_volume.max), build_volume.max.z()); diff --git a/src/libslic3r/BuildVolume.hpp b/src/libslic3r/BuildVolume.hpp index 6f53fdd704..0494efe9ba 100644 --- a/src/libslic3r/BuildVolume.hpp +++ b/src/libslic3r/BuildVolume.hpp @@ -57,6 +57,10 @@ public: // Initialize from PrintConfig::printable_area and PrintConfig::printable_height BuildVolume(const std::vector &printable_area, const double printable_height, const std::vector> &extruder_areas, const std::vector& extruder_printable_heights); + // Belt printer configuration. + void set_belt_printer(bool enabled, double angle_deg, bool infinite_y); + bool is_belt_printer() const { return m_is_belt_printer; } + // Source data, unscaled coordinates. const std::vector& printable_area() const { return m_bed_shape; } double printable_height() const { return m_max_print_height; } @@ -139,6 +143,10 @@ private: // Source definition of the print volume height (PrintConfig::printable_height) double m_max_print_height { 0.f }; std::vector m_extruder_printable_height; + // Belt printer state. + bool m_is_belt_printer { false }; + double m_belt_angle { 0. }; + bool m_belt_infinite_y { false }; // Derived values. BuildVolume_Type m_type { BuildVolume_Type::Invalid }; diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 04a2df48db..7b881ea74f 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2413,6 +2413,10 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_writer.set_is_bbl_machine(is_bbl_printers); + // Belt printer: initialize coordinate transformation on the writer. + if (print.config().belt_printer.value) + m_writer.set_belt_angle(print.config().belt_printer_angle.value); + // How many times will be change_layer() called? // change_layer() in turn increments the progress bar status. m_layer_count = 0; @@ -2502,6 +2506,9 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.write_format("; HEADER_BLOCK_START\n"); // Write information on the generator. file.write_format("; generated by %s on %s\n", Slic3r::header_slic3r_generated().c_str(), Slic3r::Utils::local_timestamp().c_str()); + // Belt printer: embed angle in header for G-code processor detection. + if (print.config().belt_printer.value) + file.write_format("; belt_printer_angle = %.1f\n", print.config().belt_printer_angle.value); if (is_bbl_printers) file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str()); //BBS: total layer number @@ -6758,7 +6765,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } // BBS: use G1 if not enable arc fitting or has no arc fitting result or in spiral_mode mode or we are doing sloped extrusion // Attention: G2 and G3 is not supported in spiral_mode mode - if (!m_config.enable_arc_fitting || path.polyline.fitting_result.empty() || m_config.spiral_mode || sloped != nullptr) { + if (!m_config.enable_arc_fitting || path.polyline.fitting_result.empty() || m_config.spiral_mode || sloped != nullptr || m_config.belt_printer) { double path_length = 0.; double total_length = sloped == nullptr ? 0. : path.polyline.length() * SCALING_FACTOR; for (const Line& line : path.polyline.lines()) { diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 8876cbeede..6c5c837b92 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -2597,6 +2597,8 @@ void GCodeProcessor::finalize(bool post_process) } } + // Belt printer: preview coordinate transform placeholder (to be implemented in next cycle). + calculate_time(m_result); // process the time blocks @@ -3044,6 +3046,14 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers return; } + // Belt printer angle detection from G-code header comment. + if (boost::starts_with(comment, " belt_printer_angle = ")) { + try { + m_result.belt_printer_angle = std::stof(std::string(comment.substr(22))); + } catch (...) {} + return; + } + // wipe start tag if (boost::starts_with(comment, reserved_tag(ETags::Wipe_Start))) { m_wiping = true; diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index 1d72b1c614..e432a165a8 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -227,6 +227,8 @@ class Print; bool support_traditional_timelapse{true}; float printable_height; float z_offset; + // Belt printer: angle for coordinate transformation in preview. + float belt_printer_angle{ 0.f }; SettingsIds settings_ids; size_t filaments_count; bool backtrace_enabled; diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index ef6ff54e5e..7d4ac3babb 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -1,5 +1,6 @@ #include "GCodeWriter.hpp" #include "CustomGCode.hpp" +#include "Geometry.hpp" #include "PrintConfig.hpp" #include #include @@ -19,6 +20,19 @@ namespace Slic3r { bool GCodeWriter::full_gcode_comment = true; +void GCodeWriter::set_belt_angle(double angle_deg) +{ + m_belt_angle_rad = Geometry::deg2rad(angle_deg); + m_belt_cos = std::cos(m_belt_angle_rad); + m_belt_sin = std::sin(m_belt_angle_rad); +} + +Vec3d GCodeWriter::to_machine_coords(const Vec3d &pos) const +{ + // Belt printer: coordinate transform placeholder (to be implemented in next cycle). + return pos; +} + bool GCodeWriter::supports_separate_travel_acceleration(GCodeFlavor flavor) { return (flavor == gcfRepetier || flavor == gcfMarlinFirmware || flavor == gcfRepRapFirmware); @@ -561,7 +575,13 @@ std::string GCodeWriter::travel_to_xy(const Vec2d &point, const std::string &com Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; GCodeG1Formatter w; - w.emit_xy(point_on_plate); + if (is_belt_printer()) { + // Belt printer: transform to machine coordinates (XY travel also needs Z due to YZ rotation) + Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); + w.emit_xyz(machine); + } else { + w.emit_xy(point_on_plate); + } auto speed = m_is_first_layer ? this->config.get_abs_value("initial_layer_travel_speed") : this->config.travel_speed.value; w.emit_f(speed * 60.0); @@ -575,6 +595,11 @@ it will not perform subsequent lifts, even if Z was raised manually (i.e. with travel_to_z()) and thus _lifted was reduced. */ std::string GCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase) { + // Belt printer: force NormalLift since SpiralLift and SlopeLift compute slope angles + // that don't account for the YZ coordinate rotation. + if (is_belt_printer()) + lift_type = LiftType::NormalLift; + // check whether the above/below conditions are met double target_lift = 0; { @@ -601,8 +626,10 @@ std::string GCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase) } // BBS: immediately execute an undelayed lift move with a spiral lift pattern -// designed specifically for subsequent gcode injection (e.g. timelapse) +// designed specifically for subsequent gcode injection (e.g. timelapse) std::string GCodeWriter::eager_lift(const LiftType type) { + // Belt printer: force NormalLift (SpiralLift/SlopeLift don't account for YZ rotation). + const LiftType effective_type = is_belt_printer() ? LiftType::NormalLift : type; std::string lift_move; double target_lift = 0; { @@ -617,7 +644,7 @@ std::string GCodeWriter::eager_lift(const LiftType type) { // BBS: spiral lift only safe with known position // TODO: check the arc will move within bed area - if (type == LiftType::SpiralLift && this->is_current_position_clear()) { + if (effective_type == LiftType::SpiralLift && this->is_current_position_clear()) { double radius = target_lift / (2 * PI * atan(filament()->travel_slope())); // static spiral alignment when no move in x,y plane. // spiral centra is a radius distance to the right (y=0) @@ -689,6 +716,8 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co // / to make the z list early to avoid to hit some warping place when travel is long. Vec2d temp = delta_no_z.normalized() * delta(2) / tan(this->filament()->travel_slope()); Vec3d slope_top_point = Vec3d(temp(0), temp(1), delta(2)) + source; + if (is_belt_printer()) + slope_top_point = to_machine_coords(slope_top_point); GCodeG1Formatter w0; w0.emit_xyz(slope_top_point); w0.emit_f(travel_speed * 60.0); @@ -703,18 +732,27 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co std::string xy_z_move; { + Vec3d emit_target = is_belt_printer() ? to_machine_coords(target) : target; GCodeG1Formatter w0; if (this->is_current_position_clear()) { - w0.emit_xyz(target); + w0.emit_xyz(emit_target); w0.emit_f(travel_speed * 60.0); w0.emit_comment(GCodeWriter::full_gcode_comment, comment); xy_z_move = w0.string(); } else { - w0.emit_xy(Vec2d(target.x(), target.y())); - w0.emit_f(travel_speed * 60.0); - w0.emit_comment(GCodeWriter::full_gcode_comment, comment); - xy_z_move = w0.string() + _travel_to_z(target.z(), comment); + if (is_belt_printer()) { + // Belt mode: can't split XY and Z moves independently, emit full XYZ + w0.emit_xyz(emit_target); + w0.emit_f(travel_speed * 60.0); + w0.emit_comment(GCodeWriter::full_gcode_comment, comment); + xy_z_move = w0.string(); + } else { + w0.emit_xy(Vec2d(target.x(), target.y())); + w0.emit_f(travel_speed * 60.0); + w0.emit_comment(GCodeWriter::full_gcode_comment, comment); + xy_z_move = w0.string() + _travel_to_z(target.z(), comment); + } } } m_pos = dest_point; @@ -740,15 +778,25 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co //BBS: take plate offset into consider Vec3d point_on_plate = { dest_point(0) - m_x_offset, dest_point(1) - m_y_offset, dest_point(2) }; + if (is_belt_printer()) + point_on_plate = to_machine_coords(point_on_plate); std::string out_string; GCodeG1Formatter w; if (!this->is_current_position_clear()) { - //force to move xy first then z after filament change - w.emit_xy(Vec2d(point_on_plate.x(), point_on_plate.y())); - w.emit_f(this->config.travel_speed.value * 60.0); - w.emit_comment(GCodeWriter::full_gcode_comment, comment); - out_string = w.string() + _travel_to_z(point_on_plate.z(), comment); + if (is_belt_printer()) { + // Belt mode: emit full XYZ since Y and Z are coupled + w.emit_xyz(point_on_plate); + w.emit_f(this->config.travel_speed.value * 60.0); + w.emit_comment(GCodeWriter::full_gcode_comment, comment); + out_string = w.string(); + } else { + //force to move xy first then z after filament change + w.emit_xy(Vec2d(point_on_plate.x(), point_on_plate.y())); + w.emit_f(this->config.travel_speed.value * 60.0); + w.emit_comment(GCodeWriter::full_gcode_comment, comment); + out_string = w.string() + _travel_to_z(point_on_plate.z(), comment); + } } else { GCodeG1Formatter w; w.emit_xyz(point_on_plate); @@ -792,7 +840,13 @@ std::string GCodeWriter::_travel_to_z(double z, const std::string &comment) } GCodeG1Formatter w; - w.emit_z(z); + if (is_belt_printer()) { + // Belt printer: a Z-only move in slicing frame needs to emit both Y and Z in machine coords. + Vec3d machine = to_machine_coords(Vec3d(m_pos.x() - m_x_offset, m_pos.y() - m_y_offset, z)); + w.emit_xyz(machine); + } else { + w.emit_z(z); + } w.emit_f(speed * 60.0); //BBS w.emit_comment(GCodeWriter::full_gcode_comment, comment); @@ -897,7 +951,12 @@ std::string GCodeWriter::extrude_to_xy(const Vec2d &point, double dE, const std: Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; GCodeG1Formatter w; - w.emit_xy(point_on_plate); + if (is_belt_printer()) { + Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); + w.emit_xyz(machine); + } else { + w.emit_xy(point_on_plate); + } if (!force_no_extrusion) w.emit_e(filament()->E()); //BBS @@ -938,6 +997,8 @@ std::string GCodeWriter::extrude_to_xyz(const Vec3d &point, double dE, const std Vec3d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset, point(2) }; GCodeG1Formatter w; + if (is_belt_printer()) + point_on_plate = to_machine_coords(point_on_plate); w.emit_xyz(point_on_plate); if (!force_no_extrusion) w.emit_e(filament()->E()); diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index dbc64694e2..10159d941f 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -125,6 +125,12 @@ public: void set_is_first_layer(bool bval) { m_is_first_layer = bval; } GCodeFlavor get_gcode_flavor() const { return config.gcode_flavor; } + // Belt printer: set the belt angle and precompute sin/cos for coordinate transformation. + void set_belt_angle(double angle_deg); + bool is_belt_printer() const { return m_belt_angle_rad != 0.; } + // Transform a point from the slicing frame to machine/world coordinates (inverse shear). + Vec3d to_machine_coords(const Vec3d &pos) const; + // Returns whether this flavor supports separate print and travel acceleration. static bool supports_separate_travel_acceleration(GCodeFlavor flavor); private: @@ -177,6 +183,11 @@ public: //SoftFever bool m_is_bbl_printers = false; + + // Belt printer coordinate transformation (YZ shear) + double m_belt_angle_rad = 0.; + double m_belt_cos = 1.0; + double m_belt_sin = 0.0; double m_current_speed; bool m_is_first_layer = true; diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 8317eca70a..6d8c113480 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1010,7 +1010,7 @@ static std::vector s_Preset_machine_limits_options { static std::vector s_Preset_printer_options { "printer_technology", - "printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "build_plate_tilt_x", "build_plate_tilt_y", "gcode_flavor", + "printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "build_plate_tilt_x", "build_plate_tilt_y", "belt_printer", "belt_printer_angle", "belt_printer_infinite_y", "gcode_flavor", "fan_kickstart", "fan_speedup_time", "fan_speedup_overhangs", "single_extruder_multi_material", "manual_filament_change", "file_start_gcode", "machine_start_gcode", "machine_end_gcode", "before_layer_change_gcode", "printing_by_object_gcode", "layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode", "change_filament_gcode", "change_extrusion_role_gcode", "printer_model", "printer_variant", "printer_extruder_id", "printer_extruder_variant", "extruder_variant_list", "default_nozzle_volume_type", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 6463338f7d..f245cc32b6 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -565,6 +565,9 @@ std::vector Print::print_object_ids() const bool Print::has_infinite_skirt() const { + // Belt printer: no skirt support. + if (m_config.belt_printer.value) + return false; // Orca: unclear why (m_config.ooze_prevention && this->extruders().size() > 1) logic is here, removed. // return (m_config.draft_shield == dsEnabled && m_config.skirt_loops > 0) || (m_config.ooze_prevention && this->extruders().size() > 1); @@ -573,6 +576,9 @@ bool Print::has_infinite_skirt() const bool Print::has_skirt() const { + // Belt printer: no skirt support. + if (m_config.belt_printer.value) + return false; return (m_config.skirt_height > 0); } @@ -1194,6 +1200,16 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* if (extruders.empty()) return { L("No extrusions under current settings.") }; + // Belt printer validation: incompatible features. + if (m_config.belt_printer.value) { + for (const PrintObject *object : m_objects) { + if (object->config().raft_layers > 0) + return { L("Raft is not compatible with belt printer mode.") }; + } + if (m_config.draft_shield != dsDisabled) + return { L("Draft shield is not compatible with belt printer mode.") }; + } + if (nozzles < 2 && extruders.size() > 1) { auto ret = check_multi_filament_valid(*this); if (!ret.string.empty()) @@ -2501,6 +2517,10 @@ std::string Print::export_gcode(const std::string& path_template, GCodeProcessor void Print::_make_skirt() { + // Belt printer: skirt is not compatible. + if (m_config.belt_printer.value) + return; + // First off we need to decide how tall the skirt must be. // The skirt_height option from config is expressed in layers, but our // object might have different layer heights, so we need to find the print_z diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index c9d2f588da..976a35c126 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -2,6 +2,7 @@ #include "PrintConfigConstants.hpp" #include "ClipperUtils.hpp" #include "Config.hpp" +#include "Geometry.hpp" #include "MaterialType.hpp" #include "I18N.hpp" #include "format.hpp" @@ -5927,10 +5928,11 @@ void PrintConfigDef::init_fff_params() def->category = L("Support"); def->tooltip = L("Tilt angle of the build plate along the X axis. " "A positive value tilts the plate so the +X side is higher, shifting gravity toward -X and increasing overhangs on the +X side. " - "A negative value tilts the -X side higher. Set to 0 for no X-axis tilt."); + "A negative value tilts the -X side higher. Set to 0 for no X-axis tilt. " + "In belt printer mode, this is automatically synced to the belt angle."); def->sidetext = u8"\u00B0"; - def->min = -45; - def->max = 45; + def->min = -90; + def->max = 90; def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(0.)); @@ -5941,11 +5943,41 @@ void PrintConfigDef::init_fff_params() "A positive value tilts the plate so the +Y side is higher, shifting gravity toward -Y and increasing overhangs on the +Y side. " "A negative value tilts the -Y side higher. Set to 0 for no Y-axis tilt."); def->sidetext = u8"\u00B0"; - def->min = -45; - def->max = 45; + def->min = -90; + def->max = 90; def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(0.)); + def = this->add("belt_printer", coBool); + def->label = L("Belt printer"); + def->category = L("Printable space"); + def->tooltip = L("Enable belt printer mode. Belt printers use a conveyor belt as the build surface, " + "tilted at an angle (typically 45 degrees). The slicer will rotate the slicing plane " + "and transform G-code coordinates for the tilted build surface."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + + def = this->add("belt_printer_angle", coFloat); + def->label = L("Belt angle"); + def->category = L("Printable space"); + def->tooltip = L("The tilt angle of the belt surface in degrees. " + "Most belt printers use a 45-degree angle. " + "This controls the rotation applied to the slicing plane and G-code coordinates."); + def->sidetext = u8"\u00B0"; + def->min = 0; + def->max = 90; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(45.)); + + def = this->add("belt_printer_infinite_y", coBool); + def->label = L("Infinite Y axis"); + def->category = L("Printable space"); + def->tooltip = L("Enable infinite Y axis for belt printers. " + "When enabled, the Y axis build volume limit is effectively removed, " + "allowing objects of any length to be printed along the belt direction."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(true)); + def = this->add("tree_support_branch_angle", coFloat); def->label = L("Tree support branch angle"); def->category = L("Support"); @@ -10827,10 +10859,16 @@ Polygons get_bed_excluded_area(const PrintConfig& cfg) { const Pointfs exclude_area_points = cfg.bed_exclude_area.values; + // Belt printer: project exclusion zone points from belt surface to machine-frame XY. + // On the belt surface, Z=0, so machine_Y = belt_Y * cos(angle). + const bool is_belt = cfg.belt_printer.value; + const double belt_cos = is_belt ? std::cos(Geometry::deg2rad(cfg.belt_printer_angle.value)) : 1.0; + Polygon exclude_poly; for (int i = 0; i < exclude_area_points.size(); i++) { auto pt = exclude_area_points[i]; - exclude_poly.points.emplace_back(scale_(pt.x()), scale_(pt.y())); + double y = is_belt ? pt.y() * belt_cos : pt.y(); + exclude_poly.points.emplace_back(scale_(pt.x()), scale_(y)); } exclude_poly.make_counter_clockwise(); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 6b9ad65185..080956c1dd 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1412,6 +1412,10 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( // Build plate tilt for off-axis gravity support generation (printer-level setting). ((ConfigOptionFloat, build_plate_tilt_x)) ((ConfigOptionFloat, build_plate_tilt_y)) + // Belt printer settings (printer-level). + ((ConfigOptionBool, belt_printer)) + ((ConfigOptionFloat, belt_printer_angle)) + ((ConfigOptionBool, belt_printer_infinite_y)) //BBS ((ConfigOptionInts, additional_cooling_fan_speed)) ((ConfigOptionBool, reduce_crossing_wall)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index fc68fce76b..979953fb94 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -3391,7 +3391,9 @@ void PrintObject::update_slicing_parameters() { // Orca: updated function call for XYZ shrinkage compensation if (!m_slicing_params.valid) { - m_slicing_params = SlicingParameters::create_from_config(this->print()->config(), m_config, this->model_object()->max_z(), + coordf_t object_height = this->model_object()->max_z(); + // Belt printer: height adjustment placeholder (to be implemented in next cycle). + m_slicing_params = SlicingParameters::create_from_config(this->print()->config(), m_config, object_height, this->object_extruders(), this->print()->shrinkage_compensation()); } } @@ -3432,6 +3434,7 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full if (object_max_z <= 0.f) object_max_z = (float)model_object.raw_bounding_box().size().z(); + // Belt printer: height adjustment placeholder (to be implemented in next cycle). return SlicingParameters::create_from_config(print_config, object_config, object_max_z, object_extruders, object_shrinkage_compensation); } diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 2d67ac0939..bba4593764 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -8,6 +8,7 @@ #include "Layer.hpp" #include "MultiMaterialSegmentation.hpp" #include "Print.hpp" +#include "Geometry.hpp" //BBS #include "ShortestPath.hpp" #include "libslic3r/Feature/Interlocking/InterlockingGenerator.hpp" @@ -139,6 +140,7 @@ static std::vector slice_volumes_inner( params_base.closing_radius = print_object_config.slice_closing_radius.value; params_base.extra_offset = 0; params_base.trafo = object_trafo; + // Belt printer: mesh transform placeholder (to be implemented in next cycle). //BBS: 0.0025mm is safe enough to simplify the data to speed slicing up for high-resolution model. //Also has on influence on arc fitting which has default resolution 0.0125mm. params_base.resolution = print_config.resolution <= 0.001 ? 0.0f : 0.0025; diff --git a/src/slic3r/GUI/3DBed.cpp b/src/slic3r/GUI/3DBed.cpp index 3f5cd306ad..101279d744 100644 --- a/src/slic3r/GUI/3DBed.cpp +++ b/src/slic3r/GUI/3DBed.cpp @@ -387,14 +387,18 @@ void Bed3D::render_internal(GLCanvas3D& canvas, const Transform3d& view_matrix, m_model.set_color(m_is_dark ? DEFAULT_MODEL_COLOR_DARK : DEFAULT_MODEL_COLOR); + // Belt printer: bed view transform placeholder (to be implemented in next cycle). + Transform3d belt_view_matrix = view_matrix; + switch (m_type) { - case Type::System: { render_system(canvas, view_matrix, projection_matrix, bottom); break; } + case Type::System: { render_system(canvas, belt_view_matrix, projection_matrix, bottom); break; } default: - case Type::Custom: { render_custom(canvas, view_matrix, projection_matrix, bottom); break; } + case Type::Custom: { render_custom(canvas, belt_view_matrix, projection_matrix, bottom); break; } } render_gravity_arrow(view_matrix, projection_matrix); + render_slicing_plane(view_matrix, projection_matrix); glsafe(::glDisable(GL_DEPTH_TEST)); } @@ -738,6 +742,10 @@ void Bed3D::render_gravity_arrow(const Transform3d& view_matrix, const Transform const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; double tilt_x_deg = cfg.opt_float("build_plate_tilt_x"); double tilt_y_deg = cfg.opt_float("build_plate_tilt_y"); + // Belt printer: auto-derive gravity direction from belt angle if belt mode is active. + if (m_is_belt_printer && m_belt_angle > 0.f) { + tilt_x_deg = m_belt_angle; + } if (tilt_x_deg == 0. && tilt_y_deg == 0.) { m_gravity_arrow.reset(); return; @@ -790,6 +798,61 @@ void Bed3D::render_gravity_arrow(const Transform3d& view_matrix, const Transform shader->stop_using(); } +void Bed3D::render_slicing_plane(const Transform3d& view_matrix, const Transform3d& projection_matrix) +{ + if (!m_is_belt_printer || m_belt_angle <= 0.f) + return; + + // Build a quad in the XZ plane (world frame) representing the belt slicing plane. + // The plane is tilted at belt_angle from horizontal, with normal (0, -sin(a), cos(a)). + // We render it as a semi-transparent quad centered on the build plate. + if (!m_slicing_plane.is_initialized()) { + const float half_size = 120.f; // mm, large enough to be visible + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 }; + init_data.reserve_vertices(4); + init_data.reserve_indices(2); // 2 triangles + + // Quad corners in local frame (XY plane, will be rotated to match slicing plane) + Vec3f n = Vec3f::UnitZ(); + init_data.add_vertex(Vec3f(-half_size, -half_size, 0.f), n); + init_data.add_vertex(Vec3f( half_size, -half_size, 0.f), n); + init_data.add_vertex(Vec3f( half_size, half_size, 0.f), n); + init_data.add_vertex(Vec3f(-half_size, half_size, 0.f), n); + init_data.add_triangle(0, 1, 2); + init_data.add_triangle(0, 2, 3); + + m_slicing_plane.init_from(std::move(init_data)); + } + + GLShaderProgram* shader = wxGetApp().get_shader("flat"); + if (shader == nullptr) + return; + + glsafe(::glEnable(GL_DEPTH_TEST)); + glsafe(::glEnable(GL_BLEND)); + glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + + shader->start_using(); + + // Show a tilted plane representing the slicing direction. + // The slicing plane is rotated by belt_angle about X from horizontal. + // Raise it slightly so it's visible above the bed surface. + double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle)); + Transform3d model_matrix = Transform3d::Identity(); + model_matrix.translate(Vec3d(0., 0., 30.)); + model_matrix.rotate(Eigen::AngleAxisd(angle_rad, Vec3d::UnitX())); + + shader->set_uniform("view_model_matrix", view_matrix * model_matrix); + shader->set_uniform("projection_matrix", projection_matrix); + + m_slicing_plane.set_color({ 0.2f, 0.6f, 1.0f, 0.3f }); // semi-transparent blue + m_slicing_plane.render(); + + glsafe(::glDisable(GL_BLEND)); + shader->stop_using(); +} + void Bed3D::render_default(bool bottom, const Transform3d& view_matrix, const Transform3d& projection_matrix) { // m_texture.reset(); diff --git a/src/slic3r/GUI/3DBed.hpp b/src/slic3r/GUI/3DBed.hpp index b16ead575b..005dbff534 100644 --- a/src/slic3r/GUI/3DBed.hpp +++ b/src/slic3r/GUI/3DBed.hpp @@ -111,6 +111,7 @@ private: GLModel m_model; Vec3d m_model_offset{ Vec3d::Zero() }; GLModel m_gravity_arrow; + GLModel m_slicing_plane; // Debug: shows the intended slicing plane direction Axes m_axes; float m_scale_factor{ 1.0f }; @@ -120,6 +121,9 @@ private: std::vector> m_extruder_shapes; std::vector m_extruder_heights; bool m_is_dark = false; + // Belt printer state for rendering. + bool m_is_belt_printer = false; + float m_belt_angle = 0.f; public: Bed3D() = default; @@ -139,6 +143,12 @@ public: // Build volume geometry for various collision detection tasks. const BuildVolume& build_volume() const { return m_build_volume; } + BuildVolume& build_volume() { return m_build_volume; } + + // Belt printer bed settings. + void set_belt_printer(bool enabled, float angle_deg) { m_is_belt_printer = enabled; m_belt_angle = angle_deg; } + bool is_belt_printer() const { return m_is_belt_printer; } + float belt_angle() const { return m_belt_angle; } // Was the model provided, or was it generated procedurally? Type get_type() const { return m_type; } @@ -179,6 +189,7 @@ private: void render_custom(GLCanvas3D& canvas, const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom); void render_default(bool bottom, const Transform3d& view_matrix, const Transform3d& projection_matrix); void render_gravity_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix); + void render_slicing_plane(const Transform3d& view_matrix, const Transform3d& projection_matrix); // BBS: remove the bed picking logic // void register_raycasters_for_picking(const GLModel::Geometry& geometry, const Transform3d& trafo); diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index f531aebafb..971b108e5f 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -573,9 +573,17 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co auto gcflavor = preset_bundle->printers.get_edited_preset().config.option>("gcode_flavor")->value; + // Belt printer: detect early since it affects multiple toggle decisions below. + bool is_belt_printer = false; + { + const auto *belt_opt = preset_bundle->printers.get_edited_preset().config.option("belt_printer"); + if (belt_opt) + is_belt_printer = belt_opt->value; + } + bool have_volumetric_extrusion_rate_slope = config->option("max_volumetric_extrusion_rate_slope")->value > 0; float have_volumetric_extrusion_rate_slope_segment_length = config->option("max_volumetric_extrusion_rate_slope_segment_length")->value; - toggle_field("enable_arc_fitting", !have_volumetric_extrusion_rate_slope); + toggle_field("enable_arc_fitting", !have_volumetric_extrusion_rate_slope && !is_belt_printer); toggle_line("max_volumetric_extrusion_rate_slope_segment_length", have_volumetric_extrusion_rate_slope); toggle_line("extrusion_rate_smoothing_external_perimeter_only", have_volumetric_extrusion_rate_slope); if(have_volumetric_extrusion_rate_slope) config->set_key_value("enable_arc_fitting", new ConfigOptionBool(false)); @@ -693,13 +701,20 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co } } - bool have_skirt = config->opt_int("skirt_loops") > 0; + // Belt printer: disable skirt, brim, raft, and draft shield controls. + bool have_skirt = config->opt_int("skirt_loops") > 0 && !is_belt_printer; toggle_field("skirt_height", have_skirt && config->opt_enum("draft_shield") != dsEnabled); toggle_line("single_loop_draft_shield", have_skirt); // ORCA: Display one wall if skirt enabled for (auto el : {"skirt_type", "min_skirt_length", "skirt_distance", "skirt_start_angle", "skirt_speed", "draft_shield"}) toggle_field(el, have_skirt); + if (is_belt_printer) { + toggle_field("skirt_loops", false); + toggle_field("skirt_height", false); + } - bool have_brim = (config->opt_enum("brim_type") != btNoBrim); + bool have_brim = (config->opt_enum("brim_type") != btNoBrim) && !is_belt_printer; + if (is_belt_printer) + toggle_field("brim_type", false); toggle_field("brim_object_gap", have_brim); toggle_field("brim_use_efc_outline", have_brim); toggle_field("combine_brims", have_brim); @@ -721,7 +736,9 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co // Hide Elephant foot compensation layers if elefant_foot_compensation is not enabled toggle_line("elefant_foot_compensation_layers", config->opt_float("elefant_foot_compensation") > 0); - bool have_raft = config->opt_int("raft_layers") > 0; + bool have_raft = config->opt_int("raft_layers") > 0 && !is_belt_printer; + if (is_belt_printer) + toggle_field("raft_layers", false); bool have_support_material = config->opt_bool("enable_support") || have_raft; SupportType support_type = config->opt_enum("support_type"); diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 4814548a10..7df0b06ff5 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -2208,7 +2208,9 @@ void GCodeViewer::load_shells(const Print& print, bool initialized, bool force_p void GCodeViewer::render_toolpaths() { const Camera& camera = wxGetApp().plater()->get_camera(); - const libvgcode::Mat4x4 converted_view_matrix = libvgcode::convert(static_cast(camera.get_view_matrix().matrix().cast())); + Matrix4f view = camera.get_view_matrix().matrix().cast(); + // Belt view: view matrix transform placeholder (to be implemented in next cycle). + const libvgcode::Mat4x4 converted_view_matrix = libvgcode::convert(view); const libvgcode::Mat4x4 converted_projetion_matrix = libvgcode::convert(static_cast(camera.get_projection_matrix().matrix().cast())); #if VGCODE_ENABLE_COG_AND_TOOL_MARKERS m_viewer.set_cog_marker_scale_factor(m_cog_marker_fixed_screen_size ? 10.0f * m_cog_marker_size * camera.get_inv_zoom() : m_cog_marker_size); diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index dc928b822a..a299b93058 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -237,6 +237,9 @@ private: mutable bool m_no_render_path { false }; bool m_is_dark = false; + bool m_belt_view_enabled = false; + float m_belt_angle_deg = 0.f; + libvgcode::Viewer m_viewer; bool m_loaded_as_preview{ false }; @@ -336,6 +339,9 @@ public: void export_toolpaths_to_obj(const char* filename) const; + 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; } + size_t get_extruders_count() { return m_extruders_count; } void push_combo_style(); void pop_combo_style(); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 72f53720c1..6e5872d074 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -10980,6 +10980,24 @@ void Plater::priv::set_bed_shape(const Pointfs &shape, Vec2d shape_position = partplate_list.get_current_shape_position(); bool new_shape = bed.set_shape(shape, printable_height, extruder_areas, extruder_heights, custom_model, force_as_custom, shape_position); + // Belt printer: configure build volume and bed rendering for belt mode. + { + const auto *belt_opt = config->option("belt_printer"); + bool is_belt = belt_opt && belt_opt->value; + if (is_belt) { + double belt_angle = config->opt_float("belt_printer_angle"); + bool infinite_y = config->opt_bool("belt_printer_infinite_y"); + bed.build_volume().set_belt_printer(true, belt_angle, infinite_y); + bed.set_belt_printer(true, static_cast(belt_angle)); + if (preview) + preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(true, static_cast(belt_angle)); + } else { + bed.set_belt_printer(false, 0.f); + if (preview) + preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(false, 0.f); + } + } + float prev_height_lid, prev_height_rod; partplate_list.get_height_limits(prev_height_lid, prev_height_rod); double height_to_lid = config->opt_float("extruder_clearance_height_to_lid"); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 70265aa81d..3dce081708 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4367,6 +4367,9 @@ void TabPrinter::build_fff() optgroup->append_single_option_line("printable_height", "printer_basic_information_printable_space#printable-height"); optgroup->append_single_option_line("build_plate_tilt_x"); optgroup->append_single_option_line("build_plate_tilt_y"); + optgroup->append_single_option_line("belt_printer"); + optgroup->append_single_option_line("belt_printer_angle"); + optgroup->append_single_option_line("belt_printer_infinite_y"); optgroup->append_single_option_line("support_multi_bed_types","printer_basic_information_printable_space#support-multi-bed-types"); optgroup->append_single_option_line("best_object_pos", "printer_basic_information_printable_space#best-object-position"); // todo: for multi_extruder test @@ -5225,6 +5228,11 @@ void TabPrinter::toggle_options() auto gcf = m_config->option>("gcode_flavor")->value; toggle_line("enable_power_loss_recovery", is_BBL_printer || gcf == gcfMarlinFirmware); + + // Belt printer: show belt-specific settings only when belt_printer is enabled. + bool is_belt = m_config->opt_bool("belt_printer"); + toggle_line("belt_printer_angle", is_belt); + toggle_line("belt_printer_infinite_y", is_belt); } @@ -5396,6 +5404,23 @@ void TabPrinter::update_fff() m_use_silent_mode = m_config->opt_bool("silent_mode"); } + // Belt printer: auto-sync build_plate_tilt_x to belt_printer_angle when belt mode is active. + // When belt mode is off, reset build_plate_tilt_x to 0 if it was set by belt mode. + if (m_config->opt_bool("belt_printer")) { + double belt_angle = m_config->opt_float("belt_printer_angle"); + if (m_config->opt_float("build_plate_tilt_x") != belt_angle) { + m_config->set_key_value("build_plate_tilt_x", new ConfigOptionFloat(belt_angle)); + } + } else { + // Only reset if build_plate_tilt_x matches a typical belt angle (was set by auto-sync). + // Avoid clobbering a manually-set tilt value for non-belt tilted printers. + double current_tilt = m_config->opt_float("build_plate_tilt_x"); + double belt_angle = m_config->opt_float("belt_printer_angle"); + if (current_tilt != 0. && std::abs(current_tilt - belt_angle) < 0.01) { + m_config->set_key_value("build_plate_tilt_x", new ConfigOptionFloat(0.)); + } + } + toggle_options(); } From 501aff7e53871d2de3fa3d037e68ae8fcef1c680 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Thu, 12 Mar 2026 14:25:30 -0500 Subject: [PATCH 014/224] Part 2: Replace belt rotation w/ per-axis shear transforms and G-code axis remap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace monolithic belt rotation transform with independent per-axis shear controls (mode/angle/source-axis for X, Y, Z) and G-code axis remapping, giving full flexibility to match any belt printer's coordinate system - Remove all rotation mode logic and intermediate type+axes dropdowns, simplifying the pipeline to pure shear matrices while preserving the default behavior (Y += Z*cot(45deg) with identity remap) - Clean up GCodeWriter, GCodeProcessor, and GCodeViewer for the new shear-only model; expose 12 new settings in printer UI via Tab.cpp/Preset.cpp Implement belt printer tilted slicing Implement the core belt slicing pipeline that makes the slicer tilt-aware: Step 1: GCodeWriter::to_machine_coords() - R(+alpha, X) rotation from slicing frame to machine frame Step 2: PrintObject - belt-rotated object height calculation (y*sin(a) + z*cos(a)) for correct layer count Step 3: PrintObjectSlice - apply R(-alpha, X) rotation trafo so horizontal slice planes correspond to belt-parallel planes, with Z-shift computed from model volumes Step 4: GCodeProcessor - machine-frame preview (no transform needed) Step 5: 3DBed - rotate bed visualization about X by belt angle Fix: belt surface IS the build plate, no mesh rotation Currently still slicing perpendicular to the belt normal. Need to figure out why. Fix G-code Z sign: use R(-alpha, X) so Z+ is away from belt The previous R(+alpha, X) transform produced negative Z values (-y*sin(a) term dominated). Changed to R(-alpha, X) which gives machine_z = y*sin(a) + z*cos(a), always positive for points above the belt surface. Z increases with each layer as expected. reverting and changing slice methodology Add pink slicing direction arrow from origin Shows the effective slicing direction (gantry normal) as a pink arrow from the origin. Shorter and wider than the gravity arrow. Direction: R(+alpha, X) * Z = (0, -sin(a), cos(a)), which is the layer stacking direction in the original mesh frame. Fix slicing arrow visibility and add raw G-code toggle - Disable depth test for pink slicing arrow so it renders on top of the tilted bed geometry (was being occluded) - Remove unnecessary 5mm Z-offset from arrow position - Add m_belt_show_raw toggle to GCodeViewer - Add "Show raw G-code (slicing frame)" checkbox in legend when belt mode is active Implement to_machine_coords inverse rotation for belt printer G-code The slicing pipeline rotates the mesh by R(-alpha, X) and shifts Z to start at 0. The G-code output now undoes this transform via to_machine_coords: R(+alpha, X) * T(0,0,+z_shift), recovering the original machine-frame coordinates where Y is horizontal and Z is vertical. Changes: - GCodeWriter: implement to_machine_coords with inverse rotation + Z-shift - GCodeWriter: add belt_z_shift member and setter/getter - GCode.cpp: compute Z-shift from print objects (same logic as PrintObjectSlice) and pass to writer; write z_shift to G-code header - GCodeProcessor: parse belt_z_shift from G-code header - GCodeViewer: store belt_z_shift from processor result Wire raw G-code toggle to apply slicing-frame view transform When "Show raw G-code (slicing frame)" is checked in the preview legend, the view matrix is modified to apply R(-alpha, X) * T(0,0,-z_shift) to the toolpath rendering. This shows the G-code as it was during slicing: rotated part with horizontal layers. Default (unchecked): machine-frame view — upright part with tilted layers. Remove belt printer placeholder comment from GCodeProcessor The preview now correctly displays machine-frame G-code with the optional raw view toggle. No transform is needed in the processor. --- src/libslic3r/GCode.cpp | 27 ++++++++- src/libslic3r/GCode/GCodeProcessor.cpp | 9 ++- src/libslic3r/GCode/GCodeProcessor.hpp | 3 +- src/libslic3r/GCodeWriter.cpp | 16 +++++- src/libslic3r/GCodeWriter.hpp | 7 ++- src/libslic3r/PrintObject.cpp | 19 +++++-- src/libslic3r/PrintObjectSlice.cpp | 20 ++++++- src/slic3r/GUI/3DBed.cpp | 79 ++++++++++++++++++++++++-- src/slic3r/GUI/3DBed.hpp | 2 + src/slic3r/GUI/GCodeViewer.cpp | 20 ++++++- src/slic3r/GUI/GCodeViewer.hpp | 2 + 11 files changed, 183 insertions(+), 21 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 7b881ea74f..dd9e477149 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -8,6 +8,7 @@ #include "Exception.hpp" #include "ExtrusionEntity.hpp" #include "EdgeGrid.hpp" +#include "Geometry.hpp" #include "Geometry/ConvexHull.hpp" #include "GCode/PrintExtents.hpp" #include "GCode/Thumbnails.hpp" @@ -2414,8 +2415,28 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_writer.set_is_bbl_machine(is_bbl_printers); // Belt printer: initialize coordinate transformation on the writer. - if (print.config().belt_printer.value) + if (print.config().belt_printer.value) { m_writer.set_belt_angle(print.config().belt_printer_angle.value); + // Compute the Z-shift that was applied during slicing (same logic as PrintObjectSlice.cpp). + // This is needed by to_machine_coords() to undo the slicing transform. + // For multiple objects, use the minimum min_z_rotated across all objects. + double angle_rad = Geometry::deg2rad(print.config().belt_printer_angle.value); + Transform3d belt_rotation = Transform3d::Identity(); + belt_rotation.rotate(Eigen::AngleAxisd(-angle_rad, Vec3d::UnitX())); + double global_min_z = std::numeric_limits::max(); + for (const PrintObject *obj : print.objects()) { + Transform3d obj_trafo = belt_rotation * obj->trafo_centered(); + for (const ModelVolume *mv : obj->model_object()->volumes) { + if (! mv->is_model_part() && ! mv->is_modifier()) + continue; + BoundingBoxf3 bb = mv->mesh().bounding_box(); + bb = bb.transformed(obj_trafo * mv->get_matrix()); + global_min_z = std::min(global_min_z, bb.min.z()); + } + } + if (global_min_z != std::numeric_limits::max() && std::abs(global_min_z) > EPSILON) + m_writer.set_belt_z_shift(global_min_z); // typically negative + } // How many times will be change_layer() called? // change_layer() in turn increments the progress bar status. @@ -2507,8 +2528,10 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Write information on the generator. file.write_format("; generated by %s on %s\n", Slic3r::header_slic3r_generated().c_str(), Slic3r::Utils::local_timestamp().c_str()); // Belt printer: embed angle in header for G-code processor detection. - if (print.config().belt_printer.value) + if (print.config().belt_printer.value) { file.write_format("; belt_printer_angle = %.1f\n", print.config().belt_printer_angle.value); + file.write_format("; belt_z_shift = %.4f\n", m_writer.belt_z_shift()); + } if (is_bbl_printers) file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str()); //BBS: total layer number diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 6c5c837b92..eddfc56440 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -2597,8 +2597,6 @@ void GCodeProcessor::finalize(bool post_process) } } - // Belt printer: preview coordinate transform placeholder (to be implemented in next cycle). - calculate_time(m_result); // process the time blocks @@ -3053,6 +3051,13 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers } catch (...) {} return; } + // Belt printer Z-shift for raw G-code toggle in preview. + if (boost::starts_with(comment, " belt_z_shift = ")) { + try { + m_result.belt_z_shift = std::stof(std::string(comment.substr(16))); + } catch (...) {} + return; + } // wipe start tag if (boost::starts_with(comment, reserved_tag(ETags::Wipe_Start))) { diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index e432a165a8..af68a5a6b7 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -227,8 +227,9 @@ class Print; bool support_traditional_timelapse{true}; float printable_height; float z_offset; - // Belt printer: angle for coordinate transformation in preview. + // Belt printer: angle and Z-shift for coordinate transformation in preview. float belt_printer_angle{ 0.f }; + float belt_z_shift{ 0.f }; SettingsIds settings_ids; size_t filaments_count; bool backtrace_enabled; diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index 7d4ac3babb..bb606b222e 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -29,8 +29,20 @@ void GCodeWriter::set_belt_angle(double angle_deg) Vec3d GCodeWriter::to_machine_coords(const Vec3d &pos) const { - // Belt printer: coordinate transform placeholder (to be implemented in next cycle). - return pos; + if (!is_belt_printer()) + return pos; + + // Undo the slicing transform to recover machine-frame coordinates. + // Slicing applied: T(0,0,-min_z_rot) * R(-alpha, X) * original_pos + // Inverse: R(+alpha, X) * T(0,0,+min_z_rot) * slicing_pos + // + // First undo the Z-shift, then apply R(+alpha, X). + double sz = pos.z() + m_belt_z_shift; // undo T(0,0,-min_z_rot) + return Vec3d( + pos.x(), + pos.y() * m_belt_cos - sz * m_belt_sin, // R(+alpha, X) + pos.y() * m_belt_sin + sz * m_belt_cos + ); } bool GCodeWriter::supports_separate_travel_acceleration(GCodeFlavor flavor) diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index 10159d941f..f9b5ed79ac 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -127,8 +127,10 @@ public: // Belt printer: set the belt angle and precompute sin/cos for coordinate transformation. void set_belt_angle(double angle_deg); + void set_belt_z_shift(double z_shift) { m_belt_z_shift = z_shift; } + double belt_z_shift() const { return m_belt_z_shift; } bool is_belt_printer() const { return m_belt_angle_rad != 0.; } - // Transform a point from the slicing frame to machine/world coordinates (inverse shear). + // Transform a point from the slicing frame to machine/world coordinates (inverse rotation). Vec3d to_machine_coords(const Vec3d &pos) const; // Returns whether this flavor supports separate print and travel acceleration. @@ -184,10 +186,11 @@ public: //SoftFever bool m_is_bbl_printers = false; - // Belt printer coordinate transformation (YZ shear) + // Belt printer coordinate transformation (inverse of slicing rotation) double m_belt_angle_rad = 0.; double m_belt_cos = 1.0; double m_belt_sin = 0.0; + double m_belt_z_shift = 0.; // Z-shift applied during slicing (min_z_rotated, typically negative) double m_current_speed; bool m_is_first_layer = true; diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 979953fb94..08ff847951 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -3392,7 +3392,13 @@ void PrintObject::update_slicing_parameters() // Orca: updated function call for XYZ shrinkage compensation if (!m_slicing_params.valid) { coordf_t object_height = this->model_object()->max_z(); - // Belt printer: height adjustment placeholder (to be implemented in next cycle). + if (this->print()->config().belt_printer.value) { + // After mesh rotation R(-alpha, X), effective height in the slicing + // direction is y_extent*sin(a) + z_extent*cos(a). + double angle_rad = Geometry::deg2rad(this->print()->config().belt_printer_angle.value); + BoundingBoxf3 bb = this->model_object()->raw_bounding_box(); + object_height = bb.size().y() * std::sin(angle_rad) + bb.size().z() * std::cos(angle_rad); + } m_slicing_params = SlicingParameters::create_from_config(this->print()->config(), m_config, object_height, this->object_extruders(), this->print()->shrinkage_compensation()); } @@ -3432,9 +3438,14 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full sort_remove_duplicates(object_extruders); //FIXME add painting extruders - if (object_max_z <= 0.f) - object_max_z = (float)model_object.raw_bounding_box().size().z(); - // Belt printer: height adjustment placeholder (to be implemented in next cycle). + if (object_max_z <= 0.f) { + BoundingBoxf3 bb = model_object.raw_bounding_box(); + object_max_z = (float)bb.size().z(); + if (print_config.belt_printer.value) { + double angle_rad = Geometry::deg2rad(print_config.belt_printer_angle.value); + object_max_z = (float)(bb.size().y() * std::sin(angle_rad) + bb.size().z() * std::cos(angle_rad)); + } + } return SlicingParameters::create_from_config(print_config, object_config, object_max_z, object_extruders, object_shrinkage_compensation); } diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index bba4593764..2748151aa0 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -140,7 +140,25 @@ static std::vector slice_volumes_inner( params_base.closing_radius = print_object_config.slice_closing_radius.value; params_base.extra_offset = 0; params_base.trafo = object_trafo; - // Belt printer: mesh transform placeholder (to be implemented in next cycle). + if (print_config.belt_printer.value) { + double angle_rad = Geometry::deg2rad(print_config.belt_printer_angle.value); + // Rotate mesh by -alpha about X so horizontal slice planes = gantry-parallel planes. + // The gantry (XY) is tilted by belt_printer_angle; this rotation aligns it with horizontal. + Transform3d belt_rotation = Transform3d::Identity(); + belt_rotation.rotate(Eigen::AngleAxisd(-angle_rad, Vec3d::UnitX())); + params_base.trafo = belt_rotation * params_base.trafo; + // Compute Z-shift from model_volumes: find min-Z of all rotated meshes + // so the rotated geometry starts at Z=0 (the belt surface in slicing frame). + double min_z_rotated = std::numeric_limits::max(); + for (const ModelVolume *mv : model_volumes) { + if (!model_volume_needs_slicing(*mv)) continue; + BoundingBoxf3 bb = mv->mesh().bounding_box(); + bb = bb.transformed(params_base.trafo * mv->get_matrix()); + min_z_rotated = std::min(min_z_rotated, bb.min.z()); + } + if (min_z_rotated != std::numeric_limits::max() && std::abs(min_z_rotated) > EPSILON) + params_base.trafo = Eigen::Translation3d(0, 0, -min_z_rotated) * params_base.trafo; + } //BBS: 0.0025mm is safe enough to simplify the data to speed slicing up for high-resolution model. //Also has on influence on arc fitting which has default resolution 0.0125mm. params_base.resolution = print_config.resolution <= 0.001 ? 0.0f : 0.0025; diff --git a/src/slic3r/GUI/3DBed.cpp b/src/slic3r/GUI/3DBed.cpp index 101279d744..858182946c 100644 --- a/src/slic3r/GUI/3DBed.cpp +++ b/src/slic3r/GUI/3DBed.cpp @@ -387,17 +387,18 @@ void Bed3D::render_internal(GLCanvas3D& canvas, const Transform3d& view_matrix, m_model.set_color(m_is_dark ? DEFAULT_MODEL_COLOR_DARK : DEFAULT_MODEL_COLOR); - // Belt printer: bed view transform placeholder (to be implemented in next cycle). - Transform3d belt_view_matrix = view_matrix; + // Belt printer: bed rotation is applied inside render_model() and render_default() + // using m_is_belt_printer and m_belt_angle members. switch (m_type) { - case Type::System: { render_system(canvas, belt_view_matrix, projection_matrix, bottom); break; } + case Type::System: { render_system(canvas, view_matrix, projection_matrix, bottom); break; } default: - case Type::Custom: { render_custom(canvas, belt_view_matrix, projection_matrix, bottom); break; } + case Type::Custom: { render_custom(canvas, view_matrix, projection_matrix, bottom); break; } } render_gravity_arrow(view_matrix, projection_matrix); + render_slicing_arrow(view_matrix, projection_matrix); render_slicing_plane(view_matrix, projection_matrix); glsafe(::glDisable(GL_DEPTH_TEST)); @@ -698,7 +699,13 @@ void Bed3D::render_model(const Transform3d& view_matrix, const Transform3d& proj if (shader != nullptr) { shader->start_using(); shader->set_uniform("emission_factor", 0.0f); - const Transform3d model_matrix = Geometry::assemble_transform(m_model_offset); + Transform3d model_matrix = Geometry::assemble_transform(m_model_offset); + // Belt printer: rotate the bed model about X so the belt tilt is visible. + // Negative angle: belt surface tilts downward away from the nozzle. + if (m_is_belt_printer && m_belt_angle > 0.f) { + double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle)); + model_matrix = Eigen::AngleAxisd(-angle_rad, Vec3d::UnitX()) * model_matrix; + } shader->set_uniform("volume_world_matrix", model_matrix); shader->set_uniform("view_model_matrix", view_matrix * model_matrix); shader->set_uniform("projection_matrix", projection_matrix); @@ -798,6 +805,58 @@ void Bed3D::render_gravity_arrow(const Transform3d& view_matrix, const Transform shader->stop_using(); } +void Bed3D::render_slicing_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix) +{ + if (!m_is_belt_printer || m_belt_angle <= 0.f) + return; + + // Build the arrow model: shorter and wider than the gravity arrow. + if (!m_slicing_arrow.is_initialized()) { + const float stem_length = 15.0f; // shorter than gravity arrow (25) + const float stem_radius = 1.0f; // wider than gravity arrow (~0.33) + const float tip_radius = 3.0f; // wider tip + const float tip_length = 5.0f; + m_slicing_arrow.init_from(stilized_arrow(16, tip_radius, tip_length, stem_radius, stem_length)); + } + + // The slicing direction: layers stack along the gantry normal. + // With mesh rotation R(-alpha, X), the slicing Z-axis in the original frame + // points in direction R(+alpha, X) * (0, 0, 1) = (0, -sin(alpha), cos(alpha)). + double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle)); + Vec3d slice_dir = Vec3d(0., -std::sin(angle_rad), std::cos(angle_rad)).normalized(); + + // Compute rotation to align +Z (arrow default) with slice_dir. + Vec3d from = Vec3d::UnitZ(); + double dot = from.dot(slice_dir); + Transform3d rot = Transform3d::Identity(); + if (dot < -0.9999) { + rot = Eigen::AngleAxisd(M_PI, Vec3d::UnitX()) * rot; + } else if (dot < 0.9999) { + Vec3d axis = from.cross(slice_dir).normalized(); + double angle = std::acos(std::clamp(dot, -1.0, 1.0)); + rot = Eigen::AngleAxisd(angle, axis) * rot; + } + + GLShaderProgram* shader = wxGetApp().get_shader("flat"); + if (shader == nullptr) + return; + + // Disable depth test so the arrow is always visible (not occluded by the tilted bed). + glsafe(::glDisable(GL_DEPTH_TEST)); + shader->start_using(); + + const Camera& camera = wxGetApp().plater()->get_camera(); + Transform3d model_matrix = rot; + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * model_matrix); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + + m_slicing_arrow.set_color({ 1.0f, 0.2f, 0.6f, 1.0f }); // pink + m_slicing_arrow.render(); + + shader->stop_using(); + glsafe(::glEnable(GL_DEPTH_TEST)); +} + void Bed3D::render_slicing_plane(const Transform3d& view_matrix, const Transform3d& projection_matrix) { if (!m_is_belt_printer || m_belt_angle <= 0.f) @@ -863,7 +922,15 @@ void Bed3D::render_default(bool bottom, const Transform3d& view_matrix, const Tr if (shader != nullptr) { shader->start_using(); - shader->set_uniform("view_model_matrix", view_matrix); + // Belt printer: rotate the default bed about X so the belt tilt is visible. + Transform3d view_model_matrix = view_matrix; + if (m_is_belt_printer && m_belt_angle > 0.f) { + double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle)); + Transform3d belt_rotation = Transform3d::Identity(); + belt_rotation.rotate(Eigen::AngleAxisd(-angle_rad, Vec3d::UnitX())); + view_model_matrix = view_matrix * belt_rotation; + } + shader->set_uniform("view_model_matrix", view_model_matrix); shader->set_uniform("projection_matrix", projection_matrix); glsafe(::glEnable(GL_DEPTH_TEST)); diff --git a/src/slic3r/GUI/3DBed.hpp b/src/slic3r/GUI/3DBed.hpp index 005dbff534..d01273961d 100644 --- a/src/slic3r/GUI/3DBed.hpp +++ b/src/slic3r/GUI/3DBed.hpp @@ -111,6 +111,7 @@ private: GLModel m_model; Vec3d m_model_offset{ Vec3d::Zero() }; GLModel m_gravity_arrow; + GLModel m_slicing_arrow; // Pink arrow showing the effective slicing direction GLModel m_slicing_plane; // Debug: shows the intended slicing plane direction Axes m_axes; @@ -189,6 +190,7 @@ private: void render_custom(GLCanvas3D& canvas, const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom); void render_default(bool bottom, const Transform3d& view_matrix, const Transform3d& projection_matrix); void render_gravity_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix); + void render_slicing_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix); void render_slicing_plane(const Transform3d& view_matrix, const Transform3d& projection_matrix); // BBS: remove the bed picking logic diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 7df0b06ff5..dd66c918a7 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1271,6 +1271,7 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const m_max_print_height = gcode_result.printable_height; m_z_offset = gcode_result.z_offset; + m_belt_z_shift = gcode_result.belt_z_shift; // load_toolpaths(gcode_result, build_volume, exclude_bounding_box); @@ -2209,7 +2210,16 @@ void GCodeViewer::render_toolpaths() { const Camera& camera = wxGetApp().plater()->get_camera(); Matrix4f view = camera.get_view_matrix().matrix().cast(); - // Belt view: view matrix transform placeholder (to be implemented in next cycle). + // Belt "raw" view: apply slicing rotation to view matrix so toolpaths appear + // in the slicing frame (rotated part with horizontal layers). + if (m_belt_show_raw && m_belt_view_enabled && m_belt_angle_deg > 0.f) { + double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle_deg)); + // Apply R(-alpha, X) * T(0,0,-z_shift) to bring machine coords back to slicing frame. + Transform3d slicing_trafo = Transform3d::Identity(); + slicing_trafo.translate(Vec3d(0., 0., -static_cast(m_belt_z_shift))); + slicing_trafo = Eigen::AngleAxisd(-angle_rad, Vec3d::UnitX()) * slicing_trafo; + view = (camera.get_view_matrix() * slicing_trafo).matrix().cast(); + } const libvgcode::Mat4x4 converted_view_matrix = libvgcode::convert(view); const libvgcode::Mat4x4 converted_projetion_matrix = libvgcode::convert(static_cast(camera.get_projection_matrix().matrix().cast())); #if VGCODE_ENABLE_COG_AND_TOOL_MARKERS @@ -4406,6 +4416,14 @@ 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 raw slicing-frame G-code + if (m_belt_view_enabled) { + ImGui::Spacing(); + ImGui::Dummy({ window_padding, 0 }); + ImGui::SameLine(); + ImGui::Checkbox("Show raw G-code (slicing frame)", &m_belt_show_raw); + } + legend_height = ImGui::GetCurrentWindow()->Size.y; imgui.end(); ImGui::PopStyleColor(7); diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index a299b93058..4bd3746991 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -239,6 +239,8 @@ mutable bool m_no_render_path { false }; bool m_belt_view_enabled = false; float m_belt_angle_deg = 0.f; + float m_belt_z_shift = 0.f; + bool m_belt_show_raw = false; // Toggle: show raw slicing-frame G-code (rotated part) libvgcode::Viewer m_viewer; bool m_loaded_as_preview{ false }; From 98f4d34dcb26257fdae9fc68ff9b4d3064afcb3b Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Thu, 12 Mar 2026 20:47:28 -0500 Subject: [PATCH 015/224] Part 2.5: Add global shear transform, support clipping, and belt UI improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implement per-object global shear transform in PrintObject with layer Z-offset calculation, config invalidation, and fix for shared-object layer optimization breaking copied objects - Clip support layers to the transformed belt floor plane and begin work on tree support adaptation for sheared coordinate space - Improve belt UI: gray out inactive sub-options, add B keyboard shortcut for G-code viewer design-view toggle, fix mesh clipping through build plate after shear/scale transform y' = y + z·cot(α), while x' = x and z' = z getting closer to customizable variant getting closer X/Y/Z shear initial clean up UI add 1/sin(a) transform, idea taken from blackbelt cura plugin Things work now (turns out I've been using the wrong set of transforms) --- src/libslic3r/GCode.cpp | 31 ++---- src/libslic3r/GCode/GCodeProcessor.cpp | 8 -- src/libslic3r/GCode/GCodeProcessor.hpp | 3 +- src/libslic3r/GCodeWriter.cpp | 34 ++++--- src/libslic3r/GCodeWriter.hpp | 17 ++-- src/libslic3r/Preset.cpp | 6 +- src/libslic3r/PrintConfig.cpp | 135 +++++++++++++++++++++++++ src/libslic3r/PrintConfig.hpp | 54 ++++++++++ src/libslic3r/PrintObject.cpp | 101 ++++++++++++++++-- src/libslic3r/PrintObjectSlice.cpp | 78 +++++++++++--- src/slic3r/GUI/GCodeViewer.cpp | 22 ++-- src/slic3r/GUI/GCodeViewer.hpp | 3 +- src/slic3r/GUI/Tab.cpp | 51 ++++++++++ 13 files changed, 452 insertions(+), 91 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index dd9e477149..1aeb5497a3 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2411,31 +2411,19 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_is_role_based_fan_on.fill(false); m_fan_mover.release(); - + m_writer.set_is_bbl_machine(is_bbl_printers); - // Belt printer: initialize coordinate transformation on the writer. + // Belt printer: initialize coordinate transformation and axis remap on the writer. if (print.config().belt_printer.value) { m_writer.set_belt_angle(print.config().belt_printer_angle.value); - // Compute the Z-shift that was applied during slicing (same logic as PrintObjectSlice.cpp). - // This is needed by to_machine_coords() to undo the slicing transform. - // For multiple objects, use the minimum min_z_rotated across all objects. - double angle_rad = Geometry::deg2rad(print.config().belt_printer_angle.value); - Transform3d belt_rotation = Transform3d::Identity(); - belt_rotation.rotate(Eigen::AngleAxisd(-angle_rad, Vec3d::UnitX())); - double global_min_z = std::numeric_limits::max(); - for (const PrintObject *obj : print.objects()) { - Transform3d obj_trafo = belt_rotation * obj->trafo_centered(); - for (const ModelVolume *mv : obj->model_object()->volumes) { - if (! mv->is_model_part() && ! mv->is_modifier()) - continue; - BoundingBoxf3 bb = mv->mesh().bounding_box(); - bb = bb.transformed(obj_trafo * mv->get_matrix()); - global_min_z = std::min(global_min_z, bb.min.z()); - } - } - if (global_min_z != std::numeric_limits::max() && std::abs(global_min_z) > EPSILON) - m_writer.set_belt_z_shift(global_min_z); // typically negative + m_writer.set_axis_remap( + int(print.config().belt_gcode_remap_x.value), + int(print.config().belt_gcode_remap_y.value), + int(print.config().belt_gcode_remap_z.value)); + // Build volume extents for Rev remap mode. + BoundingBoxf bbox_bed(print.config().printable_area.values); + m_writer.set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(), print.config().printable_height.value)); } // How many times will be change_layer() called? @@ -2530,7 +2518,6 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Belt printer: embed angle in header for G-code processor detection. if (print.config().belt_printer.value) { file.write_format("; belt_printer_angle = %.1f\n", print.config().belt_printer_angle.value); - file.write_format("; belt_z_shift = %.4f\n", m_writer.belt_z_shift()); } if (is_bbl_printers) file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str()); diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index eddfc56440..5dd2c0fc84 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -3051,14 +3051,6 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers } catch (...) {} return; } - // Belt printer Z-shift for raw G-code toggle in preview. - if (boost::starts_with(comment, " belt_z_shift = ")) { - try { - m_result.belt_z_shift = std::stof(std::string(comment.substr(16))); - } catch (...) {} - return; - } - // wipe start tag if (boost::starts_with(comment, reserved_tag(ETags::Wipe_Start))) { m_wiping = true; diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index af68a5a6b7..e432a165a8 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -227,9 +227,8 @@ class Print; bool support_traditional_timelapse{true}; float printable_height; float z_offset; - // Belt printer: angle and Z-shift for coordinate transformation in preview. + // Belt printer: angle for coordinate transformation in preview. float belt_printer_angle{ 0.f }; - float belt_z_shift{ 0.f }; SettingsIds settings_ids; size_t filaments_count; bool backtrace_enabled; diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index bb606b222e..da655a6548 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -23,26 +23,32 @@ bool GCodeWriter::full_gcode_comment = true; void GCodeWriter::set_belt_angle(double angle_deg) { m_belt_angle_rad = Geometry::deg2rad(angle_deg); - m_belt_cos = std::cos(m_belt_angle_rad); - m_belt_sin = std::sin(m_belt_angle_rad); +} + +void GCodeWriter::set_axis_remap(int rx, int ry, int rz) +{ + m_remap_x = rx; + m_remap_y = ry; + m_remap_z = rz; +} + +void GCodeWriter::set_build_volume_max(const Vec3d &max) +{ + m_build_vol_max = max; } Vec3d GCodeWriter::to_machine_coords(const Vec3d &pos) const { if (!is_belt_printer()) return pos; - - // Undo the slicing transform to recover machine-frame coordinates. - // Slicing applied: T(0,0,-min_z_rot) * R(-alpha, X) * original_pos - // Inverse: R(+alpha, X) * T(0,0,+min_z_rot) * slicing_pos - // - // First undo the Z-shift, then apply R(+alpha, X). - double sz = pos.z() + m_belt_z_shift; // undo T(0,0,-min_z_rot) - return Vec3d( - pos.x(), - pos.y() * m_belt_cos - sz * m_belt_sin, // R(+alpha, X) - pos.y() * m_belt_sin + sz * m_belt_cos - ); + // BeltRemapAxis: 0-2 = +X/+Y/+Z, 3-5 = -X/-Y/-Z, 6-8 = Rev X/Y/Z + auto remap = [this, &pos](int r) -> double { + int axis = r % 3; + if (r < 3) return pos[axis]; + if (r < 6) return -pos[axis]; + return m_build_vol_max[axis] - pos[axis]; + }; + return { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) }; } bool GCodeWriter::supports_separate_travel_acceleration(GCodeFlavor flavor) diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index f9b5ed79ac..6074eb247c 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -127,10 +127,12 @@ public: // Belt printer: set the belt angle and precompute sin/cos for coordinate transformation. void set_belt_angle(double angle_deg); - void set_belt_z_shift(double z_shift) { m_belt_z_shift = z_shift; } - double belt_z_shift() const { return m_belt_z_shift; } bool is_belt_printer() const { return m_belt_angle_rad != 0.; } - // Transform a point from the slicing frame to machine/world coordinates (inverse rotation). + // Set axis remap for G-code output (BeltRemapAxis enum values). + void set_axis_remap(int rx, int ry, int rz); + // Set build volume extents for Rev remap mode (max X, Y, Z). + void set_build_volume_max(const Vec3d &max); + // Transform a point from the slicing frame to machine coordinates. Vec3d to_machine_coords(const Vec3d &pos) const; // Returns whether this flavor supports separate print and travel acceleration. @@ -186,11 +188,12 @@ public: //SoftFever bool m_is_bbl_printers = false; - // Belt printer coordinate transformation (inverse of slicing rotation) + // Belt printer state double m_belt_angle_rad = 0.; - double m_belt_cos = 1.0; - double m_belt_sin = 0.0; - double m_belt_z_shift = 0.; // Z-shift applied during slicing (min_z_rotated, typically negative) + int m_remap_x = 0; + int m_remap_y = 1; + int m_remap_z = 2; + Vec3d m_build_vol_max = Vec3d::Zero(); double m_current_speed; bool m_is_first_layer = true; diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 6d8c113480..f2a0cd9d24 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1010,7 +1010,11 @@ static std::vector s_Preset_machine_limits_options { static std::vector s_Preset_printer_options { "printer_technology", - "printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "build_plate_tilt_x", "build_plate_tilt_y", "belt_printer", "belt_printer_angle", "belt_printer_infinite_y", "gcode_flavor", + "printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "build_plate_tilt_x", "build_plate_tilt_y", "belt_printer", "belt_printer_angle", "belt_printer_infinite_y", "belt_shear_x", "belt_shear_x_angle", "belt_shear_x_from", + "belt_shear_y", "belt_shear_y_angle", "belt_shear_y_from", + "belt_shear_z", "belt_shear_z_angle", "belt_shear_z_from", + "belt_scale_x", "belt_scale_x_angle", "belt_scale_y", "belt_scale_y_angle", "belt_scale_z", "belt_scale_z_angle", + "belt_gcode_remap_x", "belt_gcode_remap_y", "belt_gcode_remap_z", "gcode_flavor", "fan_kickstart", "fan_speedup_time", "fan_speedup_overhangs", "single_extruder_multi_material", "manual_filament_change", "file_start_gcode", "machine_start_gcode", "machine_end_gcode", "before_layer_change_gcode", "printing_by_object_gcode", "layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode", "change_filament_gcode", "change_extrusion_role_gcode", "printer_model", "printer_variant", "printer_extruder_id", "printer_extruder_variant", "extruder_variant_list", "default_nozzle_volume_type", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 976a35c126..7290af09fb 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -288,6 +288,44 @@ static t_config_enum_values s_keys_map_SlicingMode { }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(SlicingMode) +static t_config_enum_values s_keys_map_BeltShearMode { + { "none", int(BeltShearMode::None) }, + { "pos_cot", int(BeltShearMode::PosCot) }, + { "neg_cot", int(BeltShearMode::NegCot) }, + { "pos_tan", int(BeltShearMode::PosTan) }, + { "neg_tan", int(BeltShearMode::NegTan) }, +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltShearMode) + +static t_config_enum_values s_keys_map_BeltAxis { + { "x", int(BeltAxis::X) }, + { "y", int(BeltAxis::Y) }, + { "z", int(BeltAxis::Z) }, +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltAxis) + +static t_config_enum_values s_keys_map_BeltScaleMode { + { "none", int(BeltScaleMode::None) }, + { "inv_sin", int(BeltScaleMode::InvSin) }, + { "inv_cos", int(BeltScaleMode::InvCos) }, + { "sin", int(BeltScaleMode::Sin) }, + { "cos", int(BeltScaleMode::Cos) }, +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltScaleMode) + +static t_config_enum_values s_keys_map_BeltRemapAxis { + { "pos_x", int(BeltRemapAxis::PosX) }, + { "pos_y", int(BeltRemapAxis::PosY) }, + { "pos_z", int(BeltRemapAxis::PosZ) }, + { "neg_x", int(BeltRemapAxis::NegX) }, + { "neg_y", int(BeltRemapAxis::NegY) }, + { "neg_z", int(BeltRemapAxis::NegZ) }, + { "rev_x", int(BeltRemapAxis::RevX) }, + { "rev_y", int(BeltRemapAxis::RevY) }, + { "rev_z", int(BeltRemapAxis::RevZ) }, +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltRemapAxis) + static t_config_enum_values s_keys_map_SupportMaterialPattern { { "rectilinear", smpRectilinear }, { "rectilinear-grid", smpRectilinearGrid }, @@ -5978,6 +6016,103 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionBool(true)); + // Per-axis shear controls for belt printer + auto add_belt_shear_mode = [this](const char *key, const char *label, BeltShearMode default_mode) { + auto def = this->add(key, coEnum); + def->label = L(label); + def->category = L("Printable space"); + def->tooltip = L("Shear function applied to this axis in belt printer mode."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values = {"none", "pos_cot", "neg_cot", "pos_tan", "neg_tan"}; + def->enum_labels = {L("None"), L("+cot(α)"), L("-cot(α)"), L("+tan(α)"), L("-tan(α)")}; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(default_mode)); + }; + auto add_belt_shear_angle = [this](const char *key, const char *label) { + auto def = this->add(key, coFloat); + def->label = L(label); + def->category = L("Printable space"); + def->tooltip = L("Angle (degrees) for the shear function on this axis."); + def->sidetext = L("°"); + def->min = 0.1; + def->max = 89.9; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(45)); + }; + auto add_belt_axis_enum = [this](const char *key, const char *label, const char *tooltip, BeltAxis default_axis) { + auto def = this->add(key, coEnum); + def->label = L(label); + def->category = L("Printable space"); + def->tooltip = L(tooltip); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values = {"x", "y", "z"}; + def->enum_labels = {L("X"), L("Y"), L("Z")}; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(default_axis)); + }; + + add_belt_shear_mode("belt_shear_x", "Function", BeltShearMode::None); + add_belt_shear_angle("belt_shear_x_angle", "Angle"); + add_belt_axis_enum("belt_shear_x_from", "From", "Source axis for X shear.", BeltAxis::Z); + + add_belt_shear_mode("belt_shear_y", "Function", BeltShearMode::PosCot); + add_belt_shear_angle("belt_shear_y_angle", "Angle"); + add_belt_axis_enum("belt_shear_y_from", "From", "Source axis for Y shear.", BeltAxis::Z); + + add_belt_shear_mode("belt_shear_z", "Function", BeltShearMode::None); + add_belt_shear_angle("belt_shear_z_angle", "Angle"); + add_belt_axis_enum("belt_shear_z_from", "From", "Source axis for Z shear.", BeltAxis::Y); + + // Per-axis scale controls for belt printer + auto add_belt_scale_mode = [this](const char *key, const char *label, BeltScaleMode default_mode) { + auto def = this->add(key, coEnum); + def->label = L(label); + def->category = L("Printable space"); + def->tooltip = L("Scale factor applied to this axis in belt printer mode."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values = {"none", "inv_sin", "inv_cos", "sin", "cos"}; + def->enum_labels = {L("None"), L("1/sin(α)"), L("1/cos(α)"), L("sin(α)"), L("cos(α)")}; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(default_mode)); + }; + auto add_belt_scale_angle = [this](const char *key, const char *label) { + auto def = this->add(key, coFloat); + def->label = L(label); + def->category = L("Printable space"); + def->tooltip = L("Angle (degrees) for the scale function on this axis."); + def->sidetext = L("°"); + def->min = 0.1; + def->max = 89.9; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(45)); + }; + + add_belt_scale_mode("belt_scale_x", "Function", BeltScaleMode::None); + add_belt_scale_angle("belt_scale_x_angle", "Angle"); + + add_belt_scale_mode("belt_scale_y", "Function", BeltScaleMode::None); + add_belt_scale_angle("belt_scale_y_angle", "Angle"); + + add_belt_scale_mode("belt_scale_z", "Function", BeltScaleMode::None); + add_belt_scale_angle("belt_scale_z_angle", "Angle"); + + // G-code axis remap with sign + auto add_belt_remap = [this](const char *key, const char *label, const char *tooltip, BeltRemapAxis default_axis) { + auto def = this->add(key, coEnum); + def->label = L(label); + def->category = L("Printable space"); + def->tooltip = L(tooltip); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values = {"pos_x", "pos_y", "pos_z", "neg_x", "neg_y", "neg_z", "rev_x", "rev_y", "rev_z"}; + def->enum_labels = {L("+X"), L("+Y"), L("+Z"), L("-X"), L("-Y"), L("-Z"), L("Rev X"), L("Rev Y"), L("Rev Z")}; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(default_axis)); + }; + + add_belt_remap("belt_gcode_remap_x", "X", "Which slicing axis maps to machine X in G-code output.", BeltRemapAxis::PosX); + add_belt_remap("belt_gcode_remap_y", "Y", "Which slicing axis maps to machine Y in G-code output.", BeltRemapAxis::PosY); + add_belt_remap("belt_gcode_remap_z", "Z", "Which slicing axis maps to machine Z in G-code output.", BeltRemapAxis::PosZ); + def = this->add("tree_support_branch_angle", coFloat); def->label = L("Tree support branch angle"); def->category = L("Support"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 080956c1dd..68e9425a82 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -156,6 +156,38 @@ enum class SlicingMode CloseHoles, }; +enum class BeltShearMode +{ + None, // No shear on this axis + PosCot, // += cot(α) + NegCot, // -= cot(α) + PosTan, // += tan(α) + NegTan, // -= tan(α) +}; + +enum class BeltScaleMode +{ + None, // No scaling (factor = 1) + InvSin, // 1/sin(α) + InvCos, // 1/cos(α) + Sin, // sin(α) + Cos, // cos(α) +}; + +enum class BeltAxis +{ + X = 0, + Y = 1, + Z = 2, +}; + +enum class BeltRemapAxis +{ + PosX = 0, PosY = 1, PosZ = 2, + NegX = 3, NegY = 4, NegZ = 5, + RevX = 6, RevY = 7, RevZ = 8, // Reversed: max - pos +}; + enum SupportMaterialPattern { smpDefault, smpRectilinear, smpRectilinearGrid, smpHoneycomb, @@ -499,6 +531,10 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(NoiseType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(InfillPattern) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(IroningType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SlicingMode) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltShearMode) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltScaleMode) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltAxis) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltRemapAxis) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialPattern) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialStyle) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialInterfacePattern) @@ -1416,6 +1452,24 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionBool, belt_printer)) ((ConfigOptionFloat, belt_printer_angle)) ((ConfigOptionBool, belt_printer_infinite_y)) + ((ConfigOptionEnum, belt_shear_x)) + ((ConfigOptionFloat, belt_shear_x_angle)) + ((ConfigOptionEnum, belt_shear_x_from)) + ((ConfigOptionEnum, belt_shear_y)) + ((ConfigOptionFloat, belt_shear_y_angle)) + ((ConfigOptionEnum, belt_shear_y_from)) + ((ConfigOptionEnum, belt_shear_z)) + ((ConfigOptionFloat, belt_shear_z_angle)) + ((ConfigOptionEnum, belt_shear_z_from)) + ((ConfigOptionEnum, belt_scale_x)) + ((ConfigOptionFloat, belt_scale_x_angle)) + ((ConfigOptionEnum, belt_scale_y)) + ((ConfigOptionFloat, belt_scale_y_angle)) + ((ConfigOptionEnum, belt_scale_z)) + ((ConfigOptionFloat, belt_scale_z_angle)) + ((ConfigOptionEnum, belt_gcode_remap_x)) + ((ConfigOptionEnum, belt_gcode_remap_y)) + ((ConfigOptionEnum, belt_gcode_remap_z)) //BBS ((ConfigOptionInts, additional_cooling_fan_speed)) ((ConfigOptionBool, reduce_crossing_wall)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 08ff847951..abd309fbff 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -3392,14 +3392,55 @@ void PrintObject::update_slicing_parameters() // Orca: updated function call for XYZ shrinkage compensation if (!m_slicing_params.valid) { coordf_t object_height = this->model_object()->max_z(); - if (this->print()->config().belt_printer.value) { - // After mesh rotation R(-alpha, X), effective height in the slicing - // direction is y_extent*sin(a) + z_extent*cos(a). - double angle_rad = Geometry::deg2rad(this->print()->config().belt_printer_angle.value); - BoundingBoxf3 bb = this->model_object()->raw_bounding_box(); - object_height = bb.size().y() * std::sin(angle_rad) + bb.size().z() * std::cos(angle_rad); + // Belt shear/scale may change the effective Z height. + const auto &pcfg = this->print()->config(); + if (pcfg.belt_printer.value) { + bool has_z_shear = pcfg.belt_shear_z.value != BeltShearMode::None; + bool has_z_scale = pcfg.belt_scale_z.value != BeltScaleMode::None; + if (has_z_shear || has_z_scale) { + auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad); + switch (mode) { + case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; + case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; + case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; + case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; + default: return 0.; + } + }; + auto compute_scale_factor = [](BeltScaleMode mode, double angle_deg) -> double { + if (mode == BeltScaleMode::None) return 1.; + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad); + switch (mode) { + case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; + case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; + case BeltScaleMode::Sin: return sin_a; + case BeltScaleMode::Cos: return cos_a; + default: return 1.; + } + }; + double shear_factor = has_z_shear ? compute_shear_factor(pcfg.belt_shear_z.value, pcfg.belt_shear_z_angle.value) : 0.; + double scale_z = compute_scale_factor(pcfg.belt_scale_z.value, pcfg.belt_scale_z_angle.value); + if (has_z_shear && std::abs(shear_factor) > EPSILON) { + int from = int(pcfg.belt_shear_z_from.value); + BoundingBoxf3 bb = this->model_object()->raw_bounding_box(); + double min_rz = std::numeric_limits::max(); + double max_rz = std::numeric_limits::lowest(); + for (double vz : {bb.min.z(), bb.max.z()}) + for (double vs : {bb.min(from), bb.max(from)}) { + double new_z = scale_z * (vz + shear_factor * vs); + min_rz = std::min(min_rz, new_z); + max_rz = std::max(max_rz, new_z); + } + object_height = max_rz - min_rz; + } else { + object_height *= scale_z; + } + } } - m_slicing_params = SlicingParameters::create_from_config(this->print()->config(), m_config, object_height, + m_slicing_params = SlicingParameters::create_from_config(pcfg, m_config, object_height, this->object_extruders(), this->print()->shrinkage_compensation()); } } @@ -3441,9 +3482,51 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full if (object_max_z <= 0.f) { BoundingBoxf3 bb = model_object.raw_bounding_box(); object_max_z = (float)bb.size().z(); + // Belt shear/scale may change the effective Z height. if (print_config.belt_printer.value) { - double angle_rad = Geometry::deg2rad(print_config.belt_printer_angle.value); - object_max_z = (float)(bb.size().y() * std::sin(angle_rad) + bb.size().z() * std::cos(angle_rad)); + bool has_z_shear = print_config.belt_shear_z.value != BeltShearMode::None; + bool has_z_scale = print_config.belt_scale_z.value != BeltScaleMode::None; + if (has_z_shear || has_z_scale) { + auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad); + switch (mode) { + case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; + case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; + case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; + case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; + default: return 0.; + } + }; + auto compute_scale_factor = [](BeltScaleMode mode, double angle_deg) -> double { + if (mode == BeltScaleMode::None) return 1.; + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad); + switch (mode) { + case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; + case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; + case BeltScaleMode::Sin: return sin_a; + case BeltScaleMode::Cos: return cos_a; + default: return 1.; + } + }; + double shear_factor = has_z_shear ? compute_shear_factor(print_config.belt_shear_z.value, print_config.belt_shear_z_angle.value) : 0.; + double scale_z = compute_scale_factor(print_config.belt_scale_z.value, print_config.belt_scale_z_angle.value); + if (has_z_shear && std::abs(shear_factor) > EPSILON) { + int from = int(print_config.belt_shear_z_from.value); + double min_rz = std::numeric_limits::max(); + double max_rz = std::numeric_limits::lowest(); + for (double vz : {bb.min.z(), bb.max.z()}) + for (double vs : {bb.min(from), bb.max(from)}) { + double new_z = scale_z * (vz + shear_factor * vs); + min_rz = std::min(min_rz, new_z); + max_rz = std::max(max_rz, new_z); + } + object_max_z = (float)(max_rz - min_rz); + } else { + object_max_z *= (float)scale_z; + } + } } } return SlicingParameters::create_from_config(print_config, object_config, object_max_z, object_extruders, object_shrinkage_compensation); diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 2748151aa0..294c245eaa 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -141,23 +141,69 @@ static std::vector slice_volumes_inner( params_base.extra_offset = 0; params_base.trafo = object_trafo; if (print_config.belt_printer.value) { - double angle_rad = Geometry::deg2rad(print_config.belt_printer_angle.value); - // Rotate mesh by -alpha about X so horizontal slice planes = gantry-parallel planes. - // The gantry (XY) is tilted by belt_printer_angle; this rotation aligns it with horizontal. - Transform3d belt_rotation = Transform3d::Identity(); - belt_rotation.rotate(Eigen::AngleAxisd(-angle_rad, Vec3d::UnitX())); - params_base.trafo = belt_rotation * params_base.trafo; - // Compute Z-shift from model_volumes: find min-Z of all rotated meshes - // so the rotated geometry starts at Z=0 (the belt surface in slicing frame). - double min_z_rotated = std::numeric_limits::max(); - for (const ModelVolume *mv : model_volumes) { - if (!model_volume_needs_slicing(*mv)) continue; - BoundingBoxf3 bb = mv->mesh().bounding_box(); - bb = bb.transformed(params_base.trafo * mv->get_matrix()); - min_z_rotated = std::min(min_z_rotated, bb.min.z()); + // Build per-axis shear matrix from 3 independent axis configs. + auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad); + double cos_a = std::cos(angle_rad); + switch (mode) { + case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; + case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; + case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; + case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; + default: return 0.; + } + }; + + struct AxisShear { BeltShearMode mode; double angle; int from; }; + AxisShear axes[3] = { + { print_config.belt_shear_x.value, print_config.belt_shear_x_angle.value, int(print_config.belt_shear_x_from.value) }, + { print_config.belt_shear_y.value, print_config.belt_shear_y_angle.value, int(print_config.belt_shear_y_from.value) }, + { print_config.belt_shear_z.value, print_config.belt_shear_z_angle.value, int(print_config.belt_shear_z_from.value) }, + }; + + Transform3d belt_shear = Transform3d::Identity(); + bool has_shear = false; + for (int row = 0; row < 3; ++row) { + if (axes[row].mode != BeltShearMode::None) { + double factor = compute_shear_factor(axes[row].mode, axes[row].angle); + if (std::abs(factor) > EPSILON) { + belt_shear.matrix()(row, axes[row].from) += factor; + has_shear = true; + } + } } - if (min_z_rotated != std::numeric_limits::max() && std::abs(min_z_rotated) > EPSILON) - params_base.trafo = Eigen::Translation3d(0, 0, -min_z_rotated) * params_base.trafo; + + // Build per-axis scale matrix. + auto compute_scale_factor = [](BeltScaleMode mode, double angle_deg) -> double { + if (mode == BeltScaleMode::None) return 1.; + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad); + double cos_a = std::cos(angle_rad); + switch (mode) { + case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; + case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; + case BeltScaleMode::Sin: return sin_a; + case BeltScaleMode::Cos: return cos_a; + default: return 1.; + } + }; + + Transform3d belt_scale = Transform3d::Identity(); + bool has_scale = false; + double sx = compute_scale_factor(print_config.belt_scale_x.value, print_config.belt_scale_x_angle.value); + double sy = compute_scale_factor(print_config.belt_scale_y.value, print_config.belt_scale_y_angle.value); + double sz = compute_scale_factor(print_config.belt_scale_z.value, print_config.belt_scale_z_angle.value); + if (std::abs(sx - 1.) > EPSILON || std::abs(sy - 1.) > EPSILON || std::abs(sz - 1.) > EPSILON) { + belt_scale.matrix()(0, 0) = sx; + belt_scale.matrix()(1, 1) = sy; + belt_scale.matrix()(2, 2) = sz; + has_scale = true; + } + + // Apply: scale * shear * trafo (shear first, then scale). + if (has_shear || has_scale) + params_base.trafo = belt_scale * belt_shear * params_base.trafo; } //BBS: 0.0025mm is safe enough to simplify the data to speed slicing up for high-resolution model. //Also has on influence on arc fitting which has default resolution 0.0125mm. diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index dd66c918a7..25b4ba4e9c 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1271,7 +1271,7 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const m_max_print_height = gcode_result.printable_height; m_z_offset = gcode_result.z_offset; - m_belt_z_shift = gcode_result.belt_z_shift; + // load_toolpaths(gcode_result, build_volume, exclude_bounding_box); @@ -2210,15 +2210,17 @@ void GCodeViewer::render_toolpaths() { const Camera& camera = wxGetApp().plater()->get_camera(); Matrix4f view = camera.get_view_matrix().matrix().cast(); - // Belt "raw" view: apply slicing rotation to view matrix so toolpaths appear - // in the slicing frame (rotated part with horizontal layers). - if (m_belt_show_raw && m_belt_view_enabled && m_belt_angle_deg > 0.f) { + // Belt "designed" view: apply inverse shear to view matrix so toolpaths appear + // upright (as originally designed) instead of sheared on the belt. + if (m_belt_show_designed && m_belt_view_enabled && m_belt_angle_deg > 0.f) { double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle_deg)); - // Apply R(-alpha, X) * T(0,0,-z_shift) to bring machine coords back to slicing frame. - Transform3d slicing_trafo = Transform3d::Identity(); - slicing_trafo.translate(Vec3d(0., 0., -static_cast(m_belt_z_shift))); - slicing_trafo = Eigen::AngleAxisd(-angle_rad, Vec3d::UnitX()) * slicing_trafo; - view = (camera.get_view_matrix() * slicing_trafo).matrix().cast(); + double sin_a = std::sin(angle_rad); + if (sin_a > 1e-6) { + double cot_alpha = std::cos(angle_rad) / sin_a; + Transform3d inverse_shear = Transform3d::Identity(); + inverse_shear.matrix()(1, 2) = -cot_alpha; // Y -= Z * cot(α) + view = (camera.get_view_matrix() * inverse_shear).matrix().cast(); + } } const libvgcode::Mat4x4 converted_view_matrix = libvgcode::convert(view); const libvgcode::Mat4x4 converted_projetion_matrix = libvgcode::convert(static_cast(camera.get_projection_matrix().matrix().cast())); @@ -4421,7 +4423,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv ImGui::Spacing(); ImGui::Dummy({ window_padding, 0 }); ImGui::SameLine(); - ImGui::Checkbox("Show raw G-code (slicing frame)", &m_belt_show_raw); + ImGui::Checkbox("Show designed view (upright)", &m_belt_show_designed); } legend_height = ImGui::GetCurrentWindow()->Size.y; diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 4bd3746991..184c56e915 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -239,8 +239,7 @@ mutable bool m_no_render_path { false }; bool m_belt_view_enabled = false; float m_belt_angle_deg = 0.f; - float m_belt_z_shift = 0.f; - bool m_belt_show_raw = false; // Toggle: show raw slicing-frame G-code (rotated part) + bool m_belt_show_designed = false; // Toggle: show designed (upright) view via inverse shear libvgcode::Viewer m_viewer; bool m_loaded_as_preview{ false }; diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 3dce081708..9d1ee6b2ad 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4370,6 +4370,53 @@ void TabPrinter::build_fff() optgroup->append_single_option_line("belt_printer"); optgroup->append_single_option_line("belt_printer_angle"); optgroup->append_single_option_line("belt_printer_infinite_y"); + // Per-axis shear: group mode + angle + source on one row per axis + { + Line line = { L("Shear X"), L("Shear applied to the X axis before slicing") }; + line.append_option(optgroup->get_option("belt_shear_x")); + line.append_option(optgroup->get_option("belt_shear_x_angle")); + line.append_option(optgroup->get_option("belt_shear_x_from")); + optgroup->append_line(line); + } + { + Line line = { L("Shear Y"), L("Shear applied to the Y axis before slicing") }; + line.append_option(optgroup->get_option("belt_shear_y")); + line.append_option(optgroup->get_option("belt_shear_y_angle")); + line.append_option(optgroup->get_option("belt_shear_y_from")); + optgroup->append_line(line); + } + { + Line line = { L("Shear Z"), L("Shear applied to the Z axis before slicing") }; + line.append_option(optgroup->get_option("belt_shear_z")); + line.append_option(optgroup->get_option("belt_shear_z_angle")); + line.append_option(optgroup->get_option("belt_shear_z_from")); + optgroup->append_line(line); + } + { + Line line = { L("Scale X"), L("Scale applied to the X axis before slicing") }; + line.append_option(optgroup->get_option("belt_scale_x")); + line.append_option(optgroup->get_option("belt_scale_x_angle")); + optgroup->append_line(line); + } + { + Line line = { L("Scale Y"), L("Scale applied to the Y axis before slicing") }; + line.append_option(optgroup->get_option("belt_scale_y")); + line.append_option(optgroup->get_option("belt_scale_y_angle")); + optgroup->append_line(line); + } + { + Line line = { L("Scale Z"), L("Scale applied to the Z axis before slicing") }; + line.append_option(optgroup->get_option("belt_scale_z")); + line.append_option(optgroup->get_option("belt_scale_z_angle")); + optgroup->append_line(line); + } + { + Line line = { L("G-code axis remap"), L("Remap slicing-frame axes to machine axes in G-code output") }; + line.append_option(optgroup->get_option("belt_gcode_remap_x")); + line.append_option(optgroup->get_option("belt_gcode_remap_y")); + line.append_option(optgroup->get_option("belt_gcode_remap_z")); + optgroup->append_line(line); + } optgroup->append_single_option_line("support_multi_bed_types","printer_basic_information_printable_space#support-multi-bed-types"); optgroup->append_single_option_line("best_object_pos", "printer_basic_information_printable_space#best-object-position"); // todo: for multi_extruder test @@ -5233,6 +5280,10 @@ void TabPrinter::toggle_options() bool is_belt = m_config->opt_bool("belt_printer"); toggle_line("belt_printer_angle", is_belt); toggle_line("belt_printer_infinite_y", is_belt); + for (auto el : {"belt_shear_x", "belt_shear_y", "belt_shear_z", + "belt_scale_x", "belt_scale_y", "belt_scale_z", + "belt_gcode_remap_x"}) + toggle_line(el, is_belt); } From ea5c6776b33de30ec157e18350922bb9d39a038a Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 18 Mar 2026 12:09:28 -0500 Subject: [PATCH 016/224] Part 2.6: Add belt floor support clipping for all support types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix support clipping z-shift calculation by removing coordinate-space mismatch and sync belt_floor_z_shift with global_z_offset; fix invalidation so posSupportMaterial no longer resets slicing params - Add belt floor polygon clipping to non-organic tree support (slim/strong/hybrid) with collision surface integration in TreeSupportData, belt extension layers, and first-layer brim suppression - Add belt floor clipping to organic tree support pipeline with virtual belt raft layers, per-layer polygons in TreeModelVolumes, and post-generation layer trimming; fix pre-existing processing_last_mesh bug in calculateCollision() Fix belt floor support clipping: z-shift, invalidation, and global offset - Fix support clipping z-shift calculation by removing coordinate-space mismatch (raw_bounding_box min.z vs trafo_centered m_belt_min_z) and sync belt_floor_z_shift with global_z_offset in global shear mode - Fix invalidation so posSupportMaterial no longer resets slicing params, preventing the exact posSlice z-shift from being overwritten by the bounding-box approximation on support-only setting changes - Remove double-counting of global z_offset on support layers — support already inherits the offset from object layers during generation This Work Was Co-Authored-By Claude Opus 4.6 (1M context) UI: gray out inactive belt sub-options, rename to mesh transforms, move to Advanced Fix mesh clipping through build plate after belt shear/scale transform Generalize G-code viewer designed-view toggle for full belt transform Clip support layers to transformed belt floor plane Supports below the tilted build plate (Z = shear_factor * from_axis - min_z) are now clipped via half-plane intersection after generation. Belt floor parameters stored in SlicingParameters and populated in both update_slicing_parameters() and the static slicing_parameters() overload. Make belt G-code viewer toggle more prominent, add B keyboard shortcut - Add separator + teal "Belt Printer" header in legend panel - Append [B] hint to checkbox label - Add B key shortcut in GLCanvas3D to toggle designed/machine view - Read belt_printer_angle from loaded G-code headers to enable belt view Add per-axis global transform option for belt printer shear New belt_shear_{x,y,z}_global bool configs. When enabled, shear incorporates instance shift so objects at different bed positions get position-aware transform (Z += factor * instance_shift_on_from_axis). Fix global shear: use layer Z offset instead of mesh transform, add config invalidation - Global shear offset applied as post-slicing layer print_z adjustment instead of mesh transform (which was absorbed by min_z normalization or shifted mesh out of slice range) - Register all belt transform options in Print::invalidate_state_by_config_options to trigger posSlice re-slicing (the fallback only invalidated Print steps, not PrintObject steps — belt changes had no effect without manual re-slice) - Belt gcode remap options added to steps_gcode (gcode-export only) - Skip empty-first-layer check for belt objects with global Z offset WIP: split instances for global shear, relative Z offsets, debug logging - PrintApply: when belt global mode active, prevent instance grouping by adding unique Z perturbation to trafo — each copy becomes its own PrintObject with independent layers - PrintObjectSlice: compute global Z offset relative to minimum Y shift across all PrintObjects (lowest-Y object stays at Z=0) - Debug logging (warning level) for belt global shift values and offsets Known issues: - Cached posSlice results cause stale offsets when mixing copies with individually-added objects — need to compute min baseline outside slice() - Supports still generate to Z=0 instead of object's global Z offset Fix global shear for copied objects: disable shared-object layer optimization When belt global Z shear is active, each object needs unique layer Z values based on its bed position. The shared-object optimization was causing copies to reuse the source object's layers (and its Z offset) instead of computing their own position-based offset. started work on getting supports to work properly one step forward, one step back this version didn't quite work. Getting somewhere though about to add UI controllable tests added configuration options for supports tweak CLAUDE.md to be more aggressive for my machine. This commit should probably be pulled out before contributing upstream still chasing down some bugs moving objects between slices no longer results in improper Z-height because of caching added more data to the debug logs Z offset is getting more global again still not quite there, I think there's a fundamental logic flaw? hunting for bugs finally have a functional fix Add belt floor clipping to tree supports (organic and non-organic) - Add belt floor polygon clipping to non-organic tree support (slim/strong/hybrid) in draw_circles() and terminate nodes at the belt surface instead of the horizontal build plate - Add belt floor clipping to organic tree support pipeline with virtual belt raft layers for sub-floor branch generation, per-layer belt floor polygons in TreeModelVolumes, and post-generation layer trimming - Fix pre-existing processing_last_mesh bug in TreeModelVolumes that prevented m_anti_overhang (support blockers) from ever being applied; skip empty first layer check for belt printers Commits: current approach: make a face surface to build supports to closer! supports now terminate on shear plane, now need to get shear plane to correct Z height nearly there chasing down logic issues still committing for checkpoint, this still does not work still got logic problems... cull support clipping stashing changes for now. Going to focus on getting the global shear OFF support generation dialed first. beginning per object shear calcs Local shear transform is on correct Z offset now local shear finally works now and needs more testing global shear works now, needs thorough testing debugging non-45 degree angles debugging part 2 supports at all angles work now remove debug logging Add belt floor collision to non-organic tree support pipeline - Integrate belt floor as a collision surface in TreeSupportData so branches route around the belt naturally, replacing the explicit termination checks in drop_nodes() - Add belt extension layers below the object after draw_circles() to allow support geometry to extend to the diagonal belt surface instead of terminating at a horizontal first layer - Fix coordinate overflow in belt floor polygons (scale_(1e4) exceeds int32), skip first-layer brim expansion for belt printers, and extend empty first layer check bypass to all belt modes add debug logging, Z translate for tree supports still not seeing any cutoff surface yet adding debug options attempt #2 at trees if hit Z buildplate stop but don't set to_buildplate true getting closer tree support almost there, just need to get rid of the circles at the beginning getting closer belt / shear plane clip works, need to figure out the buidlplate plane issues more logic, added debugging logs supports now extend somewhat below Z=0 in global shear mode fix bad alloc, add 10mm below build plate fully works now shear transform + prusa tree support generation works now. pull out debug logging --- CLAUDE.md | 22 +- src/libslic3r/BuildVolume.hpp | 2 +- src/libslic3r/GCode.cpp | 30 ++- src/libslic3r/GCode/GCodeProcessor.cpp | 78 ++++++ src/libslic3r/GCode/GCodeProcessor.hpp | 33 +++ src/libslic3r/Preset.cpp | 10 +- src/libslic3r/Print.cpp | 62 ++++- src/libslic3r/Print.hpp | 10 +- src/libslic3r/PrintApply.cpp | 30 ++- src/libslic3r/PrintConfig.cpp | 64 +++++ src/libslic3r/PrintConfig.hpp | 23 ++ src/libslic3r/PrintObject.cpp | 116 ++++++++- src/libslic3r/PrintObjectSlice.cpp | 124 ++++++++- src/libslic3r/Slicing.hpp | 7 + src/libslic3r/Support/SupportCommon.hpp | 6 + src/libslic3r/Support/SupportMaterial.cpp | 290 ++++++++++++++++++++- src/libslic3r/Support/TreeModelVolumes.cpp | 117 ++++++++- src/libslic3r/Support/TreeModelVolumes.hpp | 3 + src/libslic3r/Support/TreeSupport.cpp | 98 ++++++- src/libslic3r/Support/TreeSupport.hpp | 4 + src/libslic3r/Support/TreeSupport3D.cpp | 74 ++++++ src/slic3r/GUI/GCodeViewer.cpp | 26 +- src/slic3r/GUI/GCodeViewer.hpp | 4 + src/slic3r/GUI/GLCanvas3D.cpp | 12 +- src/slic3r/GUI/GUI_ObjectList.cpp | 2 +- src/slic3r/GUI/Plater.cpp | 86 +++++- src/slic3r/GUI/Tab.cpp | 141 ++++++---- 27 files changed, 1337 insertions(+), 137 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 25aa516a56..9bd162c42c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,27 +23,7 @@ cmake --build build/arm64 --config RelWithDebInfo --target all -- ### Building on Linux **Always use this command to build the project when testing build issues on Linux.** ```bash -cmake --build build/arm64 --config RelWithDebInfo --target all -- - -``` -### Build test: - -**Always use this command to build the project when testing build issues on Windows.** -```bash -cmake --build . --config %build_type% --target ALL_BUILD -- -m -``` - -### Building on macOS -**Always use this command to build the project when testing build issues on macOS.** -```bash -cmake --build build/arm64 --config RelWithDebInfo --target all -- -``` - -### Building on Linux - **Always use this command to build the project when testing build issues on Linux.** -```bash -cmake --build build --config RelWithDebInfo --target all -- - +systemd-run --user --scope -p MemoryMax=48G cmake --build build --config RelWithDebInfo --target all -- -j18 -l 24 ``` diff --git a/src/libslic3r/BuildVolume.hpp b/src/libslic3r/BuildVolume.hpp index 0494efe9ba..2b0d8c2746 100644 --- a/src/libslic3r/BuildVolume.hpp +++ b/src/libslic3r/BuildVolume.hpp @@ -84,7 +84,7 @@ public: indexed_triangle_set bounding_mesh(bool scale=true) const; // Center of the print bed, unscaled. - Vec2d bed_center() const { return to_2d(m_bboxf.center()); } + Vec2d bed_center() const { return get_extents(m_bed_shape).center(); } // Convex hull of polygon(), scaled. const Polygon& convex_hull() const { return m_convex_hull; } // Smallest enclosing circle of polygon(), scaled. diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 1aeb5497a3..5b1330b202 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -1745,8 +1745,16 @@ std::vector GCode::collect_layers_to_print(const PrintObjec // Check that there are extrusions on the very first layer. The case with empty // first layer may result in skirt/brim in the air and maybe other issues. + // Skip this check for belt printers. The shear transform tilts the + // model so the first horizontal layer plane intersects only a thin + // sliver of the model (width ≈ first_layer_height / shear_factor). + // This sliver is often narrower than the nozzle diameter, producing + // zero perimeters and an empty first layer — which is expected, not + // an error. In global shear mode the object may also start above + // Z=0 on the tilted belt surface. if (layers_to_print.size() == 1u) { - if (!has_extrusions) + bool skip_empty_check = object.print()->config().belt_printer.value; + if (!has_extrusions && !skip_empty_check) throw Slic3r::SlicingError(_(L("One object has an empty first layer and can't be printed. Please Cut the bottom or enable supports.")), object.id().id); } @@ -2515,9 +2523,27 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.write_format("; HEADER_BLOCK_START\n"); // Write information on the generator. file.write_format("; generated by %s on %s\n", Slic3r::header_slic3r_generated().c_str(), Slic3r::Utils::local_timestamp().c_str()); - // Belt printer: embed angle in header for G-code processor detection. + // Belt printer: embed angle and transform configs in header for G-code processor detection. if (print.config().belt_printer.value) { file.write_format("; belt_printer_angle = %.1f\n", print.config().belt_printer_angle.value); + // Shear configs + const auto &full_cfg = print.full_print_config(); + file.write_format("; belt_shear_x = %s\n", full_cfg.opt_serialize("belt_shear_x").c_str()); + file.write_format("; belt_shear_x_angle = %.1f\n", print.config().belt_shear_x_angle.value); + file.write_format("; belt_shear_x_from = %s\n", full_cfg.opt_serialize("belt_shear_x_from").c_str()); + file.write_format("; belt_shear_y = %s\n", full_cfg.opt_serialize("belt_shear_y").c_str()); + file.write_format("; belt_shear_y_angle = %.1f\n", print.config().belt_shear_y_angle.value); + file.write_format("; belt_shear_y_from = %s\n", full_cfg.opt_serialize("belt_shear_y_from").c_str()); + file.write_format("; belt_shear_z = %s\n", full_cfg.opt_serialize("belt_shear_z").c_str()); + file.write_format("; belt_shear_z_angle = %.1f\n", print.config().belt_shear_z_angle.value); + file.write_format("; belt_shear_z_from = %s\n", full_cfg.opt_serialize("belt_shear_z_from").c_str()); + // Scale configs + file.write_format("; belt_scale_x = %s\n", full_cfg.opt_serialize("belt_scale_x").c_str()); + file.write_format("; belt_scale_x_angle = %.1f\n", print.config().belt_scale_x_angle.value); + file.write_format("; belt_scale_y = %s\n", full_cfg.opt_serialize("belt_scale_y").c_str()); + file.write_format("; belt_scale_y_angle = %.1f\n", print.config().belt_scale_y_angle.value); + file.write_format("; belt_scale_z = %s\n", full_cfg.opt_serialize("belt_scale_z").c_str()); + file.write_format("; belt_scale_z_angle = %.1f\n", print.config().belt_scale_z_angle.value); } if (is_bbl_printers) file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str()); diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 5dd2c0fc84..8a470eff19 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -3051,6 +3051,84 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers } catch (...) {} return; } + // Belt printer: parse shear configs from header comments. + { + auto parse_shear_mode = [](const std::string &s) -> BeltShearMode { + if (s == "pos_cot") return BeltShearMode::PosCot; + if (s == "neg_cot") return BeltShearMode::NegCot; + if (s == "pos_tan") return BeltShearMode::PosTan; + if (s == "neg_tan") return BeltShearMode::NegTan; + return BeltShearMode::None; + }; + auto parse_axis = [](const std::string &s) -> BeltAxis { + if (s == "y") return BeltAxis::Y; + if (s == "z") return BeltAxis::Z; + return BeltAxis::X; + }; + auto parse_scale_mode = [](const std::string &s) -> BeltScaleMode { + if (s == "inv_sin") return BeltScaleMode::InvSin; + if (s == "inv_cos") return BeltScaleMode::InvCos; + if (s == "sin") return BeltScaleMode::Sin; + if (s == "cos") return BeltScaleMode::Cos; + return BeltScaleMode::None; + }; + 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); + }; + // Shear X + if (boost::starts_with(comment, " belt_shear_x = ")) { + m_result.belt_shear_x = parse_shear_mode(trim(std::string(comment.substr(16)))); return; + } + if (boost::starts_with(comment, " belt_shear_x_angle = ")) { + try { m_result.belt_shear_x_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; + } + if (boost::starts_with(comment, " belt_shear_x_from = ")) { + m_result.belt_shear_x_from = parse_axis(trim(std::string(comment.substr(21)))); return; + } + // Shear Y + if (boost::starts_with(comment, " belt_shear_y = ")) { + m_result.belt_shear_y = parse_shear_mode(trim(std::string(comment.substr(16)))); return; + } + if (boost::starts_with(comment, " belt_shear_y_angle = ")) { + try { m_result.belt_shear_y_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; + } + if (boost::starts_with(comment, " belt_shear_y_from = ")) { + m_result.belt_shear_y_from = parse_axis(trim(std::string(comment.substr(21)))); return; + } + // Shear Z + if (boost::starts_with(comment, " belt_shear_z = ")) { + m_result.belt_shear_z = parse_shear_mode(trim(std::string(comment.substr(16)))); return; + } + if (boost::starts_with(comment, " belt_shear_z_angle = ")) { + try { m_result.belt_shear_z_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; + } + if (boost::starts_with(comment, " belt_shear_z_from = ")) { + m_result.belt_shear_z_from = parse_axis(trim(std::string(comment.substr(21)))); return; + } + // Scale X + if (boost::starts_with(comment, " belt_scale_x = ")) { + m_result.belt_scale_x = parse_scale_mode(trim(std::string(comment.substr(16)))); return; + } + if (boost::starts_with(comment, " belt_scale_x_angle = ")) { + try { m_result.belt_scale_x_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; + } + // Scale Y + if (boost::starts_with(comment, " belt_scale_y = ")) { + m_result.belt_scale_y = parse_scale_mode(trim(std::string(comment.substr(16)))); return; + } + if (boost::starts_with(comment, " belt_scale_y_angle = ")) { + try { m_result.belt_scale_y_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; + } + // Scale Z + if (boost::starts_with(comment, " belt_scale_z = ")) { + m_result.belt_scale_z = parse_scale_mode(trim(std::string(comment.substr(16)))); return; + } + if (boost::starts_with(comment, " belt_scale_z_angle = ")) { + try { m_result.belt_scale_z_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; + } + } // wipe start tag if (boost::starts_with(comment, reserved_tag(ETags::Wipe_Start))) { m_wiping = true; diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index e432a165a8..f953345f4e 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -229,6 +229,23 @@ class Print; float z_offset; // Belt printer: angle for coordinate transformation in preview. float belt_printer_angle{ 0.f }; + // Belt printer: per-axis shear config. + BeltShearMode belt_shear_x{ BeltShearMode::None }; + float belt_shear_x_angle{ 45.f }; + BeltAxis belt_shear_x_from{ BeltAxis::Y }; + BeltShearMode belt_shear_y{ BeltShearMode::None }; + float belt_shear_y_angle{ 45.f }; + BeltAxis belt_shear_y_from{ BeltAxis::Y }; + BeltShearMode belt_shear_z{ BeltShearMode::None }; + float belt_shear_z_angle{ 45.f }; + BeltAxis belt_shear_z_from{ BeltAxis::Y }; + // Belt printer: per-axis scale config. + BeltScaleMode belt_scale_x{ BeltScaleMode::None }; + float belt_scale_x_angle{ 45.f }; + BeltScaleMode belt_scale_y{ BeltScaleMode::None }; + float belt_scale_y_angle{ 45.f }; + BeltScaleMode belt_scale_z{ BeltScaleMode::None }; + float belt_scale_z_angle{ 45.f }; SettingsIds settings_ids; size_t filaments_count; bool backtrace_enabled; @@ -288,6 +305,22 @@ class Print; layer_filaments = other.layer_filaments; filament_change_count_map = other.filament_change_count_map; initial_layer_time = other.initial_layer_time; + belt_printer_angle = other.belt_printer_angle; + belt_shear_x = other.belt_shear_x; + belt_shear_x_angle = other.belt_shear_x_angle; + belt_shear_x_from = other.belt_shear_x_from; + belt_shear_y = other.belt_shear_y; + belt_shear_y_angle = other.belt_shear_y_angle; + belt_shear_y_from = other.belt_shear_y_from; + belt_shear_z = other.belt_shear_z; + belt_shear_z_angle = other.belt_shear_z_angle; + belt_shear_z_from = other.belt_shear_z_from; + belt_scale_x = other.belt_scale_x; + belt_scale_x_angle = other.belt_scale_x_angle; + belt_scale_y = other.belt_scale_y; + belt_scale_y_angle = other.belt_scale_y_angle; + belt_scale_z = other.belt_scale_z; + belt_scale_z_angle = other.belt_scale_z_angle; #if ENABLE_GCODE_VIEWER_STATISTICS time = other.time; #endif diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index f2a0cd9d24..3c597ac7f2 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1010,11 +1010,13 @@ static std::vector s_Preset_machine_limits_options { static std::vector s_Preset_printer_options { "printer_technology", - "printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "build_plate_tilt_x", "build_plate_tilt_y", "belt_printer", "belt_printer_angle", "belt_printer_infinite_y", "belt_shear_x", "belt_shear_x_angle", "belt_shear_x_from", - "belt_shear_y", "belt_shear_y_angle", "belt_shear_y_from", - "belt_shear_z", "belt_shear_z_angle", "belt_shear_z_from", + "printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "build_plate_tilt_x", "build_plate_tilt_y", "belt_printer", "belt_printer_angle", "belt_printer_infinite_y", "belt_shear_x", "belt_shear_x_angle", "belt_shear_x_from", "belt_shear_x_global", + "belt_shear_y", "belt_shear_y_angle", "belt_shear_y_from", "belt_shear_y_global", + "belt_shear_z", "belt_shear_z_angle", "belt_shear_z_from", "belt_shear_z_global", "belt_scale_x", "belt_scale_x_angle", "belt_scale_y", "belt_scale_y_angle", "belt_scale_z", "belt_scale_z_angle", - "belt_gcode_remap_x", "belt_gcode_remap_y", "belt_gcode_remap_z", "gcode_flavor", + "belt_gcode_remap_x", "belt_gcode_remap_y", "belt_gcode_remap_z", + "belt_support_floor_offset", "belt_support_floor_mode", "belt_support_z_offset_mode", + "gcode_flavor", "fan_kickstart", "fan_speedup_time", "fan_speedup_overhangs", "single_extruder_multi_material", "manual_filament_change", "file_start_gcode", "machine_start_gcode", "machine_end_gcode", "before_layer_change_gcode", "printing_by_object_gcode", "layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode", "change_filament_gcode", "change_extrusion_role_gcode", "printer_model", "printer_variant", "printer_extruder_id", "printer_extruder_variant", "extruder_variant_list", "default_nozzle_volume_type", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index f245cc32b6..0aef12a74b 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -99,6 +99,10 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n // Cache the plenty of parameters, which influence the G-code generator only, // or they are only notes not influencing the generated G-code. static std::unordered_set steps_gcode = { + // Belt printer G-code axis remap (only affects G-code output, not slicing). + "belt_gcode_remap_x", + "belt_gcode_remap_y", + "belt_gcode_remap_z", //BBS "additional_cooling_fan_speed", "reduce_crossing_wall", @@ -275,8 +279,34 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n // Spiral Vase forces different kind of slicing than the normal model: // In Spiral Vase mode, holes are closed and only the largest area contour is kept at each layer. // Therefore toggling the Spiral Vase on / off requires complete reslicing. - || opt_key == "spiral_mode") { + || opt_key == "spiral_mode" + // Belt printer transform options change the mesh geometry before slicing. + || opt_key == "belt_printer" + || opt_key == "belt_printer_angle" + || opt_key == "belt_shear_x" + || opt_key == "belt_shear_x_angle" + || opt_key == "belt_shear_x_from" + || opt_key == "belt_shear_x_global" + || opt_key == "belt_shear_y" + || opt_key == "belt_shear_y_angle" + || opt_key == "belt_shear_y_from" + || opt_key == "belt_shear_y_global" + || opt_key == "belt_shear_z" + || opt_key == "belt_shear_z_angle" + || opt_key == "belt_shear_z_from" + || opt_key == "belt_shear_z_global" + || opt_key == "belt_scale_x" + || opt_key == "belt_scale_x_angle" + || opt_key == "belt_scale_y" + || opt_key == "belt_scale_y_angle" + || opt_key == "belt_scale_z" + || opt_key == "belt_scale_z_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") { + osteps.emplace_back(posSupportMaterial); } else if ( opt_key == "print_sequence" || opt_key == "filament_type" @@ -2124,15 +2154,21 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) int object_count = m_objects.size(); std::set need_slicing_objects; std::set re_slicing_objects; + // Belt global Z shear: each object needs unique layer Z values based on + // its bed position, so sharing layers between "identical" objects is wrong. + bool belt_no_share = m_config.belt_printer.value && m_config.belt_shear_z_global.value + && m_config.belt_shear_z.value != BeltShearMode::None; if (!use_cache) { for (int index = 0; index < object_count; index++) { PrintObject *obj = m_objects[index]; - for (PrintObject *slicing_obj : need_slicing_objects) - { - if (is_print_object_the_same(obj, slicing_obj)) { - obj->set_shared_object(slicing_obj); - break; + if (!belt_no_share) { + for (PrintObject *slicing_obj : need_slicing_objects) + { + if (is_print_object_the_same(obj, slicing_obj)) { + obj->set_shared_object(slicing_obj); + break; + } } } if (!obj->get_shared_object()) @@ -2151,12 +2187,14 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) PrintObject *obj = m_objects[index]; bool found_shared = false; if (need_slicing_objects.find(obj) == need_slicing_objects.end()) { - for (PrintObject *slicing_obj : need_slicing_objects) - { - if (is_print_object_the_same(obj, slicing_obj)) { - obj->set_shared_object(slicing_obj); - found_shared = true; - break; + if (!belt_no_share) { + for (PrintObject *slicing_obj : need_slicing_objects) + { + if (is_print_object_the_same(obj, slicing_obj)) { + obj->set_shared_object(slicing_obj); + found_shared = true; + break; + } } } if (!found_shared) { diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index b98811c8e3..d0c050f442 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -573,7 +573,15 @@ private: PrintObject* m_shared_object{ nullptr }; - + // Belt printer: global Z offset applied to this object's layers for shear positioning. + double m_belt_global_z_offset { 0.0 }; + // Belt printer: min_z of mesh after belt shear (before Z-shift), for z_offset calc. + double m_belt_min_z { 0.0 }; +public: + double belt_global_z_offset() const { return m_belt_global_z_offset; } +private: + + // SoftFever // // object id diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index d2a44463ea..7f3b41b0f1 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -135,22 +135,29 @@ struct PrintObjectTrafoAndInstances // Generate a list of trafos and XY offsets for instances of a ModelObject // Orca: Updated to include XYZ filament shrinkage compensation -static std::vector print_objects_from_model_object(const ModelObject &model_object, const Vec3d &shrinkage_compensation) +static std::vector print_objects_from_model_object(const ModelObject &model_object, const Vec3d &shrinkage_compensation, bool force_separate_instances = false) { std::set trafos; PrintObjectTrafoAndInstances trafo; //BBS: add useful logs for debug int index = 0; + int unique_counter = 0; for (ModelInstance *model_instance : model_object.instances) { if (model_instance->is_printable()) { // Orca: Updated with XYZ filament shrinkage compensation Geometry::Transformation model_instance_transformation = model_instance->get_transformation(); trafo.trafo = model_instance_transformation.get_matrix_with_applied_shrinkage_compensation(shrinkage_compensation); - + auto shift = Point::new_scale(trafo.trafo.data()[12], trafo.trafo.data()[13]); // Reset the XY axes of the transformation. trafo.trafo.data()[12] = 0; trafo.trafo.data()[13] = 0; + // Belt printer global mode: prevent instance grouping so each + // copy gets its own PrintObject with independent layer Z values. + // Add a tiny unique perturbation to the existing Z (don't replace + // it — the Z translation from ensure_on_bed must be preserved). + if (force_separate_instances) + trafo.trafo.data()[14] += 1e-10 * (++unique_counter); // Search or insert a trafo. auto it = trafos.emplace(trafo).first; const_cast(*it).instances.emplace_back(PrintInstance{ nullptr, model_instance, shift }); @@ -1506,11 +1513,16 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ PrintObjectPtrs print_objects_new; print_objects_new.reserve(std::max(m_objects.size(), m_model.objects.size())); bool new_objects = false; + bool belt_instances_shifted = false; // Walk over all new model objects and check, whether there are matching PrintObjects. for (ModelObject *model_object : m_model.objects) { ModelObjectStatus &model_object_status = const_cast(model_object_status_db.reuse(*model_object)); // Orca: Updated for XYZ filament shrink compensation - model_object_status.print_instances = print_objects_from_model_object(*model_object, this->shrinkage_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_shear_z_global.value + && m_config.belt_shear_z.value != BeltShearMode::None; + model_object_status.print_instances = print_objects_from_model_object(*model_object, this->shrinkage_compensation(), belt_force_separate); std::vector old; old.reserve(print_object_status_db.count(*model_object)); for (const PrintObjectStatus &print_object_status : print_object_status_db.get_range(*model_object)) @@ -1558,6 +1570,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ if (status != PrintBase::APPLY_STATUS_UNCHANGED) { size_t extruder_num = new_full_config.option("nozzle_diameter")->size(); update_apply_status(status == PrintBase::APPLY_STATUS_INVALIDATED); + belt_instances_shifted = true; } print_objects_new.emplace_back((*it_old)->print_object); const_cast(*it_old)->status = PrintObjectStatus::Reused; @@ -1593,6 +1606,17 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ update_apply_status(object->invalidate_step(posSlice)); } } + + // 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_shear_z_global.value + && m_config.belt_shear_z.value != BeltShearMode::None) { + for (PrintObject *object : m_objects) + update_apply_status(object->invalidate_step(posSlice)); + } } //BBS: check the config again diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 7290af09fb..96e320ecf4 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -326,6 +326,21 @@ static t_config_enum_values s_keys_map_BeltRemapAxis { }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltRemapAxis) +static t_config_enum_values s_keys_map_BeltSupportFloorMode { + { "none", int(BeltSupportFloorMode::None) }, + { "generator_only", int(BeltSupportFloorMode::GeneratorOnly) }, + { "clip_only", int(BeltSupportFloorMode::ClipOnly) }, + { "both", int(BeltSupportFloorMode::Both) }, +}; +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_SupportMaterialPattern { { "rectilinear", smpRectilinear }, { "rectilinear-grid", smpRectilinearGrid }, @@ -6051,17 +6066,29 @@ void PrintConfigDef::init_fff_params() def->set_default_value(new ConfigOptionEnum(default_axis)); }; + auto add_belt_shear_global = [this](const char *key, const char *label) { + auto def = this->add(key, coBool); + def->label = L(label); + def->category = L("Printable space"); + def->tooltip = L("Apply shear in global coordinates (position-aware) rather than object-local coordinates."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + }; + add_belt_shear_mode("belt_shear_x", "Function", BeltShearMode::None); add_belt_shear_angle("belt_shear_x_angle", "Angle"); add_belt_axis_enum("belt_shear_x_from", "From", "Source axis for X shear.", BeltAxis::Z); + add_belt_shear_global("belt_shear_x_global", "Global"); add_belt_shear_mode("belt_shear_y", "Function", BeltShearMode::PosCot); add_belt_shear_angle("belt_shear_y_angle", "Angle"); add_belt_axis_enum("belt_shear_y_from", "From", "Source axis for Y shear.", BeltAxis::Z); + add_belt_shear_global("belt_shear_y_global", "Global"); add_belt_shear_mode("belt_shear_z", "Function", BeltShearMode::None); add_belt_shear_angle("belt_shear_z_angle", "Angle"); add_belt_axis_enum("belt_shear_z_from", "From", "Source axis for Z shear.", BeltAxis::Y); + add_belt_shear_global("belt_shear_z_global", "Global"); // Per-axis scale controls for belt printer auto add_belt_scale_mode = [this](const char *key, const char *label, BeltScaleMode default_mode) { @@ -6113,6 +6140,43 @@ void PrintConfigDef::init_fff_params() add_belt_remap("belt_gcode_remap_y", "Y", "Which slicing axis maps to machine Y in G-code output.", BeltRemapAxis::PosY); add_belt_remap("belt_gcode_remap_z", "Z", "Which slicing axis maps to machine Z in G-code output.", BeltRemapAxis::PosZ); + // Belt support floor debug controls + def = this->add("belt_support_floor_offset", coFloat); + def->label = L("Floor Z offset"); + def->category = L("Printable space"); + def->tooltip = L("Shifts the computed belt floor up or down (mm). Negative values lower the floor, allowing more supports to survive. Use this to diagnose belt floor formula issues."); + def->sidetext = L("mm"); + def->min = -500; + def->max = 500; + 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::get_enum_values(); + def->enum_values = {"none", "generator_only"}; + def->enum_labels = {L("None"), L("Generator only")}; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(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::get_enum_values(); + def->enum_values = {"none", "unconditional", "raft_only"}; + def->enum_labels = {L("None"), L("Unconditional"), L("Raft only")}; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(BeltSupportZOffsetMode::Unconditional)); + } + def = this->add("tree_support_branch_angle", coFloat); def->label = L("Tree support branch angle"); def->category = L("Support"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 68e9425a82..2d76dcc0b5 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -188,6 +188,21 @@ enum class BeltRemapAxis 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 + ClipOnly, // Only post-processing clipping + Both, // Both generator and clipping +}; + +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 +}; + enum SupportMaterialPattern { smpDefault, smpRectilinear, smpRectilinearGrid, smpHoneycomb, @@ -535,6 +550,8 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltShearMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltScaleMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltAxis) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltRemapAxis) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltSupportFloorMode) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltSupportZOffsetMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialPattern) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialStyle) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialInterfacePattern) @@ -1455,12 +1472,15 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionEnum, belt_shear_x)) ((ConfigOptionFloat, belt_shear_x_angle)) ((ConfigOptionEnum, belt_shear_x_from)) + ((ConfigOptionBool, belt_shear_x_global)) ((ConfigOptionEnum, belt_shear_y)) ((ConfigOptionFloat, belt_shear_y_angle)) ((ConfigOptionEnum, belt_shear_y_from)) + ((ConfigOptionBool, belt_shear_y_global)) ((ConfigOptionEnum, belt_shear_z)) ((ConfigOptionFloat, belt_shear_z_angle)) ((ConfigOptionEnum, belt_shear_z_from)) + ((ConfigOptionBool, belt_shear_z_global)) ((ConfigOptionEnum, belt_scale_x)) ((ConfigOptionFloat, belt_scale_x_angle)) ((ConfigOptionEnum, belt_scale_y)) @@ -1470,6 +1490,9 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionEnum, belt_gcode_remap_x)) ((ConfigOptionEnum, belt_gcode_remap_y)) ((ConfigOptionEnum, belt_gcode_remap_z)) + ((ConfigOptionFloat, belt_support_floor_offset)) + ((ConfigOptionEnum, belt_support_floor_mode)) + ((ConfigOptionEnum, belt_support_z_offset_mode)) //BBS ((ConfigOptionInts, additional_cooling_fan_speed)) ((ConfigOptionBool, reduce_crossing_wall)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index abd309fbff..f803cc0369 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -9,6 +9,7 @@ #include "MutablePolygon.hpp" #include "PrintConfig.hpp" #include "Support/SupportMaterial.hpp" +#include "Support/SupportCommon.hpp" #include "Support/SupportSpotsGenerator.hpp" #include "Support/TreeSupport.hpp" #include "Surface.hpp" @@ -1365,7 +1366,12 @@ bool PrintObject::invalidate_step(PrintObjectStep step) } else if (step == posSupportMaterial) { invalidated |= this->invalidate_steps({ posSimplifySupportPath }); invalidated |= m_print->invalidate_steps({ psSkirtBrim }); - m_slicing_params.valid = false; + // NOTE: do NOT set m_slicing_params.valid = false here. + // belt_floor_z_shift is patched to an exact value during posSlice + // (PrintObjectSlice.cpp, after slice_volumes). Invalidating slicing + // params here causes update_slicing_parameters() to overwrite that + // exact value with a bounding-box approximation, while posSlice does + // not re-run to correct it — breaking belt support clipping. } // Wipe tower depends on the ordering of extruders, which in turn depends on everything. @@ -3392,6 +3398,10 @@ void PrintObject::update_slicing_parameters() // Orca: updated function call for XYZ shrinkage compensation if (!m_slicing_params.valid) { coordf_t object_height = this->model_object()->max_z(); + // Belt floor parameters for support clipping (populated below if belt Z-shear is active). + double belt_floor_shear_factor_out = 0.0; + int belt_floor_from_axis_out = 1; + double belt_floor_z_shift_out = 0.0; // Belt shear/scale may change the effective Z height. const auto &pcfg = this->print()->config(); if (pcfg.belt_printer.value) { @@ -3435,6 +3445,12 @@ void PrintObject::update_slicing_parameters() max_rz = std::max(max_rz, new_z); } object_height = max_rz - min_rz; + belt_floor_shear_factor_out = shear_factor; + belt_floor_from_axis_out = from; + // Belt contact surface starts at bb.min.z() pre-shear; add the + // slicing Z-shift that keeps the mesh above Z=0. + // Exact value is patched after slice_volumes() in posSlice. + belt_floor_z_shift_out = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.); } else { object_height *= scale_z; } @@ -3442,6 +3458,10 @@ void PrintObject::update_slicing_parameters() } m_slicing_params = SlicingParameters::create_from_config(pcfg, m_config, object_height, this->object_extruders(), this->print()->shrinkage_compensation()); + // Populate belt floor parameters into slicing params for support clipping. + m_slicing_params.belt_floor_shear_factor = belt_floor_shear_factor_out; + m_slicing_params.belt_floor_from_axis = belt_floor_from_axis_out; + m_slicing_params.belt_floor_z_shift = belt_floor_z_shift_out; } } @@ -3479,6 +3499,11 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full sort_remove_duplicates(object_extruders); //FIXME add painting extruders + // Belt floor parameters for support clipping (populated below if belt Z-shear is active). + double belt_floor_shear_factor_out = 0.0; + int belt_floor_from_axis_out = 1; + double belt_floor_z_shift_out = 0.0; + if (object_max_z <= 0.f) { BoundingBoxf3 bb = model_object.raw_bounding_box(); object_max_z = (float)bb.size().z(); @@ -3523,13 +3548,20 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full max_rz = std::max(max_rz, new_z); } object_max_z = (float)(max_rz - min_rz); + belt_floor_shear_factor_out = shear_factor; + belt_floor_from_axis_out = from; + belt_floor_z_shift_out = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.); } else { object_max_z *= (float)scale_z; } } } } - return SlicingParameters::create_from_config(print_config, object_config, object_max_z, object_extruders, object_shrinkage_compensation); + SlicingParameters params = SlicingParameters::create_from_config(print_config, object_config, object_max_z, object_extruders, object_shrinkage_compensation); + params.belt_floor_shear_factor = belt_floor_shear_factor_out; + params.belt_floor_from_axis = belt_floor_from_axis_out; + params.belt_floor_z_shift = belt_floor_z_shift_out; + return params; } // returns 0-based indices of extruders used to print the object (without brim, support and other helper extrusions) @@ -4038,6 +4070,67 @@ void PrintObject::combine_infill() } } +// Belt printer: clip an ExtrusionEntityCollection to a region defined by clip_expoly. +// Handles ExtrusionPath, ExtrusionMultiPath, ExtrusionLoop, and nested ExtrusionEntityCollection. +static void clip_support_fills(ExtrusionEntityCollection &fills, const ExPolygons &clip_region) +{ + ExtrusionEntitiesPtr new_entities; + for (ExtrusionEntity *entity : fills.entities) { + if (auto *path = dynamic_cast(entity)) { + ExtrusionEntityCollection clipped; + path->intersect_expolygons(clip_region, &clipped); + if (!clipped.empty()) { + for (ExtrusionEntity *e : clipped.entities) + new_entities.push_back(e->clone()); + } + delete entity; + } else if (auto *multipath = dynamic_cast(entity)) { + ExtrusionPaths new_paths; + for (const ExtrusionPath &p : multipath->paths) { + ExtrusionEntityCollection clipped; + p.intersect_expolygons(clip_region, &clipped); + for (ExtrusionEntity *e : clipped.entities) + if (auto *cp = dynamic_cast(e)) + new_paths.push_back(std::move(*cp)); + } + if (!new_paths.empty()) { + multipath->paths = std::move(new_paths); + new_entities.push_back(multipath); + } else { + delete entity; + } + } else if (auto *loop = dynamic_cast(entity)) { + ExtrusionPaths new_paths; + for (const ExtrusionPath &p : loop->paths) { + ExtrusionEntityCollection clipped; + p.intersect_expolygons(clip_region, &clipped); + for (ExtrusionEntity *e : clipped.entities) + if (auto *cp = dynamic_cast(e)) + new_paths.push_back(std::move(*cp)); + } + if (!new_paths.empty()) { + // Loop is no longer a closed loop after clipping; emit as individual paths. + for (auto &p : new_paths) + new_entities.push_back(new ExtrusionPath(std::move(p))); + delete entity; + } else { + delete entity; + } + } else if (auto *coll = dynamic_cast(entity)) { + clip_support_fills(*coll, clip_region); + if (!coll->empty()) { + new_entities.push_back(coll); + } else { + delete entity; + } + } else { + // Unknown entity type — keep as-is. + new_entities.push_back(entity); + } + } + fills.entities = std::move(new_entities); +} + void PrintObject::_generate_support_material() { if (is_tree(m_config.support_type.value)) { @@ -4049,6 +4142,25 @@ void PrintObject::_generate_support_material() PrintObjectSupportMaterial support_material(this, m_slicing_params); support_material.generate(*this); } + // Global Z offset for support layers: + // - Normal support: layers already inherit global_z_offset from object layers. + // - 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 + // independently and does NOT inherit the offset — apply it here. + // Belt floor polygon clipping for non-organic tree support is done inside + // draw_circles() before area_groups and toolpaths are built. + if (is_tree(m_config.support_type.value) && std::abs(m_belt_global_z_offset) > EPSILON) { + // Resolve effective support style (same logic as SupportParameters). + auto style = m_config.support_style.value; + if (style == smsDefault) + style = smsTreeOrganic; + if (style == smsTreeOrganic) { + for (SupportLayer *sl : m_support_layers) + sl->print_z += m_belt_global_z_offset; + } + } + } // BBS diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 294c245eaa..404550da1a 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -1,4 +1,5 @@ #include +#include #include @@ -125,7 +126,8 @@ static std::vector slice_volumes_inner( ModelVolumePtrs model_volumes, const std::vector &layer_ranges, const std::vector &zs, - const std::function &throw_on_cancel_callback) + const std::function &throw_on_cancel_callback, + double *out_belt_min_z = nullptr) { model_volumes_sort_by_id(model_volumes); @@ -202,8 +204,32 @@ static std::vector slice_volumes_inner( } // Apply: scale * shear * trafo (shear first, then scale). - if (has_shear || has_scale) + if (has_shear || has_scale) { params_base.trafo = belt_scale * belt_shear * params_base.trafo; + + // After the shear/scale transform, the mesh may clip through the + // build plate (Z < 0). Detect this and shift the mesh up. + Transform3d combined = params_base.trafo; + double min_z = std::numeric_limits::max(); + for (const ModelVolume *mv : model_volumes) { + if (!mv->is_model_part()) continue; + for (const stl_vertex &v : mv->mesh().its.vertices) { + Vec3d pt = combined * v.cast(); + min_z = std::min(min_z, pt.z()); + } + } + double belt_z_shift_val = (min_z < 0. && min_z != std::numeric_limits::max()) ? -min_z : 0.; + BOOST_LOG_TRIVIAL(warning) << "Belt Z-shift: min_z=" << min_z + << " z_shift=" << belt_z_shift_val + << " trafo_z=" << object_trafo.matrix()(2, 3); + if (belt_z_shift_val > 0.) { + Transform3d z_shift = Transform3d::Identity(); + z_shift.matrix()(2, 3) = belt_z_shift_val; + params_base.trafo = z_shift * params_base.trafo; + } + if (out_belt_min_z) + *out_belt_min_z = (min_z != std::numeric_limits::max()) ? min_z : 0.; + } } //BBS: 0.0025mm is safe enough to simplify the data to speed slicing up for high-resolution model. //Also has on influence on arc fitting which has default resolution 0.0125mm. @@ -866,6 +892,20 @@ void PrintObject::slice() m_layers = new_layers(this, generate_object_layers(m_slicing_params, layer_height_profile, m_config.precise_z_height.value)); this->slice_volumes(); m_print->throw_if_canceled(); + + // After slicing, m_belt_min_z holds the exact post-shear minimum Z + // in trafo_centered space (which includes the ensure_on_bed Z offset). + // The belt surface is at Z=0 in trafo_centered space; after shear it + // becomes Z = sf*Y, and after the z-shift that keeps the mesh above + // Z=0 it becomes Z = sf*Y + z_shift_val. So belt_floor_z_shift is + // simply the z-shift applied, i.e. max(0, -m_belt_min_z). + // NOTE: do NOT add raw_bounding_box().min.z() here — m_belt_min_z + // already includes the ensure_on_bed offset, unlike the min_rz used + // in update_slicing_parameters() which needs that compensation. + if (std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON) { + m_slicing_params.belt_floor_z_shift = (m_belt_min_z < 0.) ? -m_belt_min_z : 0.; + } + int firstLayerReplacedBy = 0; #if 0 @@ -904,6 +944,83 @@ void PrintObject::slice() if (m_layers.empty()) throw Slic3r::SlicingError(L("No layers were detected. You might want to repair your STL file(s) or check their size or thickness and retry.\n")); + // Belt printer global mode: offset all layer Z values so objects at + // different bed positions print at different heights on the tilted belt. + // This is a post-slicing adjustment — the sliced geometry is identical + // regardless of global mode, only the output Z coordinates change. + { + const auto &pcfg = this->print()->config(); + BOOST_LOG_TRIVIAL(warning) << "Belt global check: belt_printer=" << pcfg.belt_printer.value + << " belt_shear_z=" << int(pcfg.belt_shear_z.value) + << " belt_shear_z_global=" << pcfg.belt_shear_z_global.value + << " object=" << this->model_object()->name; + if (pcfg.belt_printer.value) { + auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad); + double cos_a = std::cos(angle_rad); + switch (mode) { + case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; + case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; + case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; + case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; + default: return 0.; + } + }; + + Point inst_shift = this->instances().empty() ? Point(0, 0) + : this->instances().front().shift - this->center_offset(); + BOOST_LOG_TRIVIAL(warning) << "Belt global: object " << this->model_object()->name + << " instances=" << this->instances().size() + << " shift=(" << unscale(inst_shift.x()) << ", " << unscale(inst_shift.y()) << ")"; + double global_z_offset = 0.; + + struct GAxis { BeltShearMode mode; double angle; int from; bool global; }; + GAxis gaxes[3] = { + { pcfg.belt_shear_x.value, pcfg.belt_shear_x_angle.value, int(pcfg.belt_shear_x_from.value), pcfg.belt_shear_x_global.value }, + { pcfg.belt_shear_y.value, pcfg.belt_shear_y_angle.value, int(pcfg.belt_shear_y_from.value), pcfg.belt_shear_y_global.value }, + { pcfg.belt_shear_z.value, pcfg.belt_shear_z_angle.value, int(pcfg.belt_shear_z_from.value), pcfg.belt_shear_z_global.value }, + }; + + // Only the Z-row shear contributes a Z offset from global mode. + // (X/Y row shears with global would offset X/Y, not Z — not useful here.) + // Offsets are RELATIVE: we subtract the minimum shift across all + // PrintObjects so the lowest-positioned object stays at Z=0. + const auto &za = gaxes[2]; // Z row + if (za.global && za.mode != BeltShearMode::None && za.from < 2) { + double factor = compute_shear_factor(za.mode, za.angle); + // The Z-shift brought the mesh's lowest sheared vertex to + // Z=0. That vertex's physical Y determines the belt contact + // point. With trafo_z preserved (ensure_on_bed offset), + // min_z = Y_at_contact * factor for bottom-face vertices, + // so: z_offset = center_Y * factor + min_z. + Point phys = inst_shift; // already has center_offset subtracted + double center_on_axis = (za.from == 0) ? unscale(phys.x()) : unscale(phys.y()); + global_z_offset += center_on_axis * factor + m_belt_min_z; + } + + BOOST_LOG_TRIVIAL(warning) << "Belt global: z_offset=" << global_z_offset + << " za.global=" << za.global << " za.mode=" << int(za.mode) << " za.from=" << za.from + << " (relative to min across " << this->print()->objects().size() << " objects)"; + m_belt_global_z_offset = global_z_offset; + if (std::abs(global_z_offset) > EPSILON) { + for (Layer *layer : m_layers) + layer->print_z += global_z_offset; + // Keep belt floor clipping in sync with the shifted print_z + // values — the support generator sees globally-offset object + // layer print_z, so belt_floor_z_shift must match. + m_slicing_params.belt_floor_z_shift += global_z_offset; + } + if (!m_layers.empty()) { + BOOST_LOG_TRIVIAL(warning) << "Belt global: first_layer_z=" << m_layers.front()->print_z + << " last_layer_z=" << m_layers.back()->print_z + << " num_layers=" << m_layers.size() + << " center_offset=(" << unscale(m_center_offset.x()) + << ", " << unscale(m_center_offset.y()) << ")"; + } + } + } + // BBS this->set_done(posSlice); } @@ -1208,7 +1325,8 @@ void PrintObject::slice_volumes() if (!slice_zs.empty()) { objSliceByVolume = slice_volumes_inner( print->config(), this->config(), this->trafo_centered(), - this->model_object()->volumes, m_shared_regions->layer_ranges, slice_zs, throw_on_cancel_callback); + this->model_object()->volumes, m_shared_regions->layer_ranges, slice_zs, throw_on_cancel_callback, + &m_belt_min_z); } //BBS: "model_part" volumes are grouded according to their connections diff --git a/src/libslic3r/Slicing.hpp b/src/libslic3r/Slicing.hpp index a7b21a140d..b06f2a7bc4 100644 --- a/src/libslic3r/Slicing.hpp +++ b/src/libslic3r/Slicing.hpp @@ -110,6 +110,13 @@ struct SlicingParameters coordf_t object_print_z_uncompensated_max { 0 }; // Scaling factor for compensating shrinkage in Z-axis. coordf_t object_shrinkage_compensation_z { 0 }; + + // Belt printer: floor plane parameters for support clipping. + // Belt contact surface in slicing coords: Z = bb_min_z + sf*Y + slicing_z_shift. + // cutoff = (print_z - belt_floor_z_shift - floor_offset) / shear_factor + double belt_floor_shear_factor { 0.0 }; // shear factor (e.g. cot(45deg)) + int belt_floor_from_axis { 1 }; // which axis the shear is from (0=X, 1=Y) + double belt_floor_z_shift { 0.0 }; // bb_min_z + max(0, -min_z_after_shear) }; static_assert(IsTriviallyCopyable::value, "SlicingParameters class is not POD (and it should be - see constructor)."); diff --git a/src/libslic3r/Support/SupportCommon.hpp b/src/libslic3r/Support/SupportCommon.hpp index 531daaa0a8..f15c7d417a 100644 --- a/src/libslic3r/Support/SupportCommon.hpp +++ b/src/libslic3r/Support/SupportCommon.hpp @@ -144,6 +144,12 @@ int idx_lower_or_equal(const std::vector &vec, int idx, FN_LOWER_EQUAL fn_lo return idx_lower_or_equal(vec.begin(), vec.end(), idx, fn_lower_equal); } +// Belt floor: compute the belt-side half-plane polygon at a given print_z. +// Used to clip support polygons against the belt surface. +Polygons belt_floor_surface_polygon( + const SlicingParameters &slicing_params, const PrintConfig &print_config, + const PrintObject &object, coordf_t print_z); + } // namespace Slic3r #endif /* slic3r_SupportCommon_hpp_ */ diff --git a/src/libslic3r/Support/SupportMaterial.cpp b/src/libslic3r/Support/SupportMaterial.cpp index c230924ac8..d70153a7dd 100644 --- a/src/libslic3r/Support/SupportMaterial.cpp +++ b/src/libslic3r/Support/SupportMaterial.cpp @@ -367,10 +367,21 @@ inline void layers_append(SupportGeneratorLayersPtr &dst, const SupportGenerator } // Support layer that is covered by some form of dense interface. -static constexpr const std::initializer_list support_types_interface { +static constexpr const std::initializer_list support_types_interface { SupporLayerType::RaftInterface, SupporLayerType::BottomContact, SupporLayerType::BottomInterface, SupporLayerType::TopContact, SupporLayerType::TopInterface }; +// Forward declarations for belt floor helpers (defined later in this file). +// belt_floor_surface_polygon is declared in SupportCommon.hpp (non-static, +// shared with TreeSupport.cpp). + +static Polygons belt_floor_valid_region_polygon( + const SlicingParameters &slicing_params, const PrintConfig &print_config, + const PrintObject &object, coordf_t print_z); +static void trim_support_layers_by_belt_floor( + const SlicingParameters &slicing_params, const PrintConfig &print_config, + const PrintObject &object, SupportGeneratorLayersPtr &support_layers); + void PrintObjectSupportMaterial::generate(PrintObject &object) { BOOST_LOG_TRIVIAL(info) << "Support generator - Start"; @@ -443,11 +454,12 @@ void PrintObjectSupportMaterial::generate(PrintObject &object) object, bottom_contacts, top_contacts, layer_storage); this->trim_support_layers_by_object(object, top_contacts, m_slicing_params.gap_support_object, m_slicing_params.gap_object_support, m_support_params.gap_xy); + trim_support_layers_by_belt_floor(m_slicing_params, *m_print_config, object, top_contacts); #ifdef SLIC3R_DEBUG for (const SupportGeneratorLayer *layer : top_contacts) Slic3r::SVG::export_expolygons( - debug_out_path("support-top-contacts-trimmed-by-object-%d-%lf.svg", iRun, layer->print_z), + debug_out_path("support-top-contacts-trimmed-by-object-%d-%lf.svg", iRun, layer->print_z), union_ex(layer->polygons)); #endif @@ -603,6 +615,139 @@ Polygons collect_region_slices_by_type(const Layer &layer, SurfaceType surface_t return out; } +// Belt printer: compute the belt-side half-plane polygon at a given print_z. +// This represents the region where the belt surface exists (the "phantom top surface"). +// Support that overlaps with this polygon should terminate with a bottom contact. +// Returns empty if belt floor is not active. +Polygons belt_floor_surface_polygon( + const SlicingParameters &slicing_params, + const PrintConfig &print_config, + const PrintObject &object, + coordf_t print_z) +{ + const double shear_factor = slicing_params.belt_floor_shear_factor; + if (std::abs(shear_factor) < EPSILON) + return {}; + + const int from_axis = slicing_params.belt_floor_from_axis; // 0=X, 1=Y + const double floor_offset = print_config.belt_support_floor_offset.value; + + // Belt floor line in slicing coordinates: Z = sf * Y + z_shift. + // z_shift accounts for the upward shift applied when post-shear geometry + // extends below the bed (overhangs). Solving for Y: + // cutoff = (print_z - z_shift - floor_offset) / sf + const double z_shift = slicing_params.belt_floor_z_shift; + const double cutoff = (print_z - z_shift - floor_offset) / shear_factor; + const coord_t cutoff_scaled = scale_(cutoff); + const coord_t large_bound = scale_(1e4); + + // Build the belt-side half-plane (inverted from the valid region). + // If shear_factor > 0: valid region is from_axis < cutoff, so belt surface is from_axis >= cutoff. + // If shear_factor < 0: valid region is from_axis > cutoff, so belt surface is from_axis <= cutoff. + Polygon belt_poly; + if (from_axis == 0) { + if (shear_factor > 0) { + // Belt surface: X >= cutoff + belt_poly.points = { + Point(cutoff_scaled, -large_bound), + Point(large_bound, -large_bound), + Point(large_bound, large_bound), + Point(cutoff_scaled, large_bound) + }; + } else { + // Belt surface: X <= cutoff + belt_poly.points = { + Point(-large_bound, -large_bound), + Point(cutoff_scaled, -large_bound), + Point(cutoff_scaled, large_bound), + Point(-large_bound, large_bound) + }; + } + } else { + if (shear_factor > 0) { + // Belt surface: Y >= cutoff + belt_poly.points = { + Point(-large_bound, cutoff_scaled), + Point( large_bound, cutoff_scaled), + Point( large_bound, large_bound), + Point(-large_bound, large_bound) + }; + } else { + // Belt surface: Y <= cutoff + belt_poly.points = { + Point(-large_bound, -large_bound), + Point( large_bound, -large_bound), + Point( large_bound, cutoff_scaled), + Point(-large_bound, cutoff_scaled) + }; + } + } + return { belt_poly }; +} + +// Belt printer: compute the valid-region half-plane polygon at a given print_z. +// This is the region where support is allowed to exist (above the belt). +// Used to clip the downward-propagating support projection. +static Polygons belt_floor_valid_region_polygon( + const SlicingParameters &slicing_params, + const PrintConfig &print_config, + const PrintObject &object, + coordf_t print_z) +{ + const double shear_factor = slicing_params.belt_floor_shear_factor; + if (std::abs(shear_factor) < EPSILON) + return {}; + + const int from_axis = slicing_params.belt_floor_from_axis; + const double floor_offset = print_config.belt_support_floor_offset.value; + + const double z_shift = slicing_params.belt_floor_z_shift; + const double cutoff = (print_z - z_shift - floor_offset) / shear_factor; + const coord_t cutoff_scaled = scale_(cutoff); + const coord_t large_bound = scale_(1e4); + + // Valid region: the complement of the belt surface polygon. + Polygon valid_poly; + if (from_axis == 0) { + if (shear_factor > 0) { + // Valid: X < cutoff + valid_poly.points = { + Point(-large_bound, -large_bound), + Point(cutoff_scaled, -large_bound), + Point(cutoff_scaled, large_bound), + Point(-large_bound, large_bound) + }; + } else { + // Valid: X > cutoff + valid_poly.points = { + Point(cutoff_scaled, -large_bound), + Point(large_bound, -large_bound), + Point(large_bound, large_bound), + Point(cutoff_scaled, large_bound) + }; + } + } else { + if (shear_factor > 0) { + // Valid: Y < cutoff + valid_poly.points = { + Point(-large_bound, -large_bound), + Point( large_bound, -large_bound), + Point( large_bound, cutoff_scaled), + Point(-large_bound, cutoff_scaled) + }; + } else { + // Valid: Y > cutoff + valid_poly.points = { + Point(-large_bound, cutoff_scaled), + Point( large_bound, cutoff_scaled), + Point( large_bound, large_bound), + Point(-large_bound, large_bound) + }; + } + } + return { valid_poly }; +} + // Collect outer contours of all slices of this layer. // This is useful for calculating the support base with holes filled. Polygons collect_slices_outer(const Layer &layer) @@ -2516,6 +2661,82 @@ static inline SupportGeneratorLayer* detect_bottom_contacts( return &layer_new; } +// Belt printer: detect bottom contacts where support meets the belt floor plane. +// Modeled on detect_bottom_contacts() but uses the belt plane polygon instead of stTop surfaces. +static inline SupportGeneratorLayer* detect_belt_floor_bottom_contacts( + const SlicingParameters &slicing_params, + const SupportParameters &support_params, + const PrintConfig &print_config, + const PrintObject &object, + const Layer &layer, + // Existing top contact layers, for snapping. + const SupportGeneratorLayersPtr &top_contacts, + size_t contact_idx, + SupportGeneratorLayerStorage &layer_storage, + std::vector &layer_support_areas, + const Polygons &supports_projected) +{ + // Compute the belt surface polygon at this layer's Z. + Polygons belt_surface = belt_floor_surface_polygon(slicing_params, print_config, object, layer.print_z); + if (belt_surface.empty()) + return nullptr; + + // Find where projected support overlaps the belt surface. + Polygons touching = intersection(belt_surface, supports_projected); + if (touching.empty()) + return nullptr; + + assert(layer.id() >= slicing_params.raft_layers()); + size_t layer_id = layer.id() - slicing_params.raft_layers(); + + // Allocate a new bottom contact layer resting on the belt plane. + SupportGeneratorLayer &layer_new = layer_storage.allocate_unguarded(SupporLayerType::BottomContact); + + // No object layer to sync with -- compute heights directly from flow parameters. + layer_new.height = support_params.support_material_bottom_interface_flow.height(); + layer_new.print_z = layer.print_z + layer_new.height + slicing_params.gap_object_support; + layer_new.bottom_z = layer.print_z; + layer_new.idx_object_layer_below = layer_id; + layer_new.bridging = ! slicing_params.soluble_interface && object.config().thick_bridges; + layer_new.polygons = expand(touching, float(support_params.support_material_flow.scaled_width()), SUPPORT_SURFACES_OFFSET_PARAMETERS); + + if (! slicing_params.soluble_interface) { + // Snap to nearby top contact layers to avoid very thin support layers. + for (size_t top_idx = size_t(std::max(0, int(contact_idx))); + top_idx < top_contacts.size() && top_contacts[top_idx]->print_z < layer_new.print_z + support_params.support_layer_height_min + EPSILON; + ++ top_idx) { + if (top_contacts[top_idx]->print_z > layer_new.print_z - support_params.support_layer_height_min - EPSILON) { + coordf_t diff = layer_new.print_z - top_contacts[top_idx]->print_z; + assert(std::abs(diff) <= support_params.support_layer_height_min + EPSILON); + if (diff > 0.F) { + if (layer_new.height - diff > support_params.support_layer_height_min) { + layer_new.print_z = top_contacts[top_idx]->print_z; + layer_new.height -= diff; + } else { + continue; + } + } else { + layer_new.print_z = top_contacts[top_idx]->print_z; + layer_new.height -= diff; + } + break; + } + } + } + + // Trim the already created base layers above this belt contact. + touching = expand(touching, float(SCALED_EPSILON)); + for (int layer_id_above = int(layer_id) + 1; layer_id_above < int(object.total_layer_count()); ++ layer_id_above) { + const Layer &layer_above = *object.layers()[layer_id_above]; + if (layer_above.print_z > layer_new.print_z - EPSILON) + break; + if (Polygons &above = layer_support_areas[layer_id_above]; ! above.empty()) + above = diff(above, touching); + } + + return &layer_new; +} + // Returns polygons to print + polygons to propagate downwards. // Called twice: First for normal supports, possibly trimmed by "on build plate only", second for support enforcers not trimmed by "on build plate only". static inline std::pair project_support_to_grid(const Layer &layer, const SupportGridParams &grid_params, const Polygons &overhangs, Polygons *layer_buildplate_covered @@ -2615,6 +2836,8 @@ 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; // Allocate empty surface areas, one per object layer. layer_support_areas.assign(object.total_layer_count(), Polygons()); @@ -2675,8 +2898,9 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::bottom_contact_layers_and_ tbb::task_group task_group; const Polygons &overhangs_for_bottom_contacts = buildplate_only ? enforcers_projection_raw : overhangs_projection_raw; if (! overhangs_for_bottom_contacts.empty()) - // Find the bottom contact layers above the top surfaces of this layer. - task_group.run([this, &object, &layer, &top_contacts, contact_idx, &layer_storage, &layer_support_areas, &bottom_contacts, &overhangs_for_bottom_contacts + // Find the bottom contact layers above the top surfaces of this layer, + // and also detect belt floor contacts if belt mode is active. + task_group.run([this, &object, &layer, &top_contacts, contact_idx, &layer_storage, &layer_support_areas, &bottom_contacts, &overhangs_for_bottom_contacts, has_belt_floor #ifdef SLIC3R_DEBUG , iRun, &polygons_new #endif // SLIC3R_DEBUG @@ -2690,6 +2914,15 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::bottom_contact_layers_and_ ); if (layer_new) bottom_contacts.push_back(layer_new); + // Belt floor phantom surface: detect where support meets the belt plane. + if (has_belt_floor) { + SupportGeneratorLayer *belt_layer = detect_belt_floor_bottom_contacts( + m_slicing_params, m_support_params, *m_print_config, object, + layer, top_contacts, contact_idx, layer_storage, + layer_support_areas, overhangs_for_bottom_contacts); + if (belt_layer) + bottom_contacts.push_back(belt_layer); + } }); Polygons &layer_support_area = layer_support_areas[layer_id]; @@ -2728,6 +2961,23 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::bottom_contact_layers_and_ task_group.wait(); + // Belt floor: clip projections and support areas so support doesn't + // propagate below the belt plane. + if (has_belt_floor) { + Polygons valid_region = belt_floor_valid_region_polygon( + m_slicing_params, *m_print_config, object, layer.print_z); + if (! valid_region.empty()) { + if (! overhangs_projection.empty()) + overhangs_projection = intersection(overhangs_projection, valid_region); + if (! enforcers_projection.empty()) + enforcers_projection = intersection(enforcers_projection, valid_region); + if (! layer_support_area.empty()) + layer_support_area = intersection(layer_support_area, valid_region); + if (! layer_support_area_enforcers.empty()) + layer_support_area_enforcers = intersection(layer_support_area_enforcers, valid_region); + } + } + if (! layer_support_area_enforcers.empty()) { if (layer_support_area.empty()) layer_support_area = std::move(layer_support_area_enforcers); @@ -2738,6 +2988,7 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::bottom_contact_layers_and_ std::reverse(bottom_contacts.begin(), bottom_contacts.end()); trim_support_layers_by_object(object, bottom_contacts, m_slicing_params.gap_support_object, m_slicing_params.gap_object_support, m_support_params.gap_xy); + trim_support_layers_by_belt_floor(m_slicing_params, *m_print_config, object, bottom_contacts); return bottom_contacts; } @@ -3108,6 +3359,37 @@ void PrintObjectSupportMaterial::generate_base_layers( #endif /* SLIC3R_DEBUG */ this->trim_support_layers_by_object(object, intermediate_layers, m_slicing_params.gap_support_object, m_slicing_params.gap_object_support, m_support_params.gap_xy); + trim_support_layers_by_belt_floor(m_slicing_params, *m_print_config, object, intermediate_layers); +} + +// Belt printer: trim support layer polygons by the belt floor plane. +// For each support layer, computes the belt floor half-plane at that layer's print_z +// and subtracts it from the support polygons. This follows the same diff() pattern +// as trim_support_layers_by_object() so that interface layers derived from trimmed +// intermediates automatically inherit the belt floor trimming. +static void trim_support_layers_by_belt_floor( + const SlicingParameters &slicing_params, + const PrintConfig &print_config, + const PrintObject &object, + SupportGeneratorLayersPtr &support_layers) +{ + if (std::abs(slicing_params.belt_floor_shear_factor) < EPSILON) + return; + if (print_config.belt_support_floor_mode.value != BeltSupportFloorMode::GeneratorOnly) + return; + + tbb::parallel_for(tbb::blocked_range(0, support_layers.size()), + [&](const tbb::blocked_range &range) { + for (size_t i = range.begin(); i < range.end(); ++ i) { + SupportGeneratorLayer *layer = support_layers[i]; + if (layer->polygons.empty()) + continue; + Polygons belt_surface = belt_floor_surface_polygon( + slicing_params, print_config, object, layer->print_z); + if (! belt_surface.empty()) + layer->polygons = diff(layer->polygons, belt_surface); + } + }); } void PrintObjectSupportMaterial::trim_support_layers_by_object( diff --git a/src/libslic3r/Support/TreeModelVolumes.cpp b/src/libslic3r/Support/TreeModelVolumes.cpp index 0267c0c327..e270b62aab 100644 --- a/src/libslic3r/Support/TreeModelVolumes.cpp +++ b/src/libslic3r/Support/TreeModelVolumes.cpp @@ -94,6 +94,46 @@ TreeModelVolumes::TreeModelVolumes( #else { m_anti_overhang = print_object.slice_support_blockers(); + // Belt floor: add belt surface polygons to anti_overhang so support + // is never generated inside the belt. Only in global shear mode — + // in local mode the belt floor clipping handles everything and + // anti_overhang at the bottom layers would block all support. + { + const auto &sp = print_object.slicing_parameters(); + const auto &pcfg = print_object.print()->config(); + const double sf = sp.belt_floor_shear_factor; + if (std::abs(sf) > EPSILON + && std::abs(print_object.belt_global_z_offset()) > EPSILON + && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + const int from_axis = sp.belt_floor_from_axis; + const double floor_off = pcfg.belt_support_floor_offset.value; + const double z_shift = sp.belt_floor_z_shift - print_object.belt_global_z_offset(); + size_t num_layers_needed = print_object.layer_count(); + // Ensure m_anti_overhang is large enough. + if (m_anti_overhang.size() < num_layers_needed) + m_anti_overhang.resize(num_layers_needed, Polygons{}); + for (size_t layer_idx = 0; layer_idx < num_layers_needed; ++layer_idx) { + double print_z = print_object.get_layer(layer_idx)->print_z + - print_object.belt_global_z_offset(); + double cutoff = (print_z - z_shift - floor_off) / sf; + coord_t cutoff_sc = scale_(cutoff); + coord_t big = scale_(1e4); + Polygon belt_poly; + if (from_axis == 0) { + if (sf > 0) + belt_poly.points = {{cutoff_sc,-big},{big,-big},{big,big},{cutoff_sc,big}}; + else + belt_poly.points = {{-big,-big},{cutoff_sc,-big},{cutoff_sc,big},{-big,big}}; + } else { + if (sf > 0) + belt_poly.points = {{-big,cutoff_sc},{big,cutoff_sc},{big,big},{-big,big}}; + else + belt_poly.points = {{-big,-big},{big,-big},{big,cutoff_sc},{-big,cutoff_sc}}; + } + append(m_anti_overhang[layer_idx], Polygons{belt_poly}); + } + } + } TreeSupportMeshGroupSettings mesh_settings(print_object); const TreeSupportSettings config{ mesh_settings, print_object.slicing_parameters() }; m_current_min_xy_dist = config.xy_min_distance; @@ -102,6 +142,27 @@ 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(print_object.model_object()->raw_bounding_box().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 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()); + } + } + } m_current_outline_idx = 0; m_layer_outlines.emplace_back(mesh_settings, std::vector{}); @@ -114,6 +175,47 @@ TreeModelVolumes::TreeModelVolumes( for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) outlines[layer_idx] = polygons_simplify(to_polygons(print_object.get_layer(layer_idx - num_raft_layers)->lslices), mesh_settings.resolution, polygons_strictly_simple); }); + + // Belt floor: pre-compute belt surface polygon per-layer for clipping. + // Branches grow toward the belt and their slices are clipped at the belt + // surface in organic_draw_branches(). The organic pipeline works in LOCAL + // Z (no global_z_offset), so use local z_shift and local print_z. + const auto &slicing_params = print_object.slicing_parameters(); + const auto &pcfg = print_object.print()->config(); + const double sf = slicing_params.belt_floor_shear_factor; + if (std::abs(sf) > EPSILON + && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + const int from_axis = slicing_params.belt_floor_from_axis; + const double floor_off = pcfg.belt_support_floor_offset.value; + // Subtract global_z_offset to get the LOCAL z_shift — the organic + // pipeline's Z coordinates don't include the global offset. + const double z_shift = slicing_params.belt_floor_z_shift + - print_object.belt_global_z_offset(); + m_belt_floor.assign(num_layers, Polygons{}); + for (size_t layer_idx = 0; layer_idx < num_layers; ++layer_idx) { + // Use local print_z (subtract global offset from object layer). + double print_z = (layer_idx >= num_raft_layers) + ? print_object.get_layer(layer_idx - num_raft_layers)->print_z + - print_object.belt_global_z_offset() + : 0.; + double cutoff = (print_z - z_shift - floor_off) / sf; + coord_t cutoff_sc = scale_(cutoff); + coord_t big = scale_(1e4); + Polygon belt_poly; + if (from_axis == 0) { + if (sf > 0) + belt_poly.points = {{cutoff_sc,-big},{big,-big},{big,big},{cutoff_sc,big}}; + else + belt_poly.points = {{-big,-big},{cutoff_sc,-big},{cutoff_sc,big},{-big,big}}; + } else { + if (sf > 0) + belt_poly.points = {{-big,cutoff_sc},{big,cutoff_sc},{big,big},{-big,big}}; + else + belt_poly.points = {{-big,-big},{big,-big},{big,cutoff_sc},{-big,cutoff_sc}}; + } + m_belt_floor[layer_idx] = { belt_poly }; + } + } } #endif @@ -469,9 +571,9 @@ void TreeModelVolumes::calculateCollision(const coord_t radius, const LayerIndex }); // 2) Sum over top / bottom ranges. - const bool processing_last_mesh = outline_idx == layer_outline_indices.size(); + const bool processing_last_mesh = outline_idx == layer_outline_indices.back(); tbb::parallel_for(tbb::blocked_range(data.begin(), data.end()), - [&collision_areas_offsetted, &outlines, &machine_border = m_machine_border, &anti_overhang = m_anti_overhang, radius, + [&collision_areas_offsetted, &outlines, &machine_border = m_machine_border, &anti_overhang = m_anti_overhang, radius, xy_distance, z_distance_bottom_layers, z_distance_top_layers, min_resolution = m_min_resolution, &data, processing_last_mesh, &throw_on_cancel] (const tbb::blocked_range& range) { for (LayerIndex layer_idx = range.begin(); layer_idx != range.end(); ++ layer_idx) { @@ -517,9 +619,14 @@ void TreeModelVolumes::calculateCollision(const coord_t radius, const LayerIndex // not support an overhang<90 degree than to risk fusing to it. append(collisions, offset(union_ex(collision_areas_original), radius + required_range_x, ClipperLib::jtMiter, 1.2)); } - collisions = processing_last_mesh && layer_idx < int(anti_overhang.size()) ? - union_(collisions, offset(union_ex(anti_overhang[layer_idx]), radius, ClipperLib::jtMiter, 1.2)) : - union_(collisions); + if (processing_last_mesh) { + if (layer_idx < int(anti_overhang.size())) + append(collisions, offset(union_ex(anti_overhang[layer_idx]), radius, ClipperLib::jtMiter, 1.2)); + // NOTE: m_belt_floor is NOT added to collision here — branches + // should grow toward the belt and terminate at it, not avoid it. + // Belt floor clipping is done post-generation in organic_draw_branches(). + } + collisions = union_(collisions); auto &dst = data[layer_idx]; if (processing_last_mesh) { if (! dst.empty()) diff --git a/src/libslic3r/Support/TreeModelVolumes.hpp b/src/libslic3r/Support/TreeModelVolumes.hpp index eff4cadcaf..0ded395091 100644 --- a/src/libslic3r/Support/TreeModelVolumes.hpp +++ b/src/libslic3r/Support/TreeModelVolumes.hpp @@ -168,6 +168,9 @@ public: } Polygon m_bed_area; + // Belt floor polygons per layer — used for post-generation clipping + // in organic_draw_branches(). Public so the organic pipeline can access it. + std::vector m_belt_floor; private: // Caching polygons for a range of layers. diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 864a0eb0e0..4966a5a139 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -637,6 +637,20 @@ TreeSupport::TreeSupport(PrintObject& object, const SlicingParameters &slicing_p } +double TreeSupport::belt_floor_print_z(const Point &pos_slicing) const +{ + double sf = m_slicing_params.belt_floor_shear_factor; + if (std::abs(sf) < EPSILON) + return -std::numeric_limits::max(); // no belt floor + int from = m_slicing_params.belt_floor_from_axis; + // Belt floor in slicing coords: Z = sf * Y + z_shift + floor_offset. + // Inverse of cutoff = (Z - z_shift - floor_offset) / sf. + double pos = unscale(from == 0 ? pos_slicing.x() : pos_slicing.y()); + double floor_offset = m_print_config->belt_support_floor_offset.value; + double z_shift = m_slicing_params.belt_floor_z_shift; + return sf * pos + floor_offset + z_shift; +} + #define SUPPORT_SURFACES_OFFSET_PARAMETERS ClipperLib::jtSquare, 0. void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) { @@ -2141,6 +2155,41 @@ void TreeSupport::draw_circles() base_areas = diff_ex(base_areas, ClipperUtils::clip_clipper_polygons_with_subject_bbox(roofs, get_extents(base_areas))); base_areas = intersection_ex(base_areas, m_machine_border); + // Belt floor: clip tree support polygons by the belt surface plane. + // ts_layer->print_z is at LOCAL Z (global offset applied later in + // _generate_support_material), but belt_floor_z_shift includes + // global_z_offset — subtract it to get the cutoff in local coords. + if (std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON + && m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + const double sf = m_slicing_params.belt_floor_shear_factor; + const int from_axis = m_slicing_params.belt_floor_from_axis; + const double floor_off = m_print_config->belt_support_floor_offset.value; + const double z_shift_local = m_slicing_params.belt_floor_z_shift + - m_object->belt_global_z_offset(); + const double cutoff = (ts_layer->print_z - z_shift_local - floor_off) / sf; + const coord_t cutoff_sc = scale_(cutoff); + const coord_t big = scale_(1e4); + + Polygon belt_poly; + if (from_axis == 0) { + if (sf > 0) + belt_poly.points = { {cutoff_sc,-big}, {big,-big}, {big,big}, {cutoff_sc,big} }; + else + belt_poly.points = { {-big,-big}, {cutoff_sc,-big}, {cutoff_sc,big}, {-big,big} }; + } else { + if (sf > 0) + belt_poly.points = { {-big,cutoff_sc}, {big,cutoff_sc}, {big,big}, {-big,big} }; + else + belt_poly.points = { {-big,-big}, {big,-big}, {big,cutoff_sc}, {-big,cutoff_sc} }; + } + Polygons belt_surface = { belt_poly }; + base_areas = diff_ex(base_areas, belt_surface); + roof_areas = diff_ex(roof_areas, belt_surface); + roof_1st_layer = diff_ex(roof_1st_layer, belt_surface); + floor_areas = diff_ex(floor_areas, belt_surface); + roof_gap_areas = diff_ex(roof_gap_areas, belt_surface); + } + if (SQUARE_SUPPORT) { // simplify support contours ExPolygons base_areas_simplified; @@ -2445,6 +2494,9 @@ void TreeSupport::drop_nodes() 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 size_t top_interface_layers = config.support_interface_top_layers.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 size_t bottom_interface_layers = config.support_interface_bottom_layers.value < 0 ? top_interface_layers : config.support_interface_bottom_layers.value; 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 @@ -2677,15 +2729,22 @@ 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); - const bool to_buildplate = !is_inside_ex(get_collision(0, obj_layer_nr_next), next_position); - SupportNode* next_node = m_ts_data->create_node(next_position, node_parent->distance_to_top + 1, obj_layer_nr_next, node_parent->support_roof_layers_below - 1, to_buildplate, node_parent, - print_z_next, height_next); - get_max_move_dist(next_node); - m_ts_data->m_mutex.lock(); - contact_nodes[layer_nr_next].push_back(next_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)) { + node_parent->to_buildplate = false; + } else { + const bool to_buildplate = !is_inside_ex(get_collision(0, obj_layer_nr_next), next_position); + SupportNode* next_node = m_ts_data->create_node(next_position, node_parent->distance_to_top + 1, obj_layer_nr_next, node_parent->support_roof_layers_below - 1, to_buildplate, node_parent, + print_z_next, height_next); + get_max_move_dist(next_node); + m_ts_data->m_mutex.lock(); + contact_nodes[layer_nr_next].push_back(next_node); + m_ts_data->m_mutex.unlock(); + } neighbour->valid = false; p_node->valid = false; - m_ts_data->m_mutex.unlock(); } else if (neighbours.size() > 1) //Don't merge leaf nodes because we would then incur movement greater than the maximum move distance. { @@ -2729,6 +2788,12 @@ 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. + // Treat as object-surface termination (not buildplate). + if (has_belt_floor && print_z_next <= belt_floor_print_z(next_pt)) { + p_node->to_buildplate = false; + continue; + } SupportNode *next_node = m_ts_data->create_node(next_pt, p_node->distance_to_top + 1, obj_layer_nr_next, p_node->support_roof_layers_below - 1, to_buildplate, p_node, print_z_next, height_next); next_node->max_move_dist = 0; @@ -2873,6 +2938,12 @@ void TreeSupport::drop_nodes() if (is_outside) { next_layer_vertex = candidate_vertex; } } } + // Belt floor: don't drop regular node below belt surface. + // Treat as object-surface termination (not buildplate). + if (has_belt_floor && print_z_next <= belt_floor_print_z(next_layer_vertex)) { + p_node->to_buildplate = false; + return; // from parallel_for_each lambda + } auto next_collision = get_collision(0, obj_layer_nr_next); const bool to_buildplate = !is_inside_ex(m_ts_data->m_layer_outlines[obj_layer_nr_next], next_layer_vertex); SupportNode * next_node = m_ts_data->create_node(next_layer_vertex, node.distance_to_top + 1, obj_layer_nr_next, node.support_roof_layers_below - 1, to_buildplate, p_node, @@ -3167,6 +3238,9 @@ void TreeSupport::generate_contact_points() bool on_buildplate_only = m_object_config->support_on_build_plate_only.value; const bool roof_enabled = config.support_interface_top_layers.value > 0; const bool force_tip_to_roof = roof_enabled && m_support_params.soluble_interface; + 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; //First generate grid points to cover the entire area of the print. BoundingBox bounding_box = m_object->bounding_box(); @@ -3257,6 +3331,10 @@ void TreeSupport::generate_contact_points() auto insert_point = [&](Point pt, const ExPolygon& overhang, double radius, bool force_add = false, bool add_interface=true) { + // Belt floor: skip contact points whose bottom_z is at or below + // the belt floor at this XY position (overhang rests on the belt). + if (has_belt_floor && bottom_z <= belt_floor_print_z(pt)) + return (SupportNode*) nullptr; Point hash_pos = pt / ((radius_scaled + 1) / 1); SupportNode* contact_node = nullptr; if (force_add || !already_inserted.count(hash_pos)) { @@ -3292,8 +3370,10 @@ void TreeSupport::generate_contact_points() double radius = unscale_(overhang_bounds.radius()); Point candidate = overhang_bounds.center(); SupportNode *contact_node = insert_point(candidate, overhang, radius, true, true); - contact_node->type = ePolygon; - curr_nodes.emplace_back(contact_node); + if (contact_node) { + contact_node->type = ePolygon; + curr_nodes.emplace_back(contact_node); + } } }else{ // otherwise, all nodes should be circle nodes diff --git a/src/libslic3r/Support/TreeSupport.hpp b/src/libslic3r/Support/TreeSupport.hpp index e0446ad5f1..c3bd186df1 100644 --- a/src/libslic3r/Support/TreeSupport.hpp +++ b/src/libslic3r/Support/TreeSupport.hpp @@ -446,6 +446,10 @@ private: bool is_slim = false; bool with_infill = false; + // 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; + /*! diff --git a/src/libslic3r/Support/TreeSupport3D.cpp b/src/libslic3r/Support/TreeSupport3D.cpp index 2c24b933b2..b551a42ece 100644 --- a/src/libslic3r/Support/TreeSupport3D.cpp +++ b/src/libslic3r/Support/TreeSupport3D.cpp @@ -3366,6 +3366,36 @@ 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(); + const double sf = sp.belt_floor_shear_factor; + if (std::abs(sf) > EPSILON && 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(po.model_object()->raw_bounding_box().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 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(); @@ -3561,6 +3591,43 @@ static void generate_support_areas(Print &print, TreeSupport* tree_support, cons if (layer) layer->polygons = intersection(layer->polygons, volumes.m_bed_area); }); + // Belt floor: clip ALL organic support layers (including intermediate/base + // fill) against the belt surface. The branch slices were already clipped + // in organic_draw_branches(), but intermediate layers generated between + // branches and the build plate need clipping too. + // Compute the belt floor polygon directly from each layer's print_z + // rather than mapping to a layer index (avoids index mismatch issues). + { + const auto &sp = print_object.slicing_parameters(); + const double sf = sp.belt_floor_shear_factor; + const double z_shift = sp.belt_floor_z_shift - print_object.belt_global_z_offset(); + const double floor_off = print_object.print()->config().belt_support_floor_offset.value; + const int from_axis = sp.belt_floor_from_axis; + if (std::abs(sf) > EPSILON + && print_object.print()->config().belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + tbb::parallel_for_each(layers_sorted.begin(), layers_sorted.end(), [&](SupportGeneratorLayer *layer) { + if (!layer || layer->polygons.empty()) + return; + double cutoff = (layer->print_z - z_shift - floor_off) / sf; + coord_t cutoff_sc = scale_(cutoff); + coord_t big = scale_(1e4); + Polygon belt_poly; + if (from_axis == 0) { + if (sf > 0) + belt_poly.points = {{cutoff_sc,-big},{big,-big},{big,big},{cutoff_sc,big}}; + else + belt_poly.points = {{-big,-big},{cutoff_sc,-big},{cutoff_sc,big},{-big,big}}; + } else { + if (sf > 0) + belt_poly.points = {{-big,cutoff_sc},{big,cutoff_sc},{big,big},{-big,big}}; + else + belt_poly.points = {{-big,-big},{big,-big},{big,cutoff_sc},{-big,cutoff_sc}}; + } + layer->polygons = diff(layer->polygons, Polygons{belt_poly}); + }); + } + } + print.set_status(69, _L("Generating support")); generate_support_toolpaths(print_object.support_layers(), print_object.config(), support_params, print_object.slicing_parameters(), raft_layers, bottom_contacts, top_contacts, intermediate_layers, interface_layers, base_interface_layers); @@ -3816,6 +3883,10 @@ void organic_draw_branches( for (LayerIndex i = 0; i < LayerIndex(slices.size()); ++i) { slices[i] = diff_clipped(slices[i], volumes.getCollision(0, layer_begin + i, true)); // FIXME parent_uses_min || draw_area.element->state.use_min_xy_dist); slices[i] = intersection(slices[i], volumes.m_bed_area); + // 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]); } size_t num_empty = 0; if (slices.front().empty()) { @@ -3850,6 +3921,9 @@ void organic_draw_branches( //double support_area_min = 0.1 * support_area_min_radius; for (LayerIndex layer_idx = layer_begin - 1; layer_idx >= layer_bottommost; -- layer_idx) { rest_support = diff_clipped(rest_support.empty() ? slices.front() : rest_support, volumes.getCollision(0, layer_idx, false)); + // Belt floor: clip propagated support at belt surface. + if (layer_idx < LayerIndex(volumes.m_belt_floor.size()) && !volumes.m_belt_floor[layer_idx].empty()) + rest_support = diff(rest_support, volumes.m_belt_floor[layer_idx]); double rest_support_area = area(rest_support); if (rest_support_area < support_area_stop) // Don't propagate a fraction of the tree contact surface. diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 25b4ba4e9c..b9fe17ff2a 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -2210,17 +2210,11 @@ void GCodeViewer::render_toolpaths() { const Camera& camera = wxGetApp().plater()->get_camera(); Matrix4f view = camera.get_view_matrix().matrix().cast(); - // Belt "designed" view: apply inverse shear to view matrix so toolpaths appear - // upright (as originally designed) instead of sheared on the belt. - if (m_belt_show_designed && m_belt_view_enabled && m_belt_angle_deg > 0.f) { - double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle_deg)); - double sin_a = std::sin(angle_rad); - if (sin_a > 1e-6) { - double cot_alpha = std::cos(angle_rad) / sin_a; - Transform3d inverse_shear = Transform3d::Identity(); - inverse_shear.matrix()(1, 2) = -cot_alpha; // Y -= Z * cot(α) - view = (camera.get_view_matrix() * inverse_shear).matrix().cast(); - } + // Belt "designed" view: apply the precomputed inverse of the full belt + // shear+scale transform so toolpaths appear upright (as originally designed) + // instead of transformed on the belt. + if (m_belt_show_designed && m_belt_view_enabled) { + view = (camera.get_view_matrix() * m_belt_inverse_transform).matrix().cast(); } const libvgcode::Mat4x4 converted_view_matrix = libvgcode::convert(view); const libvgcode::Mat4x4 converted_projetion_matrix = libvgcode::convert(static_cast(camera.get_projection_matrix().matrix().cast())); @@ -4418,12 +4412,18 @@ 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 raw slicing-frame G-code + // 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::Checkbox("Show designed view (upright)", &m_belt_show_designed); + ImGui::TextColored(ImVec4(0.f, 0.59f, 0.53f, 1.f), "%s", _u8L("Belt Printer").c_str()); + ImGui::Dummy({ window_padding, 0 }); + ImGui::SameLine(); + ImGui::Checkbox(_u8L("Show designed view (upright) [B]").c_str(), &m_belt_show_designed); } legend_height = ImGui::GetCurrentWindow()->Size.y; diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 184c56e915..cd97613b2a 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -240,6 +240,7 @@ 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 = false; // Toggle: show designed (upright) view via inverse shear + Transform3d m_belt_inverse_transform{Transform3d::Identity()}; libvgcode::Viewer m_viewer; bool m_loaded_as_preview{ false }; @@ -341,7 +342,10 @@ public: void export_toolpaths_to_obj(const char* filename) const; void set_belt_printer(bool enabled, float angle_deg) { m_belt_view_enabled = enabled; m_belt_angle_deg = angle_deg; } + void set_belt_inverse_transform(const Transform3d& t) { m_belt_inverse_transform = t; } 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(); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 2c8285720b..b8947d77b0 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -3463,8 +3463,16 @@ void GLCanvas3D::on_char(wxKeyEvent& evt) post_event(SimpleEvent(EVT_GLCANVAS_ARRANGE)); break; } - //case 'B': - //case 'b': { zoom_to_bed(); break; } + case 'B': + case 'b': { + // Toggle belt printer "show designed" view when in G-code preview with belt mode active. + if (dynamic_cast(m_canvas->GetParent()) != nullptr && + m_gcode_viewer.is_belt_view()) { + m_gcode_viewer.toggle_belt_show_designed(); + m_dirty = true; + } + break; + } case 'C': case 'c': { wxGetApp().toggle_show_gcode_window(); m_dirty = true; request_extra_frame(); break; } //case 'G': diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index fe2479386b..a3c68d34d4 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -2516,7 +2516,7 @@ void ObjectList::load_mesh_object(const TriangleMesh &mesh, const wxString &name Slic3r::save_object_mesh(*new_object); // BBS: find an empty cell to put the copied object - auto start_point = wxGetApp().plater()->build_volume().bounding_volume2d().center(); + auto start_point = wxGetApp().plater()->build_volume().bed_center(); auto empty_cell = wxGetApp().plater()->canvas3D()->get_nearest_empty_cell({start_point(0), start_point(1)}); new_object->instances[0]->set_offset(center ? to_3d(Vec2d(empty_cell(0), empty_cell(1)), -new_object->origin_translation.z()) : bb.center()); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 6e5872d074..b6c3e28553 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -6907,7 +6907,7 @@ std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& mode // BBS: find an empty cell to put the copied object for (auto& instance : new_instances) { auto offset = instance->get_offset(); - auto start_point = this->bed.build_volume().bounding_volume2d().center(); + auto start_point = this->bed.build_volume().bed_center(); bool plate_empty = partplate_list.get_curr_plate()->empty(); Vec3d displacement; if (plate_empty) @@ -10991,10 +10991,83 @@ void Plater::priv::set_bed_shape(const Pointfs &shape, bed.set_belt_printer(true, static_cast(belt_angle)); if (preview) preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(true, static_cast(belt_angle)); + + // Compute the inverse of the full belt shear+scale transform for the G-code viewer. + auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad); + double cos_a = std::cos(angle_rad); + switch (mode) { + case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; + case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; + case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; + case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; + default: return 0.; + } + }; + auto compute_scale_factor = [](BeltScaleMode mode, double angle_deg) -> double { + if (mode == BeltScaleMode::None) return 1.; + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad); + double cos_a = std::cos(angle_rad); + switch (mode) { + case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; + case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; + case BeltScaleMode::Sin: return sin_a; + case BeltScaleMode::Cos: return cos_a; + default: return 1.; + } + }; + + // Read shear configs. + auto get_shear_mode = [this](const char *key) -> BeltShearMode { + auto opt = config->option>(key); + return opt ? opt->value : BeltShearMode::None; + }; + auto get_axis = [this](const char *key) -> BeltAxis { + auto opt = config->option>(key); + return opt ? opt->value : BeltAxis::X; + }; + auto get_scale_mode = [this](const char *key) -> BeltScaleMode { + auto opt = config->option>(key); + return opt ? opt->value : BeltScaleMode::None; + }; + + struct AxisShear { BeltShearMode mode; double angle; int from; }; + AxisShear axes[3] = { + { get_shear_mode("belt_shear_x"), config->opt_float("belt_shear_x_angle"), int(get_axis("belt_shear_x_from")) }, + { get_shear_mode("belt_shear_y"), config->opt_float("belt_shear_y_angle"), int(get_axis("belt_shear_y_from")) }, + { get_shear_mode("belt_shear_z"), config->opt_float("belt_shear_z_angle"), int(get_axis("belt_shear_z_from")) }, + }; + + Transform3d belt_shear = Transform3d::Identity(); + for (int row = 0; row < 3; ++row) { + if (axes[row].mode != BeltShearMode::None) { + double factor = compute_shear_factor(axes[row].mode, axes[row].angle); + if (std::abs(factor) > EPSILON) + belt_shear.matrix()(row, axes[row].from) += factor; + } + } + + double sx = compute_scale_factor(get_scale_mode("belt_scale_x"), config->opt_float("belt_scale_x_angle")); + double sy = compute_scale_factor(get_scale_mode("belt_scale_y"), config->opt_float("belt_scale_y_angle")); + double sz = compute_scale_factor(get_scale_mode("belt_scale_z"), config->opt_float("belt_scale_z_angle")); + Transform3d belt_scale = Transform3d::Identity(); + belt_scale.matrix()(0, 0) = sx; + belt_scale.matrix()(1, 1) = sy; + belt_scale.matrix()(2, 2) = sz; + + // Forward transform: scale * shear. Inverse for the viewer. + Transform3d forward = belt_scale * belt_shear; + Transform3d inverse = forward.inverse(); + if (preview) + preview->get_canvas3d()->get_gcode_viewer().set_belt_inverse_transform(inverse); } else { bed.set_belt_printer(false, 0.f); - if (preview) + if (preview) { preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(false, 0.f); + preview->get_canvas3d()->get_gcode_viewer().set_belt_inverse_transform(Transform3d::Identity()); + } } } @@ -13286,6 +13359,15 @@ void Plater::load_gcode(const wxString& filename) current_print.set_gcode_file_ready(); + // Belt printer: detect belt_printer_angle from loaded G-code header and enable + // belt view mode on the GCodeViewer so the "Show designed view" toggle appears. + if (current_result->belt_printer_angle > 0.f) { + float angle = current_result->belt_printer_angle; + p->preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(true, angle); + } else { + p->preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(false, 0.f); + } + // show results p->preview->reload_print(m_only_gcode); //BBS: zoom to bed 0 for gcode preview diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 9d1ee6b2ad..d997fbb7a9 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4365,58 +4365,6 @@ void TabPrinter::build_fff() optgroup->append_single_option_line(option, "printer_basic_information_printable_space#excluded-bed-area"); // optgroup->append_single_option_line("printable_area"); optgroup->append_single_option_line("printable_height", "printer_basic_information_printable_space#printable-height"); - optgroup->append_single_option_line("build_plate_tilt_x"); - optgroup->append_single_option_line("build_plate_tilt_y"); - optgroup->append_single_option_line("belt_printer"); - optgroup->append_single_option_line("belt_printer_angle"); - optgroup->append_single_option_line("belt_printer_infinite_y"); - // Per-axis shear: group mode + angle + source on one row per axis - { - Line line = { L("Shear X"), L("Shear applied to the X axis before slicing") }; - line.append_option(optgroup->get_option("belt_shear_x")); - line.append_option(optgroup->get_option("belt_shear_x_angle")); - line.append_option(optgroup->get_option("belt_shear_x_from")); - optgroup->append_line(line); - } - { - Line line = { L("Shear Y"), L("Shear applied to the Y axis before slicing") }; - line.append_option(optgroup->get_option("belt_shear_y")); - line.append_option(optgroup->get_option("belt_shear_y_angle")); - line.append_option(optgroup->get_option("belt_shear_y_from")); - optgroup->append_line(line); - } - { - Line line = { L("Shear Z"), L("Shear applied to the Z axis before slicing") }; - line.append_option(optgroup->get_option("belt_shear_z")); - line.append_option(optgroup->get_option("belt_shear_z_angle")); - line.append_option(optgroup->get_option("belt_shear_z_from")); - optgroup->append_line(line); - } - { - Line line = { L("Scale X"), L("Scale applied to the X axis before slicing") }; - line.append_option(optgroup->get_option("belt_scale_x")); - line.append_option(optgroup->get_option("belt_scale_x_angle")); - optgroup->append_line(line); - } - { - Line line = { L("Scale Y"), L("Scale applied to the Y axis before slicing") }; - line.append_option(optgroup->get_option("belt_scale_y")); - line.append_option(optgroup->get_option("belt_scale_y_angle")); - optgroup->append_line(line); - } - { - Line line = { L("Scale Z"), L("Scale applied to the Z axis before slicing") }; - line.append_option(optgroup->get_option("belt_scale_z")); - line.append_option(optgroup->get_option("belt_scale_z_angle")); - optgroup->append_line(line); - } - { - Line line = { L("G-code axis remap"), L("Remap slicing-frame axes to machine axes in G-code output") }; - line.append_option(optgroup->get_option("belt_gcode_remap_x")); - line.append_option(optgroup->get_option("belt_gcode_remap_y")); - line.append_option(optgroup->get_option("belt_gcode_remap_z")); - optgroup->append_line(line); - } optgroup->append_single_option_line("support_multi_bed_types","printer_basic_information_printable_space#support-multi-bed-types"); optgroup->append_single_option_line("best_object_pos", "printer_basic_information_printable_space#best-object-position"); // todo: for multi_extruder test @@ -4435,6 +4383,68 @@ void TabPrinter::build_fff() //option.opt.full_width = true; //optgroup->append_single_option_line(option); optgroup->append_single_option_line("disable_m73", "printer_basic_information_advanced#disable-set-remaining-print-time"); + optgroup->append_single_option_line("build_plate_tilt_x"); + optgroup->append_single_option_line("build_plate_tilt_y"); + optgroup->append_single_option_line("belt_printer"); + optgroup->append_single_option_line("belt_printer_angle"); + optgroup->append_single_option_line("belt_printer_infinite_y"); + // Per-axis shear: group mode + angle + source on one row per axis + { + Line line = { L("Mesh shear X"), L("Shear applied to the X axis before slicing") }; + line.append_option(optgroup->get_option("belt_shear_x")); + line.append_option(optgroup->get_option("belt_shear_x_angle")); + line.append_option(optgroup->get_option("belt_shear_x_from")); + line.append_option(optgroup->get_option("belt_shear_x_global")); + optgroup->append_line(line); + } + { + Line line = { L("Mesh shear Y"), L("Shear applied to the Y axis before slicing") }; + line.append_option(optgroup->get_option("belt_shear_y")); + line.append_option(optgroup->get_option("belt_shear_y_angle")); + line.append_option(optgroup->get_option("belt_shear_y_from")); + line.append_option(optgroup->get_option("belt_shear_y_global")); + optgroup->append_line(line); + } + { + Line line = { L("Mesh shear Z"), L("Shear applied to the Z axis before slicing") }; + line.append_option(optgroup->get_option("belt_shear_z")); + line.append_option(optgroup->get_option("belt_shear_z_angle")); + line.append_option(optgroup->get_option("belt_shear_z_from")); + line.append_option(optgroup->get_option("belt_shear_z_global")); + optgroup->append_line(line); + } + { + Line line = { L("Mesh scale X"), L("Scale applied to the X axis before slicing") }; + line.append_option(optgroup->get_option("belt_scale_x")); + line.append_option(optgroup->get_option("belt_scale_x_angle")); + optgroup->append_line(line); + } + { + Line line = { L("Mesh scale Y"), L("Scale applied to the Y axis before slicing") }; + line.append_option(optgroup->get_option("belt_scale_y")); + line.append_option(optgroup->get_option("belt_scale_y_angle")); + optgroup->append_line(line); + } + { + Line line = { L("Mesh scale Z"), L("Scale applied to the Z axis before slicing") }; + line.append_option(optgroup->get_option("belt_scale_z")); + line.append_option(optgroup->get_option("belt_scale_z_angle")); + optgroup->append_line(line); + } + { + Line line = { L("G-code axis remap"), L("Remap slicing-frame axes to machine axes in G-code output") }; + line.append_option(optgroup->get_option("belt_gcode_remap_x")); + line.append_option(optgroup->get_option("belt_gcode_remap_y")); + line.append_option(optgroup->get_option("belt_gcode_remap_z")); + optgroup->append_line(line); + } + { + Line line = { L("Support floor"), L("Belt floor awareness for support generation and clipping") }; + line.append_option(optgroup->get_option("belt_support_floor_mode")); + line.append_option(optgroup->get_option("belt_support_floor_offset")); + line.append_option(optgroup->get_option("belt_support_z_offset_mode")); + optgroup->append_line(line); + } option = optgroup->get_option("thumbnails"); option.opt.full_width = true; optgroup->append_single_option_line(option, "printer_basic_information_advanced#g-code-thumbnails"); @@ -5284,6 +5294,33 @@ void TabPrinter::toggle_options() "belt_scale_x", "belt_scale_y", "belt_scale_z", "belt_gcode_remap_x"}) toggle_line(el, is_belt); + + // Gray out angle/from sub-options when their parent shear/scale mode is None. + auto sx = m_config->option>("belt_shear_x")->value; + toggle_option("belt_shear_x_angle", is_belt && sx != BeltShearMode::None); + toggle_option("belt_shear_x_from", is_belt && sx != BeltShearMode::None); + toggle_option("belt_shear_x_global", is_belt && sx != BeltShearMode::None); + + auto sy = m_config->option>("belt_shear_y")->value; + toggle_option("belt_shear_y_angle", is_belt && sy != BeltShearMode::None); + toggle_option("belt_shear_y_from", is_belt && sy != BeltShearMode::None); + toggle_option("belt_shear_y_global", is_belt && sy != BeltShearMode::None); + + auto sz = m_config->option>("belt_shear_z")->value; + toggle_option("belt_shear_z_angle", is_belt && sz != BeltShearMode::None); + toggle_option("belt_shear_z_from", is_belt && sz != BeltShearMode::None); + toggle_option("belt_shear_z_global", is_belt && sz != BeltShearMode::None); + + auto scx = m_config->option>("belt_scale_x")->value; + toggle_option("belt_scale_x_angle", is_belt && scx != BeltScaleMode::None); + + auto scy = m_config->option>("belt_scale_y")->value; + toggle_option("belt_scale_y_angle", is_belt && scy != BeltScaleMode::None); + + auto scz = m_config->option>("belt_scale_z")->value; + toggle_option("belt_scale_z_angle", is_belt && scz != BeltScaleMode::None); + + toggle_line("belt_support_floor_mode", is_belt); } From 9bbac19de424b903666f415f07a46ccc5f7646a7 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 25 Mar 2026 22:19:31 -0500 Subject: [PATCH 017/224] Part 2.7: Add G-code back-transform and tree support belt floor clipping - Add BeltBackTransform class that inverts the shear/scale matrix and applies it in GCodeWriter::to_machine_coords() so G-code outputs in the machine's physical coordinate space, gated by new belt_gcode_back_transform config option - Extend belt floor clipping to all three tree support pipelines (Prusa-style, Orca organic, TreeModelVolumes) with per-layer polygon clipping, anti-overhang integration, and belt raft extension layers - Fix tree drop_nodes() belt termination, organic support global Z offset, collision calculation index bug, and first-layer brim/empty layer checks for belt printers two-shot - first build built but didn't plumb to UI. Woah. add pre-slice axis remap, because Y needs to be Z going to change tactic and move based on bbox min switch to per axis snapping per axis swap snap now per object build plate tilt wasn't invalidating slicer settings support upper bound now correct, need to get lower bound corrected axis swapped support termination corrected Z Shear works with and without pre-slice remap now --- src/libslic3r/CMakeLists.txt | 2 + src/libslic3r/GCode.cpp | 142 +++++++++++++++++++ src/libslic3r/GCode.hpp | 5 + src/libslic3r/GCode/BeltBackTransform.cpp | 150 +++++++++++++++++++++ src/libslic3r/GCode/BeltBackTransform.hpp | 45 +++++++ src/libslic3r/GCode/GCodeProcessor.cpp | 22 +++ src/libslic3r/GCode/GCodeProcessor.hpp | 6 + src/libslic3r/GCodeWriter.cpp | 32 ++++- src/libslic3r/GCodeWriter.hpp | 9 ++ src/libslic3r/Preset.cpp | 6 +- src/libslic3r/Print.cpp | 11 +- src/libslic3r/Print.hpp | 31 +++++ src/libslic3r/PrintConfig.cpp | 60 ++++++++- src/libslic3r/PrintConfig.hpp | 10 ++ src/libslic3r/PrintObject.cpp | 19 ++- src/libslic3r/PrintObjectSlice.cpp | 117 +++++++++++++--- src/libslic3r/Support/TreeModelVolumes.cpp | 2 +- src/libslic3r/Support/TreeSupport.cpp | 93 +++++++++++++ src/libslic3r/Support/TreeSupport3D.cpp | 2 +- src/slic3r/GUI/Tab.cpp | 39 +++++- 20 files changed, 767 insertions(+), 36 deletions(-) create mode 100644 src/libslic3r/GCode/BeltBackTransform.cpp create mode 100644 src/libslic3r/GCode/BeltBackTransform.hpp diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 5f9591452f..c2d92ee800 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -208,6 +208,8 @@ set(lisbslic3r_sources GCode/AdaptivePAProcessor.hpp GCode/AvoidCrossingPerimeters.cpp GCode/AvoidCrossingPerimeters.hpp + GCode/BeltBackTransform.cpp + GCode/BeltBackTransform.hpp GCode/ConflictChecker.cpp GCode/ConflictChecker.hpp GCode/CoolingBuffer.cpp diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 5b1330b202..ac04c6dd5b 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2432,6 +2432,16 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Build volume extents for Rev remap mode. BoundingBoxf bbox_bed(print.config().printable_area.values); m_writer.set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(), print.config().printable_height.value)); + // Initialize the back-transform that undoes slicing shear/scale. + m_writer.set_belt_back_transform(print.config()); + // Per-axis origin snap: store config; actual snap is computed + // per-instance in update_origin_snap() called from set_origin(). + m_origin_snap[0] = print.config().belt_origin_snap_x.value; + m_origin_snap[1] = print.config().belt_origin_snap_y.value; + m_origin_snap[2] = print.config().belt_origin_snap_z.value; + m_origin_snap_offset[0] = print.config().belt_origin_offset_x.value; + m_origin_snap_offset[1] = print.config().belt_origin_offset_y.value; + m_origin_snap_offset[2] = print.config().belt_origin_offset_z.value; } // How many times will be change_layer() called? @@ -2544,6 +2554,9 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.write_format("; belt_scale_y_angle = %.1f\n", print.config().belt_scale_y_angle.value); file.write_format("; belt_scale_z = %s\n", full_cfg.opt_serialize("belt_scale_z").c_str()); file.write_format("; belt_scale_z_angle = %.1f\n", print.config().belt_scale_z_angle.value); + file.write_format("; belt_preslice_remap_x = %s\n", full_cfg.opt_serialize("belt_preslice_remap_x").c_str()); + file.write_format("; belt_preslice_remap_y = %s\n", full_cfg.opt_serialize("belt_preslice_remap_y").c_str()); + file.write_format("; belt_preslice_remap_z = %s\n", full_cfg.opt_serialize("belt_preslice_remap_z").c_str()); } if (is_bbl_printers) file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str()); @@ -3248,6 +3261,8 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } print.throw_if_canceled(); this->set_origin(unscale((*print_object_instance_sequential_active)->shift)); + this->update_origin_snap((*print_object_instance_sequential_active)->print_object, + (*print_object_instance_sequential_active)->shift); // BBS: prime extruder if extruder change happens before this object instance bool prime_extruder = false; @@ -5263,6 +5278,7 @@ LayerResult GCode::process_layer( m_avoid_crossing_perimeters.use_external_mp_once(); m_last_obj_copy = this_object_copy; this->set_origin(unscale(offset)); + this->update_origin_snap(&instance_to_print.print_object, offset); if (instance_to_print.object_by_extruder.support != nullptr) { m_layer = layers[instance_to_print.layer_id].support_layer; m_object_layer_over_raft = false; @@ -5286,6 +5302,7 @@ LayerResult GCode::process_layer( m_avoid_crossing_perimeters.use_external_mp_once(); m_last_obj_copy = this_object_copy; this->set_origin(unscale(offset)); + this->update_origin_snap(&instance_to_print.print_object, offset); ExtrusionEntityCollection support_eec; // BBS @@ -5334,6 +5351,7 @@ LayerResult GCode::process_layer( m_avoid_crossing_perimeters.use_external_mp_once(); m_last_obj_copy = this_object_copy; this->set_origin(unscale(offset)); + this->update_origin_snap(&instance_to_print.print_object, offset); //FIXME the following code prints regions in the order they are defined, the path is not optimized in any way. auto has_infill = [](const std::vector &by_region) { @@ -5560,6 +5578,130 @@ void GCode::set_origin(const Vec2d &pointf) m_origin = pointf; } +void GCode::update_origin_snap(const PrintObject *obj, const Point &inst_shift) +{ + if (!m_origin_snap[0] && !m_origin_snap[1] && !m_origin_snap[2]) + return; + + // Clear existing snap so to_machine_coords gives raw machine coords for bbox computation. + for (int a = 0; a < 3; ++a) + m_writer.set_origin_snap(a, false, 0., 0.); + + // Reconstruct the belt pipeline transform for this object (same as + // PrintObjectSlice.cpp: z_shift * scale * shear * pre_remap). + const auto &cfg = m_config; + Transform3d belt = Transform3d::Identity(); + + // Pre-slice remap + int pre_rx = int(cfg.belt_preslice_remap_x.value); + int pre_ry = int(cfg.belt_preslice_remap_y.value); + int pre_rz = int(cfg.belt_preslice_remap_z.value); + if (pre_rx != int(BeltRemapAxis::PosX) || + pre_ry != int(BeltRemapAxis::PosY) || + pre_rz != int(BeltRemapAxis::PosZ)) { + auto remap_col = [](int r) -> Vec3d { + int a = r % 3; Vec3d c = Vec3d::Zero(); + c[a] = (r < 3) ? 1.0 : -1.0; + return c; + }; + Matrix3d lin; + lin.col(0) = remap_col(pre_rx); + lin.col(1) = remap_col(pre_ry); + lin.col(2) = remap_col(pre_rz); + Transform3d pre = Transform3d::Identity(); + pre.linear() = lin; + if (pre_rx >= 6 || pre_ry >= 6 || pre_rz >= 6) { + BoundingBoxf bb(cfg.printable_area.values); + Vec3d vm(bb.max.x(), bb.max.y(), cfg.printable_height.value); + Vec3d tr = Vec3d::Zero(); + if (pre_rx >= 6) tr[0] = vm[pre_rx % 3]; + if (pre_ry >= 6) tr[1] = vm[pre_ry % 3]; + if (pre_rz >= 6) tr[2] = vm[pre_rz % 3]; + pre.translation() = tr; + } + belt = pre * belt; + } + + // Shear + auto shear_f = [](BeltShearMode m, double a) -> double { + double r = Geometry::deg2rad(a), s = std::sin(r), c = std::cos(r); + switch (m) { + case BeltShearMode::PosCot: return (s > EPSILON) ? c/s : 0.; + case BeltShearMode::NegCot: return (s > EPSILON) ? -c/s : 0.; + case BeltShearMode::PosTan: return (c > EPSILON) ? s/c : 0.; + case BeltShearMode::NegTan: return (c > EPSILON) ? -s/c : 0.; + default: return 0.; + } + }; + struct AS { BeltShearMode m; double a; int f; }; + AS axes[3] = { + {cfg.belt_shear_x.value, cfg.belt_shear_x_angle.value, int(cfg.belt_shear_x_from.value)}, + {cfg.belt_shear_y.value, cfg.belt_shear_y_angle.value, int(cfg.belt_shear_y_from.value)}, + {cfg.belt_shear_z.value, cfg.belt_shear_z_angle.value, int(cfg.belt_shear_z_from.value)}, + }; + Transform3d shear = Transform3d::Identity(); + for (int i = 0; i < 3; ++i) + if (axes[i].m != BeltShearMode::None) { + double f = shear_f(axes[i].m, axes[i].a); + if (std::abs(f) > EPSILON) + shear.matrix()(i, axes[i].f) += f; + } + + // Scale + auto scale_f = [](BeltScaleMode m, double a) -> double { + if (m == BeltScaleMode::None) return 1.; + double r = Geometry::deg2rad(a), s = std::sin(r), c = std::cos(r); + switch (m) { + case BeltScaleMode::InvSin: return (s > EPSILON) ? 1./s : 1.; + case BeltScaleMode::InvCos: return (c > EPSILON) ? 1./c : 1.; + case BeltScaleMode::Sin: return s; + case BeltScaleMode::Cos: return c; + default: return 1.; + } + }; + Transform3d sc = Transform3d::Identity(); + sc.matrix()(0,0) = scale_f(cfg.belt_scale_x.value, cfg.belt_scale_x_angle.value); + sc.matrix()(1,1) = scale_f(cfg.belt_scale_y.value, cfg.belt_scale_y_angle.value); + sc.matrix()(2,2) = scale_f(cfg.belt_scale_z.value, cfg.belt_scale_z_angle.value); + + belt = sc * shear * belt; + + // Z-shift + double zs = (obj->belt_min_z() < 0.) ? -obj->belt_min_z() : 0.; + if (zs > 0.) { + Transform3d zsh = Transform3d::Identity(); + zsh.matrix()(2, 3) = zs; + belt = zsh * belt; + } + + // Full transform: belt * trafo_centered + Transform3d full = belt * obj->trafo_centered(); + + // Instance shift in slicer space + global Z offset + Vec3d shift(unscale(inst_shift.x()), + unscale(inst_shift.y()), + obj->belt_global_z_offset()); + + // Compute this instance's machine-space bbox min + BoundingBoxf3 bb = obj->model_object()->raw_bounding_box(); + Vec3d mn = bb.min.cast(), mx = bb.max.cast(); + Vec3d inst_min(std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max()); + 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 mc = m_writer.to_machine_coords(full * c + shift); + for (int a = 0; a < 3; ++a) + inst_min[a] = std::min(inst_min[a], mc[a]); + } + + // Update writer snap for each enabled axis + for (int a = 0; a < 3; ++a) + m_writer.set_origin_snap(a, m_origin_snap[a], m_origin_snap_offset[a], inst_min[a]); +} + std::string GCode::preamble() { std::string gcode = m_writer.preamble(); diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index d379300bad..dbec7d0315 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -493,6 +493,11 @@ private: This affects the input arguments supplied to the extrude*() and travel_to() methods. */ Vec2d m_origin; + // Per-axis origin snap: shift G-code so each object's bbox min = offset. + bool m_origin_snap[3] = {false, false, false}; + double m_origin_snap_offset[3] = {0., 0., 0.}; + // Called when switching instances to recompute the writer's snap for this instance. + void update_origin_snap(const PrintObject *obj, const Point &inst_shift); FullPrintConfig m_config; DynamicConfig m_calib_config; // scaled G-code resolution diff --git a/src/libslic3r/GCode/BeltBackTransform.cpp b/src/libslic3r/GCode/BeltBackTransform.cpp new file mode 100644 index 0000000000..7cd5bc6b53 --- /dev/null +++ b/src/libslic3r/GCode/BeltBackTransform.cpp @@ -0,0 +1,150 @@ +#include "BeltBackTransform.hpp" +#include "../Geometry.hpp" +#include + +namespace Slic3r { + +// Keep in sync with PrintObjectSlice.cpp compute_shear_factor (lines ~147-157). +static double compute_shear_factor(BeltShearMode mode, double angle_deg) +{ + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad); + double cos_a = std::cos(angle_rad); + switch (mode) { + case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; + case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; + case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; + case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; + default: return 0.; + } +} + +// Keep in sync with PrintObjectSlice.cpp compute_scale_factor (lines ~180-192). +static double compute_scale_factor(BeltScaleMode mode, double angle_deg) +{ + if (mode == BeltScaleMode::None) return 1.; + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad); + double cos_a = std::cos(angle_rad); + switch (mode) { + case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; + case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; + case BeltScaleMode::Sin: return sin_a; + case BeltScaleMode::Cos: return cos_a; + default: return 1.; + } +} + +bool BeltBackTransform::init_from_config(const PrintConfig &config) +{ + m_active = false; + m_inverse = Transform3d::Identity(); + + if (!config.belt_printer.value || !config.belt_gcode_back_transform.value) + return false; + + // --- Pre-slice axis remap (same as PrintObjectSlice.cpp) --- + int pre_rx = int(config.belt_preslice_remap_x.value); + int pre_ry = int(config.belt_preslice_remap_y.value); + int pre_rz = int(config.belt_preslice_remap_z.value); + + bool has_preslice_remap = (pre_rx != int(BeltRemapAxis::PosX) || + pre_ry != int(BeltRemapAxis::PosY) || + pre_rz != int(BeltRemapAxis::PosZ)); + + // Require at least one active transform to proceed. + bool has_global_shear = config.belt_shear_x_global.value || + config.belt_shear_y_global.value || + config.belt_shear_z_global.value; + if (!has_global_shear && !has_preslice_remap) + return false; + + // Build pre-slice remap matrix. + Transform3d pre_remap = Transform3d::Identity(); + if (has_preslice_remap) { + auto remap_column = [](int r) -> Vec3d { + int axis = r % 3; + Vec3d col = Vec3d::Zero(); + if (r < 3) col[axis] = 1.0; + else if (r < 6) col[axis] = -1.0; + else col[axis] = -1.0; // Rev: max - pos + 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; + + // Rev mode translation (needs build volume extents). + Vec3d remap_trans = Vec3d::Zero(); + 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); + 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; + } + + // Build per-axis shear matrix (same as PrintObjectSlice.cpp). + struct AxisShear { BeltShearMode mode; double angle; int from; }; + AxisShear axes[3] = { + { config.belt_shear_x.value, config.belt_shear_x_angle.value, int(config.belt_shear_x_from.value) }, + { config.belt_shear_y.value, config.belt_shear_y_angle.value, int(config.belt_shear_y_from.value) }, + { config.belt_shear_z.value, config.belt_shear_z_angle.value, int(config.belt_shear_z_from.value) }, + }; + + Matrix3d shear = Matrix3d::Identity(); + bool has_shear = false; + for (int row = 0; row < 3; ++row) { + if (axes[row].mode != BeltShearMode::None) { + double factor = compute_shear_factor(axes[row].mode, axes[row].angle); + if (std::abs(factor) > EPSILON) { + shear(row, axes[row].from) += factor; + has_shear = true; + } + } + } + + // Build per-axis scale diagonal matrix (same as PrintObjectSlice.cpp). + double sx = compute_scale_factor(config.belt_scale_x.value, config.belt_scale_x_angle.value); + double sy = compute_scale_factor(config.belt_scale_y.value, config.belt_scale_y_angle.value); + double sz = compute_scale_factor(config.belt_scale_z.value, config.belt_scale_z_angle.value); + + Matrix3d scale = Matrix3d::Identity(); + bool has_scale = (std::abs(sx - 1.) > EPSILON || + std::abs(sy - 1.) > EPSILON || + std::abs(sz - 1.) > EPSILON); + if (has_scale) { + scale(0, 0) = sx; + scale(1, 1) = sy; + scale(2, 2) = sz; + } + + if (!has_shear && !has_scale && !has_preslice_remap) + return false; + + // Forward pipeline: scale * shear * pre_remap (same order as PrintObjectSlice.cpp). + Transform3d combined = Transform3d::Identity(); + combined.linear() = scale * shear; + combined = combined * pre_remap; + m_inverse = combined.inverse(); + m_active = true; + return true; +} + +Vec3d BeltBackTransform::apply(const Vec3d &pos) const +{ + if (!m_active) + return pos; + return m_inverse * pos; +} + +} // namespace Slic3r diff --git a/src/libslic3r/GCode/BeltBackTransform.hpp b/src/libslic3r/GCode/BeltBackTransform.hpp new file mode 100644 index 0000000000..5a75d94dc8 --- /dev/null +++ b/src/libslic3r/GCode/BeltBackTransform.hpp @@ -0,0 +1,45 @@ +#ifndef slic3r_BeltBackTransform_hpp_ +#define slic3r_BeltBackTransform_hpp_ + +#include "../libslic3r.h" +#include "../Point.hpp" +#include "../PrintConfig.hpp" + +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. +// +// Initialized once from PrintConfig, then applied per-point in +// GCodeWriter::to_machine_coords() before axis remapping. +// +// Active when belt_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. +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. + bool init_from_config(const PrintConfig &config); + + // Apply the inverse transform to a point. Returns pos unchanged if + // no back-transform is active. + Vec3d apply(const Vec3d &pos) const; + + // True if a non-identity back-transform is active. + bool is_active() const { return m_active; } + +private: + bool m_active = false; + Transform3d m_inverse = Transform3d::Identity(); +}; + +} // namespace Slic3r + +#endif // slic3r_BeltBackTransform_hpp_ diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 8a470eff19..2288c9e670 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -3128,6 +3128,28 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers if (boost::starts_with(comment, " belt_scale_z_angle = ")) { try { m_result.belt_scale_z_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; } + // Pre-slice axis remap + auto parse_remap_axis = [](const std::string &s) -> BeltRemapAxis { + if (s == "pos_x") return BeltRemapAxis::PosX; + if (s == "pos_y") return BeltRemapAxis::PosY; + if (s == "pos_z") return BeltRemapAxis::PosZ; + if (s == "neg_x") return BeltRemapAxis::NegX; + if (s == "neg_y") return BeltRemapAxis::NegY; + if (s == "neg_z") return BeltRemapAxis::NegZ; + if (s == "rev_x") return BeltRemapAxis::RevX; + if (s == "rev_y") return BeltRemapAxis::RevY; + if (s == "rev_z") return BeltRemapAxis::RevZ; + return BeltRemapAxis::PosX; + }; + if (boost::starts_with(comment, " belt_preslice_remap_x = ")) { + m_result.belt_preslice_remap_x = parse_remap_axis(trim(std::string(comment.substr(25)))); return; + } + if (boost::starts_with(comment, " belt_preslice_remap_y = ")) { + m_result.belt_preslice_remap_y = parse_remap_axis(trim(std::string(comment.substr(25)))); return; + } + if (boost::starts_with(comment, " belt_preslice_remap_z = ")) { + m_result.belt_preslice_remap_z = parse_remap_axis(trim(std::string(comment.substr(25)))); return; + } } // wipe start tag if (boost::starts_with(comment, reserved_tag(ETags::Wipe_Start))) { diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index f953345f4e..7d08a9cdfd 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -246,6 +246,9 @@ class Print; float belt_scale_y_angle{ 45.f }; BeltScaleMode belt_scale_z{ BeltScaleMode::None }; float belt_scale_z_angle{ 45.f }; + BeltRemapAxis belt_preslice_remap_x{ BeltRemapAxis::PosX }; + BeltRemapAxis belt_preslice_remap_y{ BeltRemapAxis::PosY }; + BeltRemapAxis belt_preslice_remap_z{ BeltRemapAxis::PosZ }; SettingsIds settings_ids; size_t filaments_count; bool backtrace_enabled; @@ -321,6 +324,9 @@ class Print; belt_scale_y_angle = other.belt_scale_y_angle; belt_scale_z = other.belt_scale_z; belt_scale_z_angle = other.belt_scale_z_angle; + belt_preslice_remap_x = other.belt_preslice_remap_x; + belt_preslice_remap_y = other.belt_preslice_remap_y; + belt_preslice_remap_z = other.belt_preslice_remap_z; #if ENABLE_GCODE_VIEWER_STATISTICS time = other.time; #endif diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index da655a6548..b5a6402a14 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -37,18 +37,40 @@ void GCodeWriter::set_build_volume_max(const Vec3d &max) m_build_vol_max = max; } +void GCodeWriter::set_belt_back_transform(const PrintConfig &config) +{ + m_belt_back_transform.init_from_config(config); +} + +void GCodeWriter::set_origin_snap(int axis, bool enable, double offset, double bbox_min) +{ + if (axis >= 0 && axis < 3) { + m_origin_snap[axis] = enable; + m_origin_offset[axis] = offset; + m_origin_bbox_min[axis] = bbox_min; + } +} + Vec3d GCodeWriter::to_machine_coords(const Vec3d &pos) const { if (!is_belt_printer()) return pos; + // Step 1: Undo the shear/scale applied during slicing. + Vec3d p = m_belt_back_transform.apply(pos); + // Step 2: Apply axis remapping for the machine's coordinate convention. // BeltRemapAxis: 0-2 = +X/+Y/+Z, 3-5 = -X/-Y/-Z, 6-8 = Rev X/Y/Z - auto remap = [this, &pos](int r) -> double { + auto remap = [this, &p](int r) -> double { int axis = r % 3; - if (r < 3) return pos[axis]; - if (r < 6) return -pos[axis]; - return m_build_vol_max[axis] - pos[axis]; + if (r < 3) return p[axis]; + if (r < 6) return -p[axis]; + return m_build_vol_max[axis] - p[axis]; }; - return { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) }; + Vec3d result = { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) }; + // Per-axis origin snap: shift so bbox min on each enabled axis = offset. + for (int i = 0; i < 3; ++i) + if (m_origin_snap[i]) + result[i] -= (m_origin_bbox_min[i] - m_origin_offset[i]); + return result; } bool GCodeWriter::supports_separate_travel_acceleration(GCodeFlavor flavor) diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index 6074eb247c..2334f5d416 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -8,6 +8,7 @@ #include "Point.hpp" #include "PrintConfig.hpp" #include "GCode/CoolingBuffer.hpp" +#include "GCode/BeltBackTransform.hpp" namespace Slic3r { @@ -132,6 +133,10 @@ public: void set_axis_remap(int rx, int ry, int rz); // Set build volume extents for Rev remap mode (max X, Y, Z). void set_build_volume_max(const Vec3d &max); + // Initialize the belt back-transform that undoes slicing shear/scale. + void set_belt_back_transform(const PrintConfig &config); + // Set per-axis origin snap: shifts G-code so bbox min on this axis = offset. + void set_origin_snap(int axis, bool enable, double offset, double bbox_min); // Transform a point from the slicing frame to machine coordinates. Vec3d to_machine_coords(const Vec3d &pos) const; @@ -194,6 +199,10 @@ public: int m_remap_y = 1; int m_remap_z = 2; Vec3d m_build_vol_max = Vec3d::Zero(); + BeltBackTransform m_belt_back_transform; + bool m_origin_snap[3] = {false, false, false}; + double m_origin_offset[3] = {0., 0., 0.}; // target coord for bbox min + double m_origin_bbox_min[3] = {0., 0., 0.}; // computed bbox min in machine space double m_current_speed; bool m_is_first_layer = true; diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 3c597ac7f2..737ee2d663 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1014,7 +1014,11 @@ static std::vector s_Preset_printer_options { "belt_shear_y", "belt_shear_y_angle", "belt_shear_y_from", "belt_shear_y_global", "belt_shear_z", "belt_shear_z_angle", "belt_shear_z_from", "belt_shear_z_global", "belt_scale_x", "belt_scale_x_angle", "belt_scale_y", "belt_scale_y_angle", "belt_scale_z", "belt_scale_z_angle", - "belt_gcode_remap_x", "belt_gcode_remap_y", "belt_gcode_remap_z", + "belt_preslice_remap_x", "belt_preslice_remap_y", "belt_preslice_remap_z", + "belt_gcode_remap_x", "belt_gcode_remap_y", "belt_gcode_remap_z", "belt_gcode_back_transform", + "belt_origin_snap_x", "belt_origin_offset_x", + "belt_origin_snap_y", "belt_origin_offset_y", + "belt_origin_snap_z", "belt_origin_offset_z", "belt_support_floor_offset", "belt_support_floor_mode", "belt_support_z_offset_mode", "gcode_flavor", "fan_kickstart", "fan_speedup_time", "fan_speedup_overhangs", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 0aef12a74b..0b7190e8d1 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -103,6 +103,9 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n "belt_gcode_remap_x", "belt_gcode_remap_y", "belt_gcode_remap_z", + "belt_origin_snap_x", "belt_origin_offset_x", + "belt_origin_snap_y", "belt_origin_offset_y", + "belt_origin_snap_z", "belt_origin_offset_z", //BBS "additional_cooling_fan_speed", "reduce_crossing_wall", @@ -280,6 +283,9 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n // In Spiral Vase mode, holes are closed and only the largest area contour is kept at each layer. // Therefore toggling the Spiral Vase on / off requires complete reslicing. || opt_key == "spiral_mode" + // Build plate tilt changes slicing plane orientation. + || opt_key == "build_plate_tilt_x" + || opt_key == "build_plate_tilt_y" // Belt printer transform options change the mesh geometry before slicing. || opt_key == "belt_printer" || opt_key == "belt_printer_angle" @@ -300,7 +306,10 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "belt_scale_y" || opt_key == "belt_scale_y_angle" || opt_key == "belt_scale_z" - || opt_key == "belt_scale_z_angle") { + || opt_key == "belt_scale_z_angle" + || opt_key == "belt_preslice_remap_x" + || opt_key == "belt_preslice_remap_y" + || opt_key == "belt_preslice_remap_z") { osteps.emplace_back(posSlice); } else if ( opt_key == "belt_support_floor_offset" diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index d0c050f442..c13a1407c0 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -186,6 +186,36 @@ class ConstSupportLayerPtrsAdaptor : public ConstVectorOfPtrsAdaptor(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) +{ + BoundingBoxf3 bb = model_object.raw_bounding_box(); + int pre_rx = int(config.belt_preslice_remap_x.value); + int pre_ry = int(config.belt_preslice_remap_y.value); + int pre_rz = int(config.belt_preslice_remap_z.value); + if (pre_rx == int(BeltRemapAxis::PosX) && + pre_ry == int(BeltRemapAxis::PosY) && + pre_rz == int(BeltRemapAxis::PosZ)) + return bb; // Identity remap, no change. + 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(), mx = bb.max.cast(); + 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; +} + // Single instance of a PrintObject. // As multiple PrintObjects may be generated for a single ModelObject (their instances differ in rotation around Z), // ModelObject's instancess will be distributed among these multiple PrintObjects. @@ -579,6 +609,7 @@ private: double m_belt_min_z { 0.0 }; public: double belt_global_z_offset() const { return m_belt_global_z_offset; } + double belt_min_z() const { return m_belt_min_z; } private: diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 96e320ecf4..8c637b7f7c 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -6136,9 +6136,63 @@ void PrintConfigDef::init_fff_params() def->set_default_value(new ConfigOptionEnum(default_axis)); }; - add_belt_remap("belt_gcode_remap_x", "X", "Which slicing axis maps to machine X in G-code output.", BeltRemapAxis::PosX); - add_belt_remap("belt_gcode_remap_y", "Y", "Which slicing axis maps to machine Y in G-code output.", BeltRemapAxis::PosY); - add_belt_remap("belt_gcode_remap_z", "Z", "Which slicing axis maps to machine Z in G-code output.", BeltRemapAxis::PosZ); + add_belt_remap("belt_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.", + BeltRemapAxis::PosX); + add_belt_remap("belt_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.", + BeltRemapAxis::PosY); + add_belt_remap("belt_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.", + BeltRemapAxis::PosZ); + + add_belt_remap("belt_gcode_remap_x", "X", "Which slicing axis maps to machine X in G-code output. Applied AFTER slicing, during G-code generation.", BeltRemapAxis::PosX); + add_belt_remap("belt_gcode_remap_y", "Y", "Which slicing axis maps to machine Y in G-code output. Applied AFTER slicing, during G-code generation.", BeltRemapAxis::PosY); + add_belt_remap("belt_gcode_remap_z", "Z", "Which slicing axis maps to machine Z in G-code output. Applied AFTER slicing, during G-code generation.", BeltRemapAxis::PosZ); + + def = this->add("belt_gcode_back_transform", coBool); + def->label = L("G-code back-transform"); + def->category = L("Printable space"); + def->tooltip = L("Reverse the shear/scale transform applied during slicing so G-code " + "coordinates are in the machine's physical coordinate space. " + "Requires at least one shear axis with global mode enabled."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + + auto add_belt_origin_snap = [this](const char *key_snap, const char *key_offset, + const char *axis_label) { + auto def = this->add(key_snap, coBool); + def->label = L(axis_label); + def->category = L("Printable space"); + std::string tip = std::string("Shift G-code output so the object's bounding box minimum on machine ") + + axis_label + " equals the offset value."; + def->tooltip = L(tip); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + + def = this->add(key_offset, coFloat); + def->label = L("Offset"); + def->category = L("Printable space"); + def->tooltip = L("Target coordinate for the bounding box minimum on this machine axis."); + def->sidetext = L("mm"); + def->min = -10000; + def->max = 10000; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0)); + }; + add_belt_origin_snap("belt_origin_snap_x", "belt_origin_offset_x", "X"); + add_belt_origin_snap("belt_origin_snap_y", "belt_origin_offset_y", "Y"); + add_belt_origin_snap("belt_origin_snap_z", "belt_origin_offset_z", "Z"); // Belt support floor debug controls def = this->add("belt_support_floor_offset", coFloat); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 2d76dcc0b5..13202657ef 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1487,9 +1487,19 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionFloat, belt_scale_y_angle)) ((ConfigOptionEnum, belt_scale_z)) ((ConfigOptionFloat, belt_scale_z_angle)) + ((ConfigOptionEnum, belt_preslice_remap_x)) + ((ConfigOptionEnum, belt_preslice_remap_y)) + ((ConfigOptionEnum, belt_preslice_remap_z)) ((ConfigOptionEnum, belt_gcode_remap_x)) ((ConfigOptionEnum, belt_gcode_remap_y)) ((ConfigOptionEnum, belt_gcode_remap_z)) + ((ConfigOptionBool, belt_gcode_back_transform)) + ((ConfigOptionBool, belt_origin_snap_x)) + ((ConfigOptionFloat, belt_origin_offset_x)) + ((ConfigOptionBool, belt_origin_snap_y)) + ((ConfigOptionFloat, belt_origin_offset_y)) + ((ConfigOptionBool, belt_origin_snap_z)) + ((ConfigOptionFloat, belt_origin_offset_z)) ((ConfigOptionFloat, belt_support_floor_offset)) ((ConfigOptionEnum, belt_support_floor_mode)) ((ConfigOptionEnum, belt_support_z_offset_mode)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index f803cc0369..ba2ed408ce 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -3402,9 +3402,16 @@ void PrintObject::update_slicing_parameters() double belt_floor_shear_factor_out = 0.0; int belt_floor_from_axis_out = 1; double belt_floor_z_shift_out = 0.0; - // Belt shear/scale may change the effective Z height. + // Belt shear/scale/pre-remap may change the effective Z height. const auto &pcfg = this->print()->config(); if (pcfg.belt_printer.value) { + BoundingBoxf3 bb = belt_remapped_bbox(*this->model_object(), pcfg); + bool has_preslice_remap = (int(pcfg.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || + int(pcfg.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || + int(pcfg.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ)); + if (has_preslice_remap) + object_height = bb.size().z(); + bool has_z_shear = pcfg.belt_shear_z.value != BeltShearMode::None; bool has_z_scale = pcfg.belt_scale_z.value != BeltScaleMode::None; if (has_z_shear || has_z_scale) { @@ -3435,7 +3442,6 @@ void PrintObject::update_slicing_parameters() double scale_z = compute_scale_factor(pcfg.belt_scale_z.value, pcfg.belt_scale_z_angle.value); if (has_z_shear && std::abs(shear_factor) > EPSILON) { int from = int(pcfg.belt_shear_z_from.value); - BoundingBoxf3 bb = this->model_object()->raw_bounding_box(); double min_rz = std::numeric_limits::max(); double max_rz = std::numeric_limits::lowest(); for (double vz : {bb.min.z(), bb.max.z()}) @@ -3507,8 +3513,15 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full if (object_max_z <= 0.f) { BoundingBoxf3 bb = model_object.raw_bounding_box(); object_max_z = (float)bb.size().z(); - // Belt shear/scale may change the effective Z height. + // Belt pre-remap/shear/scale may change the effective Z height. if (print_config.belt_printer.value) { + bb = belt_remapped_bbox(model_object, print_config); + bool has_preslice_remap = (int(print_config.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || + int(print_config.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || + int(print_config.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ)); + if (has_preslice_remap) + object_max_z = (float)bb.size().z(); + bool has_z_shear = print_config.belt_shear_z.value != BeltShearMode::None; bool has_z_scale = print_config.belt_scale_z.value != BeltScaleMode::None; if (has_z_shear || has_z_scale) { diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 404550da1a..4c767ea0ed 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -143,6 +143,58 @@ static std::vector slice_volumes_inner( params_base.extra_offset = 0; params_base.trafo = object_trafo; if (print_config.belt_printer.value) { + // --- Pre-slice axis remap --- + // Permutes/negates model axes before slicing so the slicer's coordinate + // system matches the physical bed orientation (e.g. XZ bed instead of XY). + int pre_rx = int(print_config.belt_preslice_remap_x.value); + int pre_ry = int(print_config.belt_preslice_remap_y.value); + int pre_rz = int(print_config.belt_preslice_remap_z.value); + + bool has_preslice_remap = (pre_rx != int(BeltRemapAxis::PosX) || + pre_ry != int(BeltRemapAxis::PosY) || + pre_rz != int(BeltRemapAxis::PosZ)); + + if (has_preslice_remap) { + // Build volume extents for Rev mode. + BoundingBoxf bbox_bed(print_config.printable_area.values); + Vec3d vol_max(bbox_bed.max.x(), bbox_bed.max.y(), + print_config.printable_height.value); + + // Each remap value selects a source axis and sign. + // The column vector tells the matrix which input axis feeds this output. + 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); + + // Translation for Rev modes: output = max[src] - input[src]. + Vec3d remap_trans = Vec3d::Zero(); + auto add_rev_offset = [&](int r, int out_axis) { + if (r >= 6) { + int src_axis = r % 3; + remap_trans[out_axis] = vol_max[src_axis]; + } + }; + add_rev_offset(pre_rx, 0); + add_rev_offset(pre_ry, 1); + add_rev_offset(pre_rz, 2); + + Transform3d pre_remap = Transform3d::Identity(); + pre_remap.linear() = remap_lin; + pre_remap.translation() = remap_trans; + + params_base.trafo = pre_remap * params_base.trafo; + } + // Build per-axis shear matrix from 3 independent axis configs. auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { double angle_rad = Geometry::deg2rad(angle_deg); @@ -204,11 +256,12 @@ static std::vector slice_volumes_inner( } // Apply: scale * shear * trafo (shear first, then scale). - if (has_shear || has_scale) { + if (has_shear || has_scale) params_base.trafo = belt_scale * belt_shear * params_base.trafo; - // After the shear/scale transform, the mesh may clip through the - // build plate (Z < 0). Detect this and shift the mesh up. + // After pre-remap/shear/scale, the mesh may clip through the build + // plate (Z < 0). Detect this and shift the mesh up along slicer Z. + if (has_preslice_remap || has_shear || has_scale) { Transform3d combined = params_base.trafo; double min_z = std::numeric_limits::max(); for (const ModelVolume *mv : model_volumes) { @@ -893,17 +946,34 @@ void PrintObject::slice() this->slice_volumes(); m_print->throw_if_canceled(); - // After slicing, m_belt_min_z holds the exact post-shear minimum Z - // in trafo_centered space (which includes the ensure_on_bed Z offset). - // The belt surface is at Z=0 in trafo_centered space; after shear it - // becomes Z = sf*Y, and after the z-shift that keeps the mesh above - // Z=0 it becomes Z = sf*Y + z_shift_val. So belt_floor_z_shift is - // simply the z-shift applied, i.e. max(0, -m_belt_min_z). - // NOTE: do NOT add raw_bounding_box().min.z() here — m_belt_min_z - // already includes the ensure_on_bed offset, unlike the min_rz used - // in update_slicing_parameters() which needs that compensation. + // Belt floor Z-shift: where is the belt surface in final slicer space? + // + // The belt surface is at model_Y=0 (XZ belt plane). After the full + // pipeline (trafo_centered → pre_remap → shear → z_shift), the belt + // surface equation in slicer space is: + // Z_belt = sf * from_axis + belt_surface_z_centered + z_shift_val + // + // belt_surface_z_centered = remapped_bbox.min.z() (the Z position of + // the belt surface in centered-pre-shear slicer space, which is 0 + // without pre-remap but nonzero when e.g. Y↔Z swap shifts the belt + // surface away from Z=0 by the centering offset). + // + // z_shift_val = max(0, -m_belt_min_z) (lifts mesh above Z=0). + // + // So: belt_floor_z_shift = remapped_bb.min.z() + z_shift_val if (std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON) { - m_slicing_params.belt_floor_z_shift = (m_belt_min_z < 0.) ? -m_belt_min_z : 0.; + 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(); + bool has_preslice_remap = (int(pcfg.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || + int(pcfg.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || + int(pcfg.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ)); + double belt_surface_z = has_preslice_remap + ? belt_remapped_bbox(*this->model_object(), pcfg).min.z() : 0.; + m_slicing_params.belt_floor_z_shift = belt_surface_z + z_shift_val; } int firstLayerReplacedBy = 0; @@ -989,14 +1059,23 @@ void PrintObject::slice() const auto &za = gaxes[2]; // Z row if (za.global && za.mode != BeltShearMode::None && za.from < 2) { double factor = compute_shear_factor(za.mode, za.angle); - // The Z-shift brought the mesh's lowest sheared vertex to - // Z=0. That vertex's physical Y determines the belt contact - // point. With trafo_z preserved (ensure_on_bed offset), - // min_z = Y_at_contact * factor for bottom-face vertices, - // so: z_offset = center_Y * factor + min_z. + // The global Z offset accounts for the instance's position- + // dependent shear contribution. m_belt_min_z is the minimum Z + // of the mesh after pre_remap + shear + trafo_centered, which + // includes the centering offset on the remapped Z axis. + // Subtract the belt surface's centered Z position so we get + // only the shear-induced contribution (same correction as the + // belt_floor_z_shift fix). + // Same pre-remap guard as belt_floor_z_shift above. + bool has_preslice_remap2 = (int(pcfg.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || + int(pcfg.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || + int(pcfg.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ)); + double belt_surface_z = has_preslice_remap2 + ? belt_remapped_bbox(*this->model_object(), this->print()->config()).min.z() : 0.; + double shear_min_z = m_belt_min_z - belt_surface_z; Point phys = inst_shift; // already has center_offset subtracted double center_on_axis = (za.from == 0) ? unscale(phys.x()) : unscale(phys.y()); - global_z_offset += center_on_axis * factor + m_belt_min_z; + global_z_offset += center_on_axis * factor + shear_min_z; } BOOST_LOG_TRIVIAL(warning) << "Belt global: z_offset=" << global_z_offset diff --git a/src/libslic3r/Support/TreeModelVolumes.cpp b/src/libslic3r/Support/TreeModelVolumes.cpp index e270b62aab..ec38ffed73 100644 --- a/src/libslic3r/Support/TreeModelVolumes.cpp +++ b/src/libslic3r/Support/TreeModelVolumes.cpp @@ -151,7 +151,7 @@ TreeModelVolumes::TreeModelVolumes( 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(print_object.model_object()->raw_bounding_box().min.z()); + 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) { diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 4966a5a139..60e681add9 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -1733,6 +1733,99 @@ 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(). + { + const auto &sp = m_slicing_params; + const auto &pcfg = *m_print_config; + const double sf = sp.belt_floor_shear_factor; + if (std::abs(sf) > EPSILON + && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly + && m_object->support_layer_count() > 0) { + const int from_axis = sp.belt_floor_from_axis; + const double floor_off = pcfg.belt_support_floor_offset.value; + // Support layer print_z values are in GLOBAL Z (non-organic inherits + // from object layers which include global_z_offset). Use the GLOBAL + // belt_floor_z_shift to match. + const double z_shift = sp.belt_floor_z_shift; + // 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; + } + } + } + if (!source_areas.empty()) { + BoundingBoxf3 bb = belt_remapped_bbox(*m_object->model_object(), m_object->print()->config()); + double from_extent = std::abs(bb.min(from_axis)); + double bb_min_z = std::abs(bb.min.z()); + double first_z = m_object->get_support_layer(0)->print_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)); + int num_extra = std::max(0, (int)std::ceil(extra_depth / sp.layer_height)); + 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; + double cutoff = (print_z - z_shift - floor_off) / sf; + coord_t cutoff_sc = scale_(cutoff); + coord_t big = scale_(1e3); + Polygon belt_poly; + if (from_axis == 0) { + if (sf > 0) belt_poly.points = {{cutoff_sc,-big},{big,-big},{big,big},{cutoff_sc,big}}; + else belt_poly.points = {{-big,-big},{cutoff_sc,-big},{cutoff_sc,big},{-big,big}}; + } else { + if (sf > 0) belt_poly.points = {{-big,cutoff_sc},{big,cutoff_sc},{big,big},{-big,big}}; + else belt_poly.points = {{-big,-big},{big,-big},{big,cutoff_sc},{-big,cutoff_sc}}; + } + ExPolygons clipped = diff_ex(source_areas, Polygons{belt_poly}); + 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. + for (auto &expoly : sl->base_areas) + sl->area_groups.emplace_back(&expoly, SupportLayer::BaseType, 0); + 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()); + } + } + } + } + profiler.stage_start(STAGE_GENERATE_TOOLPATHS); m_object->print()->set_status(70, _u8L("Generating support")); generate_toolpaths(); diff --git a/src/libslic3r/Support/TreeSupport3D.cpp b/src/libslic3r/Support/TreeSupport3D.cpp index b551a42ece..9d35cb2dd8 100644 --- a/src/libslic3r/Support/TreeSupport3D.cpp +++ b/src/libslic3r/Support/TreeSupport3D.cpp @@ -3381,7 +3381,7 @@ static void generate_support_areas(Print &print, TreeSupport* tree_support, cons // 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(po.model_object()->raw_bounding_box().min.z()); + 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) { diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index d997fbb7a9..202c28c121 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4432,12 +4432,41 @@ void TabPrinter::build_fff() optgroup->append_line(line); } { - Line line = { L("G-code axis remap"), L("Remap slicing-frame axes to machine axes in G-code output") }; + 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.append_option(optgroup->get_option("belt_preslice_remap_x")); + line.append_option(optgroup->get_option("belt_preslice_remap_y")); + line.append_option(optgroup->get_option("belt_preslice_remap_z")); + optgroup->append_line(line); + } + { + Line line = { L("G-code axis remap (post-slice)"), L("Remap slicing-frame axes to machine axes in G-code output. Applied AFTER slicing, during G-code generation.") }; line.append_option(optgroup->get_option("belt_gcode_remap_x")); line.append_option(optgroup->get_option("belt_gcode_remap_y")); line.append_option(optgroup->get_option("belt_gcode_remap_z")); optgroup->append_line(line); } + optgroup->append_single_option_line("belt_gcode_back_transform"); + { + Line line = { L("Origin snap X"), L("Snap object bbox min X to offset in G-code output") }; + line.append_option(optgroup->get_option("belt_origin_snap_x")); + line.append_option(optgroup->get_option("belt_origin_offset_x")); + optgroup->append_line(line); + } + { + Line line = { L("Origin snap Y"), L("Snap object bbox min Y to offset in G-code output") }; + line.append_option(optgroup->get_option("belt_origin_snap_y")); + line.append_option(optgroup->get_option("belt_origin_offset_y")); + optgroup->append_line(line); + } + { + Line line = { L("Origin snap Z"), L("Snap object bbox min Z to offset in G-code output") }; + line.append_option(optgroup->get_option("belt_origin_snap_z")); + line.append_option(optgroup->get_option("belt_origin_offset_z")); + optgroup->append_line(line); + } { Line line = { L("Support floor"), L("Belt floor awareness for support generation and clipping") }; line.append_option(optgroup->get_option("belt_support_floor_mode")); @@ -5292,7 +5321,9 @@ void TabPrinter::toggle_options() toggle_line("belt_printer_infinite_y", is_belt); for (auto el : {"belt_shear_x", "belt_shear_y", "belt_shear_z", "belt_scale_x", "belt_scale_y", "belt_scale_z", - "belt_gcode_remap_x"}) + "belt_preslice_remap_x", + "belt_gcode_remap_x", "belt_gcode_back_transform", + "belt_origin_snap_x", "belt_origin_snap_y", "belt_origin_snap_z"}) toggle_line(el, is_belt); // Gray out angle/from sub-options when their parent shear/scale mode is None. @@ -5320,6 +5351,10 @@ void TabPrinter::toggle_options() auto scz = m_config->option>("belt_scale_z")->value; toggle_option("belt_scale_z_angle", is_belt && scz != BeltScaleMode::None); + toggle_option("belt_origin_offset_x", is_belt && m_config->opt_bool("belt_origin_snap_x")); + toggle_option("belt_origin_offset_y", is_belt && m_config->opt_bool("belt_origin_snap_y")); + toggle_option("belt_origin_offset_z", is_belt && m_config->opt_bool("belt_origin_snap_z")); + toggle_line("belt_support_floor_mode", is_belt); } From 2facaac9e81330cb26669d6d5e48ed9528f25956 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 27 Mar 2026 17:34:19 -0500 Subject: [PATCH 018/224] Part 3.1: refactor BeltTransform pipeline add BeltGCodeWriter add BeltGCode consolidate changes into shared classes for BeltGcode --- src/libslic3r/BeltGCode.cpp | 119 +++++ src/libslic3r/BeltGCode.hpp | 28 ++ src/libslic3r/BeltGCodeWriter.cpp | 243 +++++++++ src/libslic3r/BeltGCodeWriter.hpp | 50 ++ src/libslic3r/BeltSliceStrategy.cpp | 62 +++ src/libslic3r/BeltSliceStrategy.hpp | 45 ++ src/libslic3r/BeltTransform.cpp | 246 ++++++++++ src/libslic3r/BeltTransform.hpp | 146 ++++++ src/libslic3r/CMakeLists.txt | 10 + src/libslic3r/GCode.cpp | 542 +++++++-------------- src/libslic3r/GCode.hpp | 34 +- src/libslic3r/GCode/BeltBackTransform.cpp | 124 +---- src/libslic3r/GCodeWriter.cpp | 127 +---- src/libslic3r/GCodeWriter.hpp | 74 +-- src/libslic3r/Print.cpp | 14 +- src/libslic3r/Print.hpp | 26 +- src/libslic3r/PrintObject.cpp | 143 +----- src/libslic3r/PrintObjectSlice.cpp | 177 +------ src/libslic3r/Support/BeltFloorContext.cpp | 124 +++++ src/libslic3r/Support/BeltFloorContext.hpp | 74 +++ src/libslic3r/Support/SupportMaterial.cpp | 131 +---- src/libslic3r/Support/TreeModelVolumes.cpp | 74 +-- src/libslic3r/Support/TreeSupport.cpp | 102 ++-- src/libslic3r/Support/TreeSupport3D.cpp | 36 +- 24 files changed, 1522 insertions(+), 1229 deletions(-) create mode 100644 src/libslic3r/BeltGCode.cpp create mode 100644 src/libslic3r/BeltGCode.hpp create mode 100644 src/libslic3r/BeltGCodeWriter.cpp create mode 100644 src/libslic3r/BeltGCodeWriter.hpp create mode 100644 src/libslic3r/BeltSliceStrategy.cpp create mode 100644 src/libslic3r/BeltSliceStrategy.hpp create mode 100644 src/libslic3r/BeltTransform.cpp create mode 100644 src/libslic3r/BeltTransform.hpp create mode 100644 src/libslic3r/Support/BeltFloorContext.cpp create mode 100644 src/libslic3r/Support/BeltFloorContext.hpp diff --git a/src/libslic3r/BeltGCode.cpp b/src/libslic3r/BeltGCode.cpp new file mode 100644 index 0000000000..dc1b6ca439 --- /dev/null +++ b/src/libslic3r/BeltGCode.cpp @@ -0,0 +1,119 @@ +#include "BeltGCode.hpp" +#include "BeltGCodeWriter.hpp" +#include "BeltTransform.hpp" +#include "Print.hpp" + +#include + +namespace Slic3r { + +void BeltGCode::init_belt_writer(Print &print, bool is_bbl_printers) +{ + if (!print.config().belt_printer.value) + return; + + auto belt_writer = std::make_unique(); + belt_writer->set_is_bbl_machine(is_bbl_printers); + belt_writer->set_belt_angle(print.config().belt_printer_angle.value); + belt_writer->set_axis_remap( + int(print.config().belt_gcode_remap_x.value), + int(print.config().belt_gcode_remap_y.value), + int(print.config().belt_gcode_remap_z.value)); + BoundingBoxf bbox_bed(print.config().printable_area.values); + belt_writer->set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(), + print.config().printable_height.value)); + belt_writer->set_belt_back_transform(print.config()); + m_writer = std::move(belt_writer); + + // Per-axis origin snap config. + m_origin_snap[0] = print.config().belt_origin_snap_x.value; + m_origin_snap[1] = print.config().belt_origin_snap_y.value; + m_origin_snap[2] = print.config().belt_origin_snap_z.value; + m_origin_snap_offset[0] = print.config().belt_origin_offset_x.value; + m_origin_snap_offset[1] = print.config().belt_origin_offset_y.value; + m_origin_snap_offset[2] = print.config().belt_origin_offset_z.value; +} + +void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print) +{ + if (!print.config().belt_printer.value) + return; + + file.write_format("; belt_printer_angle = %.1f\n", print.config().belt_printer_angle.value); + // Shear configs + const auto &full_cfg = print.full_print_config(); + file.write_format("; belt_shear_x = %s\n", full_cfg.opt_serialize("belt_shear_x").c_str()); + file.write_format("; belt_shear_x_angle = %.1f\n", print.config().belt_shear_x_angle.value); + file.write_format("; belt_shear_x_from = %s\n", full_cfg.opt_serialize("belt_shear_x_from").c_str()); + file.write_format("; belt_shear_y = %s\n", full_cfg.opt_serialize("belt_shear_y").c_str()); + file.write_format("; belt_shear_y_angle = %.1f\n", print.config().belt_shear_y_angle.value); + file.write_format("; belt_shear_y_from = %s\n", full_cfg.opt_serialize("belt_shear_y_from").c_str()); + file.write_format("; belt_shear_z = %s\n", full_cfg.opt_serialize("belt_shear_z").c_str()); + file.write_format("; belt_shear_z_angle = %.1f\n", print.config().belt_shear_z_angle.value); + file.write_format("; belt_shear_z_from = %s\n", full_cfg.opt_serialize("belt_shear_z_from").c_str()); + // Scale configs + file.write_format("; belt_scale_x = %s\n", full_cfg.opt_serialize("belt_scale_x").c_str()); + file.write_format("; belt_scale_x_angle = %.1f\n", print.config().belt_scale_x_angle.value); + file.write_format("; belt_scale_y = %s\n", full_cfg.opt_serialize("belt_scale_y").c_str()); + file.write_format("; belt_scale_y_angle = %.1f\n", print.config().belt_scale_y_angle.value); + file.write_format("; belt_scale_z = %s\n", full_cfg.opt_serialize("belt_scale_z").c_str()); + file.write_format("; belt_scale_z_angle = %.1f\n", print.config().belt_scale_z_angle.value); + // Pre-slice remap configs + file.write_format("; belt_preslice_remap_x = %s\n", full_cfg.opt_serialize("belt_preslice_remap_x").c_str()); + file.write_format("; belt_preslice_remap_y = %s\n", full_cfg.opt_serialize("belt_preslice_remap_y").c_str()); + file.write_format("; belt_preslice_remap_z = %s\n", full_cfg.opt_serialize("belt_preslice_remap_z").c_str()); +} + +void BeltGCode::on_set_origin(const PrintObject *obj, const Point &inst_shift) +{ + if (!m_origin_snap[0] && !m_origin_snap[1] && !m_origin_snap[2]) + return; + + auto *belt_writer = dynamic_cast(m_writer.get()); + if (!belt_writer) + return; + + // Clear existing snap so to_machine_coords gives raw machine coords for bbox computation. + for (int a = 0; a < 3; ++a) + belt_writer->set_origin_snap(a, false, 0., 0.); + + // Reconstruct the belt pipeline transform for this object. + Transform3d belt = BeltTransformPipeline::build_forward_transform(m_config); + + // Z-shift + double zs = (obj->belt_min_z() < 0.) ? -obj->belt_min_z() : 0.; + if (zs > 0.) { + Transform3d zsh = Transform3d::Identity(); + zsh.matrix()(2, 3) = zs; + belt = zsh * belt; + } + + // Full transform: belt * trafo_centered + Transform3d full = belt * obj->trafo_centered(); + + // Instance shift in slicer space + global Z offset + Vec3d shift(unscale(inst_shift.x()), + unscale(inst_shift.y()), + obj->belt_global_z_offset()); + + // Compute this instance's machine-space bbox min + BoundingBoxf3 bb = obj->model_object()->raw_bounding_box(); + Vec3d mn = bb.min.cast(), mx = bb.max.cast(); + Vec3d inst_min(std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max()); + 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 mc = belt_writer->to_machine_coords(full * c + shift); + for (int a = 0; a < 3; ++a) + inst_min[a] = std::min(inst_min[a], mc[a]); + } + + // Update writer snap for each enabled axis + for (int a = 0; a < 3; ++a) + belt_writer->set_origin_snap(a, m_origin_snap[a], m_origin_snap_offset[a], inst_min[a]); +} + +} // namespace Slic3r diff --git a/src/libslic3r/BeltGCode.hpp b/src/libslic3r/BeltGCode.hpp new file mode 100644 index 0000000000..682deb7d65 --- /dev/null +++ b/src/libslic3r/BeltGCode.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "GCode.hpp" + +namespace Slic3r { + +// Belt-printer-specific GCode export. +// +// Inherits from GCode and overrides virtual hooks to: +// - Create a BeltGCodeWriter instead of a plain GCodeWriter +// - Write belt configuration to the G-code header +// - Update per-axis origin snap when switching instances +// - Disable arc fitting (G2/G3 not supported on belt printers) +class BeltGCode : public GCode +{ +protected: + void init_belt_writer(Print &print, bool is_bbl_printers) override; + void write_belt_header(GCodeOutputStream &file, const Print &print) override; + void on_set_origin(const PrintObject *obj, const Point &inst_shift) override; + bool should_disable_arc_fitting() const override { return true; } + +private: + // Per-axis origin snap config (set during init, used in on_set_origin). + bool m_origin_snap[3] = {false, false, false}; + double m_origin_snap_offset[3] = {0., 0., 0.}; +}; + +} // namespace Slic3r diff --git a/src/libslic3r/BeltGCodeWriter.cpp b/src/libslic3r/BeltGCodeWriter.cpp new file mode 100644 index 0000000000..645bf85a5b --- /dev/null +++ b/src/libslic3r/BeltGCodeWriter.cpp @@ -0,0 +1,243 @@ +#include "BeltGCodeWriter.hpp" +#include "Geometry.hpp" + +namespace Slic3r { + +// ---- Belt configuration --------------------------------------------------- + +void BeltGCodeWriter::set_belt_angle(double angle_deg) +{ + m_belt_angle_rad = Geometry::deg2rad(angle_deg); +} + +void BeltGCodeWriter::set_axis_remap(int rx, int ry, int rz) +{ + m_remap_x = rx; + m_remap_y = ry; + m_remap_z = rz; +} + +void BeltGCodeWriter::set_build_volume_max(const Vec3d &max) +{ + m_build_vol_max = max; +} + +void BeltGCodeWriter::set_belt_back_transform(const PrintConfig &config) +{ + m_belt_back_transform.init_from_config(config); +} + +void BeltGCodeWriter::set_origin_snap(int axis, bool enable, double offset, double bbox_min) +{ + if (axis >= 0 && axis < 3) { + m_origin_snap[axis] = enable; + m_origin_offset[axis] = offset; + m_origin_bbox_min[axis] = bbox_min; + } +} + +Vec3d BeltGCodeWriter::to_machine_coords(const Vec3d &pos) const +{ + // Step 1: Undo the shear/scale applied during slicing. + Vec3d p = m_belt_back_transform.apply(pos); + // Step 2: Apply axis remapping for the machine's coordinate convention. + // BeltRemapAxis: 0-2 = +X/+Y/+Z, 3-5 = -X/-Y/-Z, 6-8 = Rev X/Y/Z + auto remap = [this, &p](int r) -> double { + int axis = r % 3; + if (r < 3) return p[axis]; + if (r < 6) return -p[axis]; + return m_build_vol_max[axis] - p[axis]; + }; + Vec3d result = { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) }; + // Per-axis origin snap: shift so bbox min on each enabled axis = offset. + for (int i = 0; i < 3; ++i) + if (m_origin_snap[i]) + result[i] -= (m_origin_bbox_min[i] - m_origin_offset[i]); + return result; +} + +// ---- Overridden movement methods ------------------------------------------ + +std::string BeltGCodeWriter::travel_to_xy(const Vec2d &point, const std::string &comment) +{ + m_pos(0) = point(0); + m_pos(1) = point(1); + + this->set_current_position_clear(true); + Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; + + // Belt printer: transform to machine coordinates (XY travel also needs Z due to YZ rotation) + Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); + + GCodeG1Formatter w; + w.emit_xyz(machine); + auto speed = m_is_first_layer + ? this->config.get_abs_value("initial_layer_travel_speed") : this->config.travel_speed.value; + w.emit_f(speed * 60.0); + w.emit_comment(GCodeWriter::full_gcode_comment, comment); + return w.string(); +} + +std::string BeltGCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase) +{ + // Belt printer: force NormalLift since SpiralLift and SlopeLift compute + // slope angles that don't account for the YZ coordinate rotation. + return GCodeWriter::lazy_lift(LiftType::NormalLift, spiral_vase); +} + +std::string BeltGCodeWriter::eager_lift(const LiftType type) +{ + // Belt printer: force NormalLift (SpiralLift/SlopeLift don't account for YZ rotation). + return GCodeWriter::eager_lift(LiftType::NormalLift); +} + +std::string BeltGCodeWriter::_travel_to_z(double z, const std::string &comment) +{ + m_pos(2) = z; + + double speed = this->config.travel_speed_z.value; + if (speed == 0.) { + speed = m_is_first_layer ? this->config.get_abs_value("initial_layer_travel_speed") + : this->config.travel_speed.value; + } + + // Belt printer: a Z-only move in slicing frame needs to emit both Y and Z in machine coords. + Vec3d machine = to_machine_coords(Vec3d(m_pos.x() - m_x_offset, m_pos.y() - m_y_offset, z)); + + GCodeG1Formatter w; + w.emit_xyz(machine); + w.emit_f(speed * 60.0); + w.emit_comment(GCodeWriter::full_gcode_comment, comment); + return w.string(); +} + +std::string BeltGCodeWriter::extrude_to_xy(const Vec2d &point, double dE, const std::string &comment, bool force_no_extrusion) +{ + m_pos(0) = point(0); + m_pos(1) = point(1); + if (std::abs(dE) <= std::numeric_limits::epsilon()) + force_no_extrusion = true; + + if (!force_no_extrusion) + filament()->extrude(dE); + + Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; + + // Belt printer: transform and emit XYZ (Y and Z are coupled) + Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); + + GCodeG1Formatter w; + w.emit_xyz(machine); + if (!force_no_extrusion) + w.emit_e(filament()->E()); + w.emit_comment(GCodeWriter::full_gcode_comment, comment); + return w.string(); +} + +std::string BeltGCodeWriter::extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment, bool force_no_extrusion) +{ + m_pos = point; + m_lifted = 0; + if (!force_no_extrusion) + filament()->extrude(dE); + + Vec3d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset, point(2) }; + point_on_plate = to_machine_coords(point_on_plate); + + GCodeG1Formatter w; + w.emit_xyz(point_on_plate); + if (!force_no_extrusion) + w.emit_e(filament()->E()); + w.emit_comment(GCodeWriter::full_gcode_comment, comment); + return w.string(); +} + +std::string BeltGCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &comment, bool force_z) +{ + // Belt-specific override of travel_to_xyz. + // Key differences from base: + // 1. All coordinates go through to_machine_coords() + // 2. Always emit full XYZ (can't split XY and Z due to coupling) + // 3. Lift type forced to NormalLift (handled by lazy_lift/eager_lift overrides) + + Vec3d dest_point = point; + auto travel_speed = + m_is_first_layer ? this->config.get_abs_value("initial_layer_travel_speed") : this->config.travel_speed.value; + + // Handle pending z_hop + if (std::abs(m_to_lift) > EPSILON) { + assert(std::abs(m_lifted) < EPSILON); + if ((!this->is_current_position_clear() || m_pos != dest_point) && + m_to_lift + m_pos(2) > point(2)) { + m_lifted = m_to_lift + m_pos(2) - point(2); + dest_point(2) = m_to_lift + m_pos(2); + } + m_to_lift = 0.; + + std::string slop_move; + Vec3d source = { m_pos(0) - m_x_offset, m_pos(1) - m_y_offset, m_pos(2) }; + Vec3d target = { dest_point(0) - m_x_offset, dest_point(1) - m_y_offset, dest_point(2) }; + Vec3d delta = target - source; + Vec2d delta_no_z = { delta(0), delta(1) }; + + if (delta(2) > 0 && delta_no_z.norm() != 0.0f) { + // Belt: SpiralLift and SlopeLift are disabled (lazy_lift forces NormalLift), + // but handle NormalLift and fallthrough. + if (m_to_lift_type == LiftType::SlopeLift && + this->is_current_position_clear() && + atan2(delta(2), delta_no_z.norm()) < this->filament()->travel_slope()) { + Vec2d temp = delta_no_z.normalized() * delta(2) / tan(this->filament()->travel_slope()); + Vec3d slope_top_point = Vec3d(temp(0), temp(1), delta(2)) + source; + slope_top_point = to_machine_coords(slope_top_point); + GCodeG1Formatter w0; + w0.emit_xyz(slope_top_point); + w0.emit_f(travel_speed * 60.0); + w0.emit_comment(GCodeWriter::full_gcode_comment, comment); + slop_move = w0.string(); + } + else if (m_to_lift_type == LiftType::NormalLift) { + slop_move = _travel_to_z(target.z(), "normal lift Z"); + } + } + + std::string xy_z_move; + { + Vec3d emit_target = to_machine_coords(target); + GCodeG1Formatter w0; + // Belt mode: always emit full XYZ since Y and Z are coupled + w0.emit_xyz(emit_target); + w0.emit_f(travel_speed * 60.0); + w0.emit_comment(GCodeWriter::full_gcode_comment, comment); + xy_z_move = w0.string(); + } + m_pos = dest_point; + this->set_current_position_clear(true); + return slop_move + xy_z_move; + } + else if (!force_z && !this->will_move_z(point(2))) { + double nominal_z = m_pos(2) - m_lifted; + m_lifted -= (point(2) - nominal_z); + if (std::abs(m_lifted) < EPSILON) + m_lifted = 0.; + this->set_current_position_clear(true); + return this->travel_to_xy(to_2d(point)); + } + else { + m_lifted = 0; + } + + Vec3d point_on_plate = { dest_point(0) - m_x_offset, dest_point(1) - m_y_offset, dest_point(2) }; + point_on_plate = to_machine_coords(point_on_plate); + + // Belt mode: always emit full XYZ + GCodeG1Formatter w; + w.emit_xyz(point_on_plate); + w.emit_f(this->config.travel_speed.value * 60.0); + w.emit_comment(GCodeWriter::full_gcode_comment, comment); + + m_pos = dest_point; + this->set_current_position_clear(true); + return w.string(); +} + +} // namespace Slic3r diff --git a/src/libslic3r/BeltGCodeWriter.hpp b/src/libslic3r/BeltGCodeWriter.hpp new file mode 100644 index 0000000000..727135b578 --- /dev/null +++ b/src/libslic3r/BeltGCodeWriter.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include "GCodeWriter.hpp" +#include "GCode/BeltBackTransform.hpp" + +namespace Slic3r { + +// Belt-printer-specific GCode writer. +// +// Inherits from GCodeWriter and overrides movement methods to apply +// coordinate transformation (back-transform, axis remap, origin snap) +// and emit coupled XYZ moves (Y and Z are coupled due to belt tilt). +class BeltGCodeWriter : public GCodeWriter +{ +public: + BeltGCodeWriter() : GCodeWriter() {} + + // Belt configuration + void set_belt_angle(double angle_deg); + bool is_belt_printer() const { return m_belt_angle_rad != 0.; } + void set_axis_remap(int rx, int ry, int rz); + void set_build_volume_max(const Vec3d &max); + void set_belt_back_transform(const PrintConfig &config); + void set_origin_snap(int axis, bool enable, double offset, double bbox_min); + Vec3d to_machine_coords(const Vec3d &pos) const; + + // Overridden movement methods + std::string travel_to_xy(const Vec2d &point, const std::string &comment = std::string()) override; + std::string travel_to_xyz(const Vec3d &point, const std::string &comment = std::string(), bool force_z = false) override; + std::string extrude_to_xy(const Vec2d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false) override; + std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false) override; + std::string lazy_lift(LiftType lift_type = LiftType::NormalLift, bool spiral_vase = false) override; + std::string eager_lift(const LiftType type) override; + +protected: + std::string _travel_to_z(double z, const std::string &comment) override; + +private: + double m_belt_angle_rad = 0.; + int m_remap_x = 0; + int m_remap_y = 1; + int m_remap_z = 2; + Vec3d m_build_vol_max = Vec3d::Zero(); + BeltBackTransform m_belt_back_transform; + bool m_origin_snap[3] = {false, false, false}; + double m_origin_offset[3] = {0., 0., 0.}; + double m_origin_bbox_min[3] = {0., 0., 0.}; +}; + +} // namespace Slic3r diff --git a/src/libslic3r/BeltSliceStrategy.cpp b/src/libslic3r/BeltSliceStrategy.cpp new file mode 100644 index 0000000000..b3adeec64a --- /dev/null +++ b/src/libslic3r/BeltSliceStrategy.cpp @@ -0,0 +1,62 @@ +#include "BeltSliceStrategy.hpp" + +#include + +namespace Slic3r { + +std::unique_ptr BeltSliceStrategy::create(const PrintConfig &config) +{ + if (!config.belt_printer.value) + return nullptr; + return std::unique_ptr(new BeltSliceStrategy(config)); +} + +BeltSliceStrategy::BeltSliceStrategy(const PrintConfig &config) +{ + m_has_remap = BeltTransformPipeline::has_preslice_remap(config); + if (m_has_remap) + m_pre_remap = BeltTransformPipeline::build_preslice_remap(config); + + m_shear = BeltTransformPipeline::build_shear_matrix(config, &m_has_shear); + m_scale = BeltTransformPipeline::build_scale_matrix(config, &m_has_scale); +} + +void BeltSliceStrategy::apply_to_trafo(Transform3d &trafo, + const ModelVolumePtrs &model_volumes, + double *out_belt_min_z) const +{ + // Step 1: Pre-slice axis remap. + if (m_has_remap) + trafo = m_pre_remap * trafo; + + // Step 2: Shear + scale. + if (m_has_shear || m_has_scale) { + Transform3d belt_xform = Transform3d::Identity(); + belt_xform.linear() = m_scale * m_shear; + trafo = belt_xform * trafo; + } + + // Step 3: Z-shift — detect if mesh clips below build plate after transforms. + if (m_has_remap || m_has_shear || m_has_scale) { + double min_z = std::numeric_limits::max(); + for (const ModelVolume *mv : model_volumes) { + if (!mv->is_model_part()) continue; + for (const stl_vertex &v : mv->mesh().its.vertices) { + Vec3d pt = trafo * v.cast(); + min_z = std::min(min_z, pt.z()); + } + } + double belt_z_shift_val = (min_z < 0. && min_z != std::numeric_limits::max()) ? -min_z : 0.; + BOOST_LOG_TRIVIAL(warning) << "Belt Z-shift: min_z=" << min_z + << " z_shift=" << belt_z_shift_val; + if (belt_z_shift_val > 0.) { + Transform3d z_shift = Transform3d::Identity(); + z_shift.matrix()(2, 3) = belt_z_shift_val; + trafo = z_shift * trafo; + } + if (out_belt_min_z) + *out_belt_min_z = (min_z != std::numeric_limits::max()) ? min_z : 0.; + } +} + +} // namespace Slic3r diff --git a/src/libslic3r/BeltSliceStrategy.hpp b/src/libslic3r/BeltSliceStrategy.hpp new file mode 100644 index 0000000000..bf19243fd2 --- /dev/null +++ b/src/libslic3r/BeltSliceStrategy.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include "libslic3r.h" +#include "Point.hpp" +#include "BeltTransform.hpp" +#include "PrintConfig.hpp" +#include "Model.hpp" + +#include +#include + +namespace Slic3r { + +// Belt printer pre-slice transform strategy. +// +// Encapsulates the pre-remap, shear, scale, and Z-shift transforms +// that are applied to model geometry before slicing on belt printers. +// Used by PrintObjectSlice.cpp to isolate belt-specific logic from +// the slicing pipeline. +class BeltSliceStrategy +{ +public: + // Create a strategy if belt_printer is enabled; returns nullptr otherwise. + static std::unique_ptr create(const PrintConfig &config); + + // Apply belt transforms to the slicing trafo. + // Modifies trafo in-place: trafo = z_shift * scale * shear * pre_remap * trafo + // Scans all model_part volumes to detect minimum Z and add z-shift if needed. + // Sets *out_belt_min_z to the minimum Z of the mesh after transforms. + void apply_to_trafo(Transform3d &trafo, + const ModelVolumePtrs &model_volumes, + double *out_belt_min_z) const; + +private: + explicit BeltSliceStrategy(const PrintConfig &config); + + bool m_has_remap = false; + bool m_has_shear = false; + bool m_has_scale = false; + Transform3d m_pre_remap = Transform3d::Identity(); + Matrix3d m_shear = Matrix3d::Identity(); + Matrix3d m_scale = Matrix3d::Identity(); +}; + +} // namespace Slic3r diff --git a/src/libslic3r/BeltTransform.cpp b/src/libslic3r/BeltTransform.cpp new file mode 100644 index 0000000000..db7eea7d80 --- /dev/null +++ b/src/libslic3r/BeltTransform.cpp @@ -0,0 +1,246 @@ +#include "BeltTransform.hpp" +#include "Model.hpp" + +#include + +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.belt_preslice_remap_x.value); + int pre_ry = int(config.belt_preslice_remap_y.value); + int pre_rz = int(config.belt_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_shear_matrix(const PrintConfig &config, bool *has_shear_out) +{ + struct AxisShear { BeltShearMode mode; double angle; int from; }; + AxisShear axes[3] = { + { config.belt_shear_x.value, config.belt_shear_x_angle.value, int(config.belt_shear_x_from.value) }, + { config.belt_shear_y.value, config.belt_shear_y_angle.value, int(config.belt_shear_y_from.value) }, + { config.belt_shear_z.value, config.belt_shear_z_angle.value, int(config.belt_shear_z_from.value) }, + }; + + Matrix3d shear = Matrix3d::Identity(); + bool active = false; + for (int row = 0; row < 3; ++row) { + if (axes[row].mode != BeltShearMode::None) { + double factor = compute_shear_factor(axes[row].mode, axes[row].angle); + if (std::abs(factor) > EPSILON) { + shear(row, axes[row].from) += factor; + active = true; + } + } + } + if (has_shear_out) *has_shear_out = active; + return shear; +} + +Matrix3d BeltTransformPipeline::build_scale_matrix(const PrintConfig &config, bool *has_scale_out) +{ + double sx = compute_scale_factor(config.belt_scale_x.value, config.belt_scale_x_angle.value); + double sy = compute_scale_factor(config.belt_scale_y.value, config.belt_scale_y_angle.value); + double sz = compute_scale_factor(config.belt_scale_z.value, config.belt_scale_z_angle.value); + + bool active = (std::abs(sx - 1.) > EPSILON || + std::abs(sy - 1.) > EPSILON || + std::abs(sz - 1.) > EPSILON); + + Matrix3d scale = Matrix3d::Identity(); + if (active) { + scale(0, 0) = sx; + scale(1, 1) = sy; + scale(2, 2) = sz; + } + if (has_scale_out) *has_scale_out = active; + return scale; +} + +Transform3d BeltTransformPipeline::build_forward_transform(const PrintConfig &config) +{ + Transform3d pre_remap = build_preslice_remap(config); + bool shear_active = false; + Matrix3d shear = build_shear_matrix(config, &shear_active); + bool scale_active = false; + Matrix3d scale = build_scale_matrix(config, &scale_active); + + // Pipeline: scale * shear * pre_remap + Transform3d combined = Transform3d::Identity(); + combined.linear() = scale * shear; + combined = combined * pre_remap; + return combined; +} + +// ---- Bounding box remap --------------------------------------------------- + +BoundingBoxf3 BeltTransformPipeline::remap_bbox(const BoundingBoxf3 &bb, const PrintConfig &config) +{ + int pre_rx = int(config.belt_preslice_remap_x.value); + int pre_ry = int(config.belt_preslice_remap_y.value); + int pre_rz = int(config.belt_preslice_remap_z.value); + + if (pre_rx == int(BeltRemapAxis::PosX) && + pre_ry == int(BeltRemapAxis::PosY) && + pre_rz == int(BeltRemapAxis::PosZ)) + return bb; // Identity remap. + + 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(), mx = bb.max.cast(); + 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 { + +template +BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl( + const Config &config, const BoundingBoxf3 &bb, double original_height) +{ + BeltTransformPipeline::BeltHeightResult result; + result.object_height = original_height; + + // Extract Z-axis shear/scale config. + BeltShearMode z_shear_mode; + double z_shear_angle; + BeltScaleMode z_scale_mode; + double z_scale_angle; + int z_shear_from; + + if constexpr (std::is_same_v) { + z_shear_mode = config.belt_shear_z.value; + z_shear_angle = config.belt_shear_z_angle.value; + z_scale_mode = config.belt_scale_z.value; + z_scale_angle = config.belt_scale_z_angle.value; + z_shear_from = int(config.belt_shear_z_from.value); + } else { + // DynamicPrintConfig path + auto get_shear = [&](const char *key) { + auto *opt = config.template option>(key); + return opt ? opt->value : BeltShearMode::None; + }; + auto get_scale = [&](const char *key) { + auto *opt = config.template option>(key); + return opt ? opt->value : BeltScaleMode::None; + }; + auto get_float = [&](const char *key) { + auto *opt = config.template option(key); + return opt ? opt->value : 45.0; + }; + auto get_axis = [&](const char *key) { + auto *opt = config.template option>(key); + return opt ? int(opt->value) : 1; + }; + z_shear_mode = get_shear("belt_shear_z"); + z_shear_angle = get_float("belt_shear_z_angle"); + z_scale_mode = get_scale("belt_scale_z"); + z_scale_angle = get_float("belt_scale_z_angle"); + z_shear_from = get_axis("belt_shear_z_from"); + } + + bool has_z_shear = z_shear_mode != BeltShearMode::None; + bool has_z_scale = z_scale_mode != BeltScaleMode::None; + + if (!has_z_shear && !has_z_scale) + return result; + + double shear_factor = has_z_shear + ? BeltTransformPipeline::compute_shear_factor(z_shear_mode, z_shear_angle) : 0.; + double scale_z = BeltTransformPipeline::compute_scale_factor(z_scale_mode, z_scale_angle); + + if (has_z_shear && std::abs(shear_factor) > EPSILON) { + int from = z_shear_from; + double min_rz = std::numeric_limits::max(); + double max_rz = std::numeric_limits::lowest(); + for (double vz : {bb.min.z(), bb.max.z()}) + for (double vs : {bb.min(from), bb.max(from)}) { + double new_z = scale_z * (vz + shear_factor * vs); + min_rz = std::min(min_rz, new_z); + max_rz = std::max(max_rz, new_z); + } + result.object_height = max_rz - min_rz; + result.floor_params.shear_factor = shear_factor; + result.floor_params.from_axis = from; + result.floor_params.z_shift = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.); + } else { + result.object_height = original_height * scale_z; + } + + return result; +} + +} // anonymous namespace + +BeltTransformPipeline::BeltHeightResult BeltTransformPipeline::compute_belt_height_and_floor( + const PrintConfig &config, const BoundingBoxf3 &remapped_bbox, double original_height) +{ + return compute_belt_height_and_floor_impl(config, remapped_bbox, original_height); +} + +BeltTransformPipeline::BeltHeightResult BeltTransformPipeline::compute_belt_height_and_floor( + const DynamicPrintConfig &config, const BoundingBoxf3 &remapped_bbox, double original_height) +{ + return compute_belt_height_and_floor_impl(config, remapped_bbox, original_height); +} + +} // namespace Slic3r diff --git a/src/libslic3r/BeltTransform.hpp b/src/libslic3r/BeltTransform.hpp new file mode 100644 index 0000000000..b9a60473e5 --- /dev/null +++ b/src/libslic3r/BeltTransform.hpp @@ -0,0 +1,146 @@ +#pragma once + +#include "libslic3r.h" +#include "Point.hpp" +#include "BoundingBox.hpp" +#include "PrintConfig.hpp" +#include "Geometry.hpp" + +#include + +namespace Slic3r { + +class ModelObject; + +// Shared belt-printer transform math. +// +// The pre-slice pipeline applied in PrintObjectSlice.cpp is: +// trafo_out = z_shift * scale * shear * pre_remap * trafo_in +// +// This class provides the building blocks so every call site uses the +// same implementation. z_shift is object-dependent (computed from mesh +// vertex bounds) and is NOT included in build_forward_transform(). +class BeltTransformPipeline +{ +public: + // ---- Pure math helpers ------------------------------------------------ + + static double compute_shear_factor(BeltShearMode mode, double angle_deg) + { + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad); + double cos_a = std::cos(angle_rad); + switch (mode) { + case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; + case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; + case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; + case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; + default: return 0.; + } + } + + static double compute_scale_factor(BeltScaleMode mode, double angle_deg) + { + if (mode == BeltScaleMode::None) return 1.; + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad); + double cos_a = std::cos(angle_rad); + switch (mode) { + case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; + case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; + case BeltScaleMode::Sin: return sin_a; + case BeltScaleMode::Cos: return cos_a; + default: return 1.; + } + } + + // ---- Identity checks -------------------------------------------------- + + static bool has_preslice_remap(const PrintConfig &config) + { + return int(config.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || + int(config.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || + int(config.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ); + } + + // Overload accepting DynamicPrintConfig (used in static slicing_parameters). + static bool has_preslice_remap(const DynamicPrintConfig &config) + { + auto get_int = [&](const char *key) -> int { + auto *opt = config.option>(key); + return opt ? int(opt->value) : 0; + }; + return get_int("belt_preslice_remap_x") != int(BeltRemapAxis::PosX) || + get_int("belt_preslice_remap_y") != int(BeltRemapAxis::PosY) || + get_int("belt_preslice_remap_z") != int(BeltRemapAxis::PosZ); + } + + static bool has_shear(const PrintConfig &config) + { + return config.belt_shear_x.value != BeltShearMode::None || + config.belt_shear_y.value != BeltShearMode::None || + config.belt_shear_z.value != BeltShearMode::None; + } + + static bool has_scale(const PrintConfig &config) + { + double sx = compute_scale_factor(config.belt_scale_x.value, config.belt_scale_x_angle.value); + double sy = compute_scale_factor(config.belt_scale_y.value, config.belt_scale_y_angle.value); + double sz = compute_scale_factor(config.belt_scale_z.value, config.belt_scale_z_angle.value); + return std::abs(sx - 1.) > EPSILON || + std::abs(sy - 1.) > EPSILON || + std::abs(sz - 1.) > EPSILON; + } + + // ---- Matrix builders -------------------------------------------------- + + // Build the pre-slice axis remap transform (includes Rev-mode translation). + static Transform3d build_preslice_remap(const PrintConfig &config); + + // Build the 3x3 shear matrix. Returns Identity if no shear is active. + // Also sets has_shear_out if non-null. + static Matrix3d build_shear_matrix(const PrintConfig &config, bool *has_shear_out = nullptr); + + // Build the 3x3 diagonal scale matrix. Returns Identity if no scale. + // Also sets has_scale_out if non-null. + static Matrix3d build_scale_matrix(const PrintConfig &config, bool *has_scale_out = nullptr); + + // Combined forward transform: scale * shear * pre_remap. + // 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 { + double shear_factor = 0.0; + int from_axis = 1; + double z_shift = 0.0; + }; + + // Result of computing belt height + floor params. + struct BeltHeightResult { + double object_height; // Effective object height after shear/scale + BeltFloorParams floor_params; + }; + + // Compute effective object height and belt floor parameters from config + // and pre-remapped 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, + 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, + double original_height); +}; + +} // namespace Slic3r diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index c2d92ee800..a3e813b1da 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -79,6 +79,14 @@ set(lisbslic3r_sources BoundingBox.hpp BridgeDetector.cpp BridgeDetector.hpp + BeltGCode.cpp + BeltGCode.hpp + BeltGCodeWriter.cpp + BeltGCodeWriter.hpp + BeltSliceStrategy.cpp + BeltSliceStrategy.hpp + BeltTransform.cpp + BeltTransform.hpp Brim.cpp BrimEarsPoint.hpp Brim.hpp @@ -417,6 +425,8 @@ set(lisbslic3r_sources SlicingAdaptive.hpp Slicing.cpp Slicing.hpp + Support/BeltFloorContext.cpp + Support/BeltFloorContext.hpp Support/SupportCommon.cpp Support/SupportCommon.hpp Support/SupportLayer.hpp diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index ac04c6dd5b..a330fad689 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -8,6 +8,7 @@ #include "Exception.hpp" #include "ExtrusionEntity.hpp" #include "EdgeGrid.hpp" +#include "BeltTransform.hpp" #include "Geometry.hpp" #include "Geometry/ConvexHull.hpp" #include "GCode/PrintExtents.hpp" @@ -795,7 +796,7 @@ static std::vector get_path_of_change_filament(const Print& print) //BBS: if needed, write the gcode_label_objects_end then priming tower, if the retract, didn't did it. std::string object_end_label_temp; - gcodegen.m_writer.add_object_end_labels(object_end_label_temp); + gcodegen.m_writer->add_object_end_labels(object_end_label_temp); // Process the custom change_filament_gcode. If it is empty, provide a simple Tn command to change the filament. // Otherwise, leave control to the user completely. @@ -839,7 +840,7 @@ static std::vector get_path_of_change_filament(const Print& print) // config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); // BBS { - GCodeWriter& gcode_writer = gcodegen.m_writer; + GCodeWriter& gcode_writer = *gcodegen.m_writer; FullPrintConfig& full_config = gcodegen.m_config; // set volumetric speed of outer wall ,ignore per obejct,just use default setting @@ -1573,8 +1574,8 @@ static std::vector get_path_of_change_filament(const Print& print) const std::vector ColorPrintColors::Colors = { "#C0392B", "#E67E22", "#F1C40F", "#27AE60", "#1ABC9C", "#2980B9", "#9B59B6" }; -#define EXTRUDER_CONFIG(OPT) m_config.OPT.get_at(m_writer.filament()->extruder_id()) -#define FILAMENT_CONFIG(OPT) m_config.OPT.get_at(m_writer.filament()->id()) +#define EXTRUDER_CONFIG(OPT) m_config.OPT.get_at(m_writer->filament()->extruder_id()) +#define FILAMENT_CONFIG(OPT) m_config.OPT.get_at(m_writer->filament()->id()) void GCode::PlaceholderParserIntegration::reset() { @@ -1687,7 +1688,7 @@ void GCode::PlaceholderParserIntegration::validate_output_vector_variables() // Collect pairs of object_layer + support_layer sorted by print_z. // object_layer & support_layer are considered to be on the same print_z, if they are not further than EPSILON. -std::vector GCode::collect_layers_to_print(const PrintObject& object) +std::vector GCode::collect_layers_to_print(const PrintObject& object, bool skip_empty_first_layer) { std::vector layers_to_print; layers_to_print.reserve(object.layers().size() + object.support_layers().size()); @@ -1753,8 +1754,7 @@ std::vector GCode::collect_layers_to_print(const PrintObjec // an error. In global shear mode the object may also start above // Z=0 on the tilted belt surface. if (layers_to_print.size() == 1u) { - bool skip_empty_check = object.print()->config().belt_printer.value; - if (!has_extrusions && !skip_empty_check) + if (!has_extrusions && !skip_empty_first_layer) throw Slic3r::SlicingError(_(L("One object has an empty first layer and can't be printed. Please Cut the bottom or enable supports.")), object.id().id); } @@ -1827,7 +1827,7 @@ std::vector>> GCode::collec for (size_t i = 0; i < print.objects().size(); ++i) { try { - per_object[i] = collect_layers_to_print(*print.objects()[i]); + per_object[i] = collect_layers_to_print(*print.objects()[i], print.config().belt_printer.value); } catch (const Slic3r::SlicingError &e) { errors.push_back(e); continue; @@ -2016,7 +2016,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu BOOST_LOG_TRIVIAL(info) << boost::format("Will export G-code to %1% soon")%path; GCodeProcessor::s_IsBBLPrinter = print->is_BBL_printer(); - m_writer.set_is_bbl_machine(print->is_BBL_printer()); + m_writer->set_is_bbl_machine(print->is_BBL_printer()); print->set_started(psGCodeExport); // check if any custom gcode contains keywords used by the gcode processor to @@ -2113,7 +2113,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu m_processor.result().support_traditional_timelapse = m_support_traditional_timelapse; bool activate_long_retraction_when_cut = false; - for (const auto& filament : m_writer.extruders()) + for (const auto& filament : m_writer->extruders()) activate_long_retraction_when_cut |= ( m_config.long_retractions_when_cut.get_at(filament.id()) && m_config.retraction_distances_when_cut.get_at(filament.id()) > 0 @@ -2144,7 +2144,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu m_processor.finalize(true); // DoExport::update_print_estimated_times_stats(m_processor, print->m_print_statistics); - DoExport::update_print_estimated_stats(m_processor, m_writer.extruders(), print->m_print_statistics, print->config()); + DoExport::update_print_estimated_stats(m_processor, m_writer->extruders(), print->m_print_statistics, print->config()); if (result != nullptr) { *result = std::move(m_processor.extract_result()); // set the filename to the correct value @@ -2404,7 +2404,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato PROFILE_FUNC(); m_print = &print; - m_timelapse_pos_picker.init(&print,m_writer.get_xy_offset().cast()); + m_timelapse_pos_picker.init(&print,m_writer->get_xy_offset().cast()); // modifies m_silent_time_estimator_enabled DoExport::init_gcode_processor(print.config(), m_processor, m_silent_time_estimator_enabled); @@ -2420,29 +2420,10 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_fan_mover.release(); - m_writer.set_is_bbl_machine(is_bbl_printers); + m_writer->set_is_bbl_machine(is_bbl_printers); - // Belt printer: initialize coordinate transformation and axis remap on the writer. - if (print.config().belt_printer.value) { - m_writer.set_belt_angle(print.config().belt_printer_angle.value); - m_writer.set_axis_remap( - int(print.config().belt_gcode_remap_x.value), - int(print.config().belt_gcode_remap_y.value), - int(print.config().belt_gcode_remap_z.value)); - // Build volume extents for Rev remap mode. - BoundingBoxf bbox_bed(print.config().printable_area.values); - m_writer.set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(), print.config().printable_height.value)); - // Initialize the back-transform that undoes slicing shear/scale. - m_writer.set_belt_back_transform(print.config()); - // Per-axis origin snap: store config; actual snap is computed - // per-instance in update_origin_snap() called from set_origin(). - m_origin_snap[0] = print.config().belt_origin_snap_x.value; - m_origin_snap[1] = print.config().belt_origin_snap_y.value; - m_origin_snap[2] = print.config().belt_origin_snap_z.value; - m_origin_snap_offset[0] = print.config().belt_origin_offset_x.value; - m_origin_snap_offset[1] = print.config().belt_origin_offset_y.value; - m_origin_snap_offset[2] = print.config().belt_origin_offset_z.value; - } + // Belt printer: initialize belt-specific writer via virtual hook. + this->init_belt_writer(print, is_bbl_printers); // How many times will be change_layer() called? // change_layer() in turn increments the progress bar status. @@ -2533,31 +2514,8 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.write_format("; HEADER_BLOCK_START\n"); // Write information on the generator. file.write_format("; generated by %s on %s\n", Slic3r::header_slic3r_generated().c_str(), Slic3r::Utils::local_timestamp().c_str()); - // Belt printer: embed angle and transform configs in header for G-code processor detection. - if (print.config().belt_printer.value) { - file.write_format("; belt_printer_angle = %.1f\n", print.config().belt_printer_angle.value); - // Shear configs - const auto &full_cfg = print.full_print_config(); - file.write_format("; belt_shear_x = %s\n", full_cfg.opt_serialize("belt_shear_x").c_str()); - file.write_format("; belt_shear_x_angle = %.1f\n", print.config().belt_shear_x_angle.value); - file.write_format("; belt_shear_x_from = %s\n", full_cfg.opt_serialize("belt_shear_x_from").c_str()); - file.write_format("; belt_shear_y = %s\n", full_cfg.opt_serialize("belt_shear_y").c_str()); - file.write_format("; belt_shear_y_angle = %.1f\n", print.config().belt_shear_y_angle.value); - file.write_format("; belt_shear_y_from = %s\n", full_cfg.opt_serialize("belt_shear_y_from").c_str()); - file.write_format("; belt_shear_z = %s\n", full_cfg.opt_serialize("belt_shear_z").c_str()); - file.write_format("; belt_shear_z_angle = %.1f\n", print.config().belt_shear_z_angle.value); - file.write_format("; belt_shear_z_from = %s\n", full_cfg.opt_serialize("belt_shear_z_from").c_str()); - // Scale configs - file.write_format("; belt_scale_x = %s\n", full_cfg.opt_serialize("belt_scale_x").c_str()); - file.write_format("; belt_scale_x_angle = %.1f\n", print.config().belt_scale_x_angle.value); - file.write_format("; belt_scale_y = %s\n", full_cfg.opt_serialize("belt_scale_y").c_str()); - file.write_format("; belt_scale_y_angle = %.1f\n", print.config().belt_scale_y_angle.value); - file.write_format("; belt_scale_z = %s\n", full_cfg.opt_serialize("belt_scale_z").c_str()); - file.write_format("; belt_scale_z_angle = %.1f\n", print.config().belt_scale_z_angle.value); - file.write_format("; belt_preslice_remap_x = %s\n", full_cfg.opt_serialize("belt_preslice_remap_x").c_str()); - file.write_format("; belt_preslice_remap_y = %s\n", full_cfg.opt_serialize("belt_preslice_remap_y").c_str()); - file.write_format("; belt_preslice_remap_z = %s\n", full_cfg.opt_serialize("belt_preslice_remap_z").c_str()); - } + // Belt printer: embed angle and transform configs in header via virtual hook. + this->write_belt_header(file, print); if (is_bbl_printers) file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str()); //BBS: total layer number @@ -2804,13 +2762,13 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Disable fan. if (m_config.auxiliary_fan.value && print.config().close_fan_the_first_x_layers.get_at(initial_extruder_id)) { - file.write(m_writer.set_fan(0)); + file.write(m_writer->set_fan(0)); //BBS: disable additional fan - file.write(m_writer.set_additional_fan(0)); + file.write(m_writer->set_additional_fan(0)); } // Update output variables after the extruders were initialized. - m_placeholder_parser_integration.init(m_writer); + m_placeholder_parser_integration.init(*m_writer); // Let the start-up script prime the 1st printing tool. @@ -2883,7 +2841,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato { BoundingBoxf bbox_bed(print.config().printable_area.values); - Vec2f plate_offset = m_writer.get_xy_offset(); + Vec2f plate_offset = m_writer->get_xy_offset(); this->placeholder_parser().set("print_bed_min", new ConfigOptionFloats({ bbox_bed.min.x(), bbox_bed.min.y()})); this->placeholder_parser().set("print_bed_max", new ConfigOptionFloats({ bbox_bed.max.x(), bbox_bed.max.y()})); this->placeholder_parser().set("print_bed_size", new ConfigOptionFloats({ bbox_bed.size().x(), bbox_bed.size().y() })); @@ -2985,7 +2943,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } bool activate_chamber_temp_control = false; auto max_chamber_temp = 0; - for (const auto &extruder : m_writer.extruders()) { + for (const auto &extruder : m_writer->extruders()) { activate_chamber_temp_control |= m_config.activate_chamber_temp_control.get_at(extruder.id()); max_chamber_temp = std::max(max_chamber_temp, m_config.chamber_temperature.get_at(extruder.id())); } @@ -2993,7 +2951,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato BedType curr_bed_type = m_config.curr_bed_type; int min_temperature_vitrification = std::numeric_limits::max(); - for (const auto& extruder : m_writer.extruders()) + for (const auto& extruder : m_writer->extruders()) min_temperature_vitrification = std::min(min_temperature_vitrification, m_config.temperature_vitrification.get_at(extruder.id())); @@ -3107,18 +3065,18 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato if (activate_chamber_temp_control && max_chamber_temp > 0){ int temp_out =0; if(!custom_gcode_sets_temperature(machine_start_gcode,141,191,false,temp_out)) - file.write(m_writer.set_chamber_temperature(max_chamber_temp, true)); // set chamber_temperature + file.write(m_writer->set_chamber_temperature(max_chamber_temp, true)); // set chamber_temperature } // Write the custom start G-code file.writeln(machine_start_gcode); //BBS: gcode writer doesn't know where the real position of extruder is after inserting custom gcode - m_writer.set_current_position_clear(false); + m_writer->set_current_position_clear(false); m_start_gcode_filament = GCodeProcessor::get_gcode_last_filament(machine_start_gcode); if (is_bbl_printers) { - m_writer.init_extruder(initial_non_support_extruder_id); + m_writer->init_extruder(initial_non_support_extruder_id); // add the missing filament start gcode in machine start gcode { DynamicConfig config; @@ -3131,7 +3089,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } // Orca: add missing PA settings for initial filament if (m_config.enable_pressure_advance.get_at(initial_non_support_extruder_id)) { - file.write(m_writer.set_pressure_advance(m_config.pressure_advance.get_at(initial_non_support_extruder_id))); + file.write(m_writer->set_pressure_advance(m_config.pressure_advance.get_at(initial_non_support_extruder_id))); // Orca: Adaptive PA // Reset Adaptive PA processor last PA value m_pa_processor->resetPreviousPA(m_config.pressure_advance.get_at(initial_non_support_extruder_id)); @@ -3157,14 +3115,14 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Orca: when activate_air_filtration is set on any extruder, find and set the highest during_print_exhaust_fan_speed bool activate_air_filtration = false; int during_print_exhaust_fan_speed = 0; - for (const auto &extruder : m_writer.extruders()) { + for (const auto &extruder : m_writer->extruders()) { activate_air_filtration |= m_config.activate_air_filtration.get_at(extruder.id()); if (m_config.activate_air_filtration.get_at(extruder.id())) during_print_exhaust_fan_speed = std::max(during_print_exhaust_fan_speed, m_config.during_print_exhaust_fan_speed.get_at(extruder.id())); } if (activate_air_filtration) - file.write(m_writer.set_exhaust_fan(during_print_exhaust_fan_speed, true)); + file.write(m_writer->set_exhaust_fan(during_print_exhaust_fan_speed, true)); print.throw_if_canceled(); @@ -3180,7 +3138,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_seam_placer.init(print, throw_if_canceled_func); // BBS: get path for change filament - if (m_writer.multiple_extruders) { + if (m_writer->multiple_extruders) { std::vector points = get_path_of_change_filament(print); if (points.size() == 3) { travel_point_1 = points[0]; @@ -3212,12 +3170,12 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato std::string gcode; gcode += ";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Layer_Change) + "\n"; if ((print.default_object_config().outer_wall_acceleration.value > 0 && print.default_object_config().outer_wall_acceleration.value > 0)) { - gcode += m_writer.set_print_acceleration((unsigned int)floor(print.default_object_config().outer_wall_acceleration.value + 0.5)); + gcode += m_writer->set_print_acceleration((unsigned int)floor(print.default_object_config().outer_wall_acceleration.value + 0.5)); } if (print.default_object_config().outer_wall_jerk.value > 0) { double jerk = print.default_object_config().outer_wall_jerk.value; - gcode += m_writer.set_jerk_xy(jerk); + gcode += m_writer->set_jerk_xy(jerk); } auto params = print.calib_params(); @@ -3261,8 +3219,8 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } print.throw_if_canceled(); this->set_origin(unscale((*print_object_instance_sequential_active)->shift)); - this->update_origin_snap((*print_object_instance_sequential_active)->print_object, - (*print_object_instance_sequential_active)->shift); + this->on_set_origin((*print_object_instance_sequential_active)->print_object, + (*print_object_instance_sequential_active)->shift); // BBS: prime extruder if extruder change happens before this object instance bool prime_extruder = false; @@ -3272,7 +3230,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_enable_cooling_markers = false; // we're not filtering these moves through CoolingBuffer m_avoid_crossing_perimeters.use_external_mp_once(); // BBS. change tool before moving to origin point. - if (m_writer.need_toolchange(initial_extruder_id)) { + if (m_writer->need_toolchange(initial_extruder_id)) { const PrintObjectConfig& object_config = object.config(); coordf_t initial_layer_print_height = print.config().initial_layer_print_height.value; @@ -3285,7 +3243,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } else { file.write(this->retract()); } - file.write(m_writer.travel_to_z(m_max_layer_z + m_writer.config.z_hop.get_at(initial_extruder_id))); + file.write(m_writer->travel_to_z(m_max_layer_z + m_writer->config.z_hop.get_at(initial_extruder_id))); file.write(this->travel_to(Point(0, 0), erNone, "move to origin position for next object")); m_enable_cooling_markers = true; // Disable motion planner when traveling to first object point. @@ -3308,7 +3266,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // and export G-code into file. tool_ordering.cal_most_used_extruder(print.config()); m_printed_objects.emplace_back(&object); - this->process_layers(print, tool_ordering, collect_layers_to_print(object), *print_object_instance_sequential_active - object.instances().data(), file, + this->process_layers(print, tool_ordering, collect_layers_to_print(object, print.config().belt_printer.value), *print_object_instance_sequential_active - object.instances().data(), file, prime_extruder); { // save the flush statitics stored in tool ordering by object @@ -3325,7 +3283,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato { const auto plr_mode = print.config().enable_power_loss_recovery.value; if (m_second_layer_things_done && plr_mode == PowerLossRecoveryMode::Enable) { - file.write(m_writer.enable_power_loss_recovery(PowerLossRecoveryMode::Disable)); + file.write(m_writer->enable_power_loss_recovery(PowerLossRecoveryMode::Disable)); } } ++ finished_objects; @@ -3346,7 +3304,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_wipe_tower->set_wipe_tower_bbx(print.get_wipe_tower_bbx()); m_wipe_tower->set_rib_offset(print.get_rib_offset()); //BBS - file.write(m_writer.travel_to_z(initial_layer_print_height + m_config.z_offset.value, "Move to the first layer height")); + file.write(m_writer->travel_to_z(initial_layer_print_height + m_config.z_offset.value, "Move to the first layer height")); if (wipe_tower_type == WipeTowerType::Type2 && print.config().single_extruder_multi_material_priming) { file.write(m_wipe_tower->prime(*this)); @@ -3405,7 +3363,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Orca: disable power loss recovery if (m_second_layer_things_done && print.config().enable_power_loss_recovery.value == PowerLossRecoveryMode::Enable) { - file.write(m_writer.enable_power_loss_recovery(PowerLossRecoveryMode::Disable)); + file.write(m_writer->enable_power_loss_recovery(PowerLossRecoveryMode::Disable)); } if (m_wipe_tower) // Purge the extruder, pull out the active filament. @@ -3419,14 +3377,14 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // if needed, write the gcode_label_objects_end { std::string gcode; - m_writer.add_object_change_labels(gcode); + m_writer->add_object_change_labels(gcode); file.write(gcode); } - file.write(m_writer.set_fan(0)); + file.write(m_writer->set_fan(0)); //BBS: make sure the additional fan is closed when end if(m_config.auxiliary_fan.value) - file.write(m_writer.set_additional_fan(0)); + file.write(m_writer->set_additional_fan(0)); if (is_bbl_printers) { //BBS: close spaghetti detector //Note: M981 is also used to tell xcam the last layer is finished, so we need always send it even if spaghetti option is disabled. @@ -3442,12 +3400,12 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato DynamicConfig config; config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index)); //BBS - config.set_key_value("layer_z", new ConfigOptionFloat(m_writer.get_position()(2) - m_config.z_offset.value)); + config.set_key_value("layer_z", new ConfigOptionFloat(m_writer->get_position()(2) - m_config.z_offset.value)); config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); if (print.config().single_extruder_multi_material) { // Process the filament_end_gcode for the active filament only. - int extruder_id = m_writer.filament()->id(); + int extruder_id = m_writer->filament()->id(); config.set_key_value("filament_extruder_id", new ConfigOptionInt(extruder_id)); file.writeln(this->placeholder_parser_process("filament_end_gcode", print.config().filament_end_gcode.get_at(extruder_id), extruder_id, &config)); } else { @@ -3457,20 +3415,20 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.writeln(this->placeholder_parser_process("filament_end_gcode", end_gcode, extruder_id, &config)); } } - file.writeln(this->placeholder_parser_process("machine_end_gcode", print.config().machine_end_gcode, m_writer.filament()->id(), &config)); + file.writeln(this->placeholder_parser_process("machine_end_gcode", print.config().machine_end_gcode, m_writer->filament()->id(), &config)); } - file.write(m_writer.update_progress(m_layer_count, m_layer_count, true)); // 100% - file.write(m_writer.postamble()); + file.write(m_writer->update_progress(m_layer_count, m_layer_count, true)); // 100% + file.write(m_writer->postamble()); if (activate_chamber_temp_control && max_chamber_temp > 0) - file.write(m_writer.set_chamber_temperature(0, false)); //close chamber_temperature + file.write(m_writer->set_chamber_temperature(0, false)); //close chamber_temperature if (activate_air_filtration) { int complete_print_exhaust_fan_speed = 0; - for (const auto& extruder : m_writer.extruders()) + for (const auto& extruder : m_writer->extruders()) if (m_config.activate_air_filtration.get_at(extruder.id())) complete_print_exhaust_fan_speed = std::max(complete_print_exhaust_fan_speed, m_config.complete_print_exhaust_fan_speed.get_at(extruder.id())); - file.write(m_writer.set_exhaust_fan(complete_print_exhaust_fan_speed, true)); + file.write(m_writer->set_exhaust_fan(complete_print_exhaust_fan_speed, true)); } // adds tags for time estimators file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Last_Line_M73_Placeholder).c_str()); @@ -3482,7 +3440,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.write(DoExport::update_print_stats_and_format_filament_stats( // Const inputs has_wipe_tower, print.wipe_tower_data(), - m_writer.extruders(), + m_writer->extruders(), // Modifies print.m_print_statistics)); print.m_print_statistics.initial_tool = initial_extruder_id; @@ -3656,7 +3614,7 @@ void GCode::process_layers( ); const auto fan_mover = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, - [&fan_mover = this->m_fan_mover, &config = this->config(), &writer = this->m_writer](std::string in)->std::string { + [&fan_mover = this->m_fan_mover, &config = this->config(), &writer = *this->m_writer](std::string in)->std::string { CNumericLocalesSetter locales_setter; @@ -3756,7 +3714,7 @@ void GCode::process_layers( ); const auto fan_mover = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, - [&fan_mover = this->m_fan_mover, &config = this->config(), &writer = this->m_writer](std::string in)->std::string { + [&fan_mover = this->m_fan_mover, &config = this->config(), &writer = *this->m_writer](std::string in)->std::string { if (config.fan_speedup_time.value != 0 || config.fan_kickstart.value > 0) { if (fan_mover.get() == nullptr) @@ -3824,21 +3782,21 @@ std::string GCode::placeholder_parser_process(const std::string &name, const std PlaceholderParserIntegration &ppi = m_placeholder_parser_integration; try { - ppi.update_from_gcodewriter(m_writer); + ppi.update_from_gcodewriter(*m_writer); std::string output = ppi.parser.process(templ, current_filament_id, config_override, &ppi.output_config, &ppi.context); ppi.validate_output_vector_variables(); if (const std::vector &pos = ppi.opt_position->values; ppi.position != pos) { // Update G-code writer. - m_writer.set_position({ pos[0], pos[1], pos[2] }); + m_writer->set_position({ pos[0], pos[1], pos[2] }); this->set_last_pos(this->gcode_to_point({ pos[0], pos[1] })); } - for (const Extruder &e : m_writer.extruders()) { + for (const Extruder &e : m_writer->extruders()) { unsigned int eid = e.id(); assert(eid < ppi.num_extruders); if ( eid < ppi.num_extruders) { - if (! m_writer.config.use_relative_e_distances && ! is_approx(ppi.e_position[eid], ppi.opt_e_position->values[eid])) + if (! m_writer->config.use_relative_e_distances && ! is_approx(ppi.e_position[eid], ppi.opt_e_position->values[eid])) const_cast(e).set_position(ppi.opt_e_position->values[eid]); if (! is_approx(ppi.e_retracted[eid], ppi.opt_e_retracted->values[eid]) || ! is_approx(ppi.e_restart_extra[eid], ppi.opt_e_restart_extra->values[eid])) @@ -3963,9 +3921,9 @@ void GCode::_print_first_layer_bed_temperature(GCodeOutputStream &file, Print &p temp = temp_by_gcode; #endif - // Always call m_writer.set_bed_temperature() so it will set the internal "current" state of the bed temp as if + // Always call m_writer->set_bed_temperature() so it will set the internal "current" state of the bed temp as if // the custom start G-code emited these. - std::string set_temp_gcode = m_writer.set_bed_temperature(bed_temp, wait); + std::string set_temp_gcode = m_writer->set_bed_temperature(bed_temp, wait); if (! temp_set_by_gcode) file.write(set_temp_gcode); } @@ -3985,14 +3943,14 @@ void GCode::_print_first_layer_extruder_temperatures(GCodeOutputStream &file, Pr int temp = print.config().nozzle_temperature_initial_layer.get_at(first_printing_extruder_id); if (temp_by_gcode >= 0 && temp_by_gcode < 1000) temp = temp_by_gcode; - m_writer.set_temperature(temp, wait, first_printing_extruder_id); + m_writer->set_temperature(temp, wait, first_printing_extruder_id); } else { // Custom G-code does not set the extruder temperature. Do it now. if (print.config().single_extruder_multi_material.value) { // Set temperature of the first printing extruder only. int temp = print.config().nozzle_temperature_initial_layer.get_at(first_printing_extruder_id); if (temp > 0) - file.write(m_writer.set_temperature(temp, wait, first_printing_extruder_id)); + file.write(m_writer->set_temperature(temp, wait, first_printing_extruder_id)); } else { // Set temperatures of all the printing extruders. for (unsigned int tool_id : print.extruders()) { @@ -4004,7 +3962,7 @@ void GCode::_print_first_layer_extruder_temperatures(GCodeOutputStream &file, Pr temp = print.config().idle_temperature.get_at(tool_id); } if (temp > 0) - file.write(m_writer.set_temperature(temp, wait, tool_id)); + file.write(m_writer->set_temperature(temp, wait, tool_id)); } } } @@ -4427,7 +4385,7 @@ LayerResult GCode::process_layer( //support is attached above the object, and support layers has independent layer height, then the lowest support //interface layer id is 0. bool first_layer = (layer.id() == 0 && abs(layer.bottom_z()) < EPSILON); - m_writer.set_is_first_layer(first_layer); + m_writer->set_is_first_layer(first_layer); unsigned int first_extruder_id = layer_tools.extruders.front(); // Initialize config with the 1st object to be printed at this layer. @@ -4477,7 +4435,7 @@ LayerResult GCode::process_layer( config.set_key_value("layer_z", new ConfigOptionFloat(print_z)); config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); gcode += this->placeholder_parser_process("before_layer_change_gcode", - print.config().before_layer_change_gcode.value, m_writer.filament()->id(), &config) + print.config().before_layer_change_gcode.value, m_writer->filament()->id(), &config) + "\n"; } @@ -4512,7 +4470,7 @@ LayerResult GCode::process_layer( config.set_key_value("layer_z", new ConfigOptionFloat(print_z)); config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); gcode += this->placeholder_parser_process("timelapse_gcode", - print.config().time_lapse_gcode.value, m_writer.filament()->id(), &config) + print.config().time_lapse_gcode.value, m_writer->filament()->id(), &config) + "\n"; } @@ -4522,7 +4480,7 @@ LayerResult GCode::process_layer( config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index)); config.set_key_value("layer_z", new ConfigOptionFloat(print_z)); gcode += this->placeholder_parser_process("layer_change_gcode", - print.config().layer_change_gcode.value, m_writer.filament()->id(), &config) + print.config().layer_change_gcode.value, m_writer->filament()->id(), &config) + "\n"; config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); } @@ -4562,7 +4520,7 @@ LayerResult GCode::process_layer( case CalibMode::Calib_Input_shaping_freq: { if (m_layer_index == 1){ gcode += writer().set_input_shaping('A', print.calib_params().start, 0.f, print.calib_params().shaper_type); - if (m_writer.get_gcode_flavor() == gcfKlipper) { + if (m_writer->get_gcode_flavor() == gcfKlipper) { // Disable minimum cruise ratio to ensure consistent motion for calibration gcode += "SET_VELOCITY_LIMIT MINIMUM_CRUISE_RATIO=0\n"; } @@ -4578,7 +4536,7 @@ LayerResult GCode::process_layer( } case CalibMode::Calib_Input_shaping_damp: { if (m_layer_index == 1){ - if (m_writer.get_gcode_flavor() == gcfKlipper) { + if (m_writer->get_gcode_flavor() == gcfKlipper) { // Disable minimum cruise ratio to ensure consistent motion for calibration gcode += "SET_VELOCITY_LIMIT MINIMUM_CRUISE_RATIO=0\n"; } @@ -4590,7 +4548,7 @@ LayerResult GCode::process_layer( break; } case CalibMode::Calib_Cornering: { - if (m_writer.get_gcode_flavor() == gcfMarlinFirmware && + if (m_writer->get_gcode_flavor() == gcfMarlinFirmware && !m_config.machine_max_junction_deviation.values.empty() && m_config.machine_max_junction_deviation.values.front() > 0) { gcode += writer().set_junction_deviation(this->interpolate_value_across_layers(print.calib_params().start, print.calib_params().end)); @@ -4605,22 +4563,22 @@ LayerResult GCode::process_layer( if (first_layer) { // Orca: we don't need to optimize the Klipper as only set once if (m_config.default_acceleration.value > 0 && m_config.initial_layer_acceleration.value > 0) { - gcode += m_writer.set_print_acceleration((unsigned int)floor(m_config.initial_layer_acceleration.value + 0.5)); + gcode += m_writer->set_print_acceleration((unsigned int)floor(m_config.initial_layer_acceleration.value + 0.5)); } if (m_config.default_jerk.value > 0 && m_config.initial_layer_jerk.value > 0) { - gcode += m_writer.set_jerk_xy(m_config.initial_layer_jerk.value); + gcode += m_writer->set_jerk_xy(m_config.initial_layer_jerk.value); } - if (m_writer.get_gcode_flavor() == gcfMarlinFirmware && m_config.default_junction_deviation.value > 0) { - gcode += m_writer.set_junction_deviation(m_config.default_junction_deviation.value); + if (m_writer->get_gcode_flavor() == gcfMarlinFirmware && m_config.default_junction_deviation.value > 0) { + gcode += m_writer->set_junction_deviation(m_config.default_junction_deviation.value); } } if (!first_layer && !m_second_layer_things_done) { // Orca: set power loss recovery const auto plr_mode = print.config().enable_power_loss_recovery.value; - gcode += m_writer.enable_power_loss_recovery(plr_mode); + gcode += m_writer->enable_power_loss_recovery(plr_mode); if (print.is_BBL_printer()) { // BBS: open first layer inspection at second layer @@ -4635,23 +4593,23 @@ LayerResult GCode::process_layer( // Reset acceleration at sencond layer // Orca: only set once, don't need to call set_accel_and_jerk if (m_config.default_acceleration.value > 0 && m_config.initial_layer_acceleration.value > 0) { - gcode += m_writer.set_print_acceleration((unsigned int) floor(m_config.default_acceleration.value + 0.5)); + gcode += m_writer->set_print_acceleration((unsigned int) floor(m_config.default_acceleration.value + 0.5)); } if (m_config.default_jerk.value > 0 && m_config.initial_layer_jerk.value > 0) { - gcode += m_writer.set_jerk_xy(m_config.default_jerk.value); + gcode += m_writer->set_jerk_xy(m_config.default_jerk.value); } // Transition from 1st to 2nd layer. Adjust nozzle temperatures as prescribed by the nozzle dependent // nozzle_temperature_initial_layer vs. temperature settings. - for (const Extruder& extruder : m_writer.extruders()) { + for (const Extruder& extruder : m_writer->extruders()) { if ((print.config().single_extruder_multi_material.value || m_ooze_prevention.enable) && - extruder.id() != m_writer.filament()->id()) + extruder.id() != m_writer->filament()->id()) // In single extruder multi material mode, set the temperature for the current extruder only. continue; int temperature = print.config().nozzle_temperature.get_at(extruder.id()); if (temperature > 0 && temperature != print.config().nozzle_temperature_initial_layer.get_at(extruder.id())) - gcode += m_writer.set_temperature(temperature, false, extruder.id()); + gcode += m_writer->set_temperature(temperature, false, extruder.id()); } // BBS @@ -4660,14 +4618,14 @@ LayerResult GCode::process_layer( bed_temp = get_highest_bed_temperature(false,print); else bed_temp = get_bed_temperature(first_extruder_id, false, m_config.curr_bed_type); - gcode += m_writer.set_bed_temperature(bed_temp); + gcode += m_writer->set_bed_temperature(bed_temp); // Mark the temperature transition from 1st to 2nd layer to be finished. m_second_layer_things_done = true; } if (single_object_instance_idx == size_t(-1)) { // Normal (non-sequential) print. - gcode += ProcessLayer::emit_custom_gcode_per_print_z(*this, layer_tools.custom_gcode, m_writer.filament()->id(), first_extruder_id, print.config()); + gcode += ProcessLayer::emit_custom_gcode_per_print_z(*this, layer_tools.custom_gcode, m_writer->filament()->id(), first_extruder_id, print.config()); } // BBS: get next extruder according to flush and soluble @@ -4958,9 +4916,9 @@ LayerResult GCode::process_layer( auto insert_timelapse_gcode = [this, print_z, &print, &most_used_extruder, &layer_object_label_ids,&printed_objects = std::as_const(m_printed_objects)]() -> std::string { PosPickCtx ctx; - ctx.curr_pos = { (coord_t)(scale_(m_writer.get_position().x())),(coord_t)(scale_(m_writer.get_position().y())) }; + ctx.curr_pos = { (coord_t)(scale_(m_writer->get_position().x())),(coord_t)(scale_(m_writer->get_position().y())) }; ctx.curr_layer = this->layer(); - ctx.curr_extruder_id = m_writer.filament()->extruder_id(); + ctx.curr_extruder_id = m_writer->filament()->extruder_id(); ctx.picture_extruder_id = most_used_extruder; if (m_config.print_sequence == PrintSequence::ByObject) ctx.printed_objects = printed_objects; @@ -4978,17 +4936,17 @@ LayerResult GCode::process_layer( config.set_key_value("timelapse_pos_x", new ConfigOptionInt((int)timelapse_pos.x())); config.set_key_value("timelapse_pos_y", new ConfigOptionInt((int)timelapse_pos.y())); config.set_key_value("has_timelapse_safe_pos", new ConfigOptionBool(timelapse_pos != DefaultTimelapsePos)); - timelapse_gcode = this->placeholder_parser_process("timelapse_gcode", print.config().time_lapse_gcode.value, m_writer.filament()->id(), &config) + "\n"; + timelapse_gcode = this->placeholder_parser_process("timelapse_gcode", print.config().time_lapse_gcode.value, m_writer->filament()->id(), &config) + "\n"; } if (!timelapse_gcode.empty()) { - m_writer.set_current_position_clear(false); + m_writer->set_current_position_clear(false); double temp_z_after_tool_change; if (GCodeProcessor::get_last_z_from_gcode(timelapse_gcode, temp_z_after_tool_change)) { - Vec3d pos = m_writer.get_position(); + Vec3d pos = m_writer->get_position(); pos(2) = temp_z_after_tool_change; - m_writer.set_position(pos); + m_writer->set_position(pos); } } @@ -5034,16 +4992,16 @@ LayerResult GCode::process_layer( config.set_key_value("layer_z", new ConfigOptionFloat(print_z)); config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); config.set_key_value("most_used_physical_extruder_id", new ConfigOptionInt(m_config.physical_extruder_map.get_at(most_used_extruder))); - config.set_key_value("curr_physical_extruder_id", new ConfigOptionInt(m_config.physical_extruder_map.get_at(m_writer.filament()->extruder_id()))); - wrapping_gcode = this->placeholder_parser_process("wrapping_detection_gcode", print.config().wrapping_detection_gcode.value, m_writer.filament()->id(), &config) +"\n"; + config.set_key_value("curr_physical_extruder_id", new ConfigOptionInt(m_config.physical_extruder_map.get_at(m_writer->filament()->extruder_id()))); + wrapping_gcode = this->placeholder_parser_process("wrapping_detection_gcode", print.config().wrapping_detection_gcode.value, m_writer->filament()->id(), &config) +"\n"; } - m_writer.set_current_position_clear(false); + m_writer->set_current_position_clear(false); double temp_z_after_tool_change; if (GCodeProcessor::get_last_z_from_gcode(wrapping_gcode, temp_z_after_tool_change)) { - Vec3d pos = m_writer.get_position(); + Vec3d pos = m_writer->get_position(); pos(2) = temp_z_after_tool_change; - m_writer.set_position(pos); + m_writer->set_position(pos); } return wrapping_gcode; }; @@ -5109,7 +5067,7 @@ LayerResult GCode::process_layer( if (should_insert) { gcode += this->retract(false, false, auto_lift_type, true); - m_writer.add_object_change_labels(gcode); + m_writer->add_object_change_labels(gcode); gcode += insert_timelapse_gcode(); has_insert_timelapse_gcode = true; @@ -5126,12 +5084,12 @@ LayerResult GCode::process_layer( } else { if (need_insert_timelapse_gcode_for_traditional && !has_insert_timelapse_gcode && - m_writer.need_toolchange(extruder_id) && + m_writer->need_toolchange(extruder_id) && m_config.nozzle_diameter.values.size() == 2 && writer().filament() && (get_extruder_id(writer().filament()->id()) == most_used_extruder)) { gcode += this->retract(false, false, auto_lift_type, true); - m_writer.add_object_change_labels(gcode); + m_writer->add_object_change_labels(gcode); gcode += insert_timelapse_gcode(); has_insert_timelapse_gcode = true; @@ -5248,19 +5206,19 @@ LayerResult GCode::process_layer( // exclude objects if (m_enable_exclude_object) { if (is_BBL_Printer()) { - m_writer.set_object_start_str( + m_writer->set_object_start_str( std::string("; start printing object, unique label id: ") + std::to_string(instance_to_print.label_object_id) + "\n" + "M624 " + _encode_label_ids_to_base64({instance_to_print.label_object_id}) + "\n"); } else { const auto gflavor = print.config().gcode_flavor.value; if (gflavor == gcfKlipper) { - m_writer.set_object_start_str(std::string("EXCLUDE_OBJECT_START NAME=") + + m_writer->set_object_start_str(std::string("EXCLUDE_OBJECT_START NAME=") + get_instance_name(&instance_to_print.print_object, inst.id) + "\n"); } else if (gflavor == gcfMarlinLegacy || gflavor == gcfMarlinFirmware || gflavor == gcfRepRapFirmware) { std::string str = std::string("M486 S") + std::to_string(inst.unique_id) + "\n"; - m_writer.set_object_start_str(str); + m_writer->set_object_start_str(str); } } } @@ -5278,7 +5236,7 @@ LayerResult GCode::process_layer( m_avoid_crossing_perimeters.use_external_mp_once(); m_last_obj_copy = this_object_copy; this->set_origin(unscale(offset)); - this->update_origin_snap(&instance_to_print.print_object, offset); + this->on_set_origin(&instance_to_print.print_object, offset); if (instance_to_print.object_by_extruder.support != nullptr) { m_layer = layers[instance_to_print.layer_id].support_layer; m_object_layer_over_raft = false; @@ -5302,7 +5260,7 @@ LayerResult GCode::process_layer( m_avoid_crossing_perimeters.use_external_mp_once(); m_last_obj_copy = this_object_copy; this->set_origin(unscale(offset)); - this->update_origin_snap(&instance_to_print.print_object, offset); + this->on_set_origin(&instance_to_print.print_object, offset); ExtrusionEntityCollection support_eec; // BBS @@ -5351,7 +5309,7 @@ LayerResult GCode::process_layer( m_avoid_crossing_perimeters.use_external_mp_once(); m_last_obj_copy = this_object_copy; this->set_origin(unscale(offset)); - this->update_origin_snap(&instance_to_print.print_object, offset); + this->on_set_origin(&instance_to_print.print_object, offset); //FIXME the following code prints regions in the order they are defined, the path is not optimized in any way. auto has_infill = [](const std::vector &by_region) { @@ -5388,20 +5346,20 @@ LayerResult GCode::process_layer( } // exclude objects // Don't set m_gcode_label_objects_end if you don't had to write the m_gcode_label_objects_start. - if (!m_writer.is_object_start_str_empty()) { - m_writer.set_object_start_str(""); + if (!m_writer->is_object_start_str_empty()) { + m_writer->set_object_start_str(""); } else if (m_enable_exclude_object) { if (is_BBL_Printer()) { - m_writer.set_object_end_str(std::string("; stop printing object, unique label id: ") + + m_writer->set_object_end_str(std::string("; stop printing object, unique label id: ") + std::to_string(instance_to_print.label_object_id) + "\n" + "M625\n"); } else { const auto gflavor = print.config().gcode_flavor.value; if (gflavor == gcfKlipper) { - m_writer.set_object_end_str(std::string("EXCLUDE_OBJECT_END NAME=") + + m_writer->set_object_end_str(std::string("EXCLUDE_OBJECT_END NAME=") + get_instance_name(&instance_to_print.print_object, inst.id) + "\n"); } else if (gflavor == gcfMarlinLegacy || gflavor == gcfMarlinFirmware || gflavor == gcfRepRapFirmware) { - m_writer.set_object_end_str(std::string("M486 S-1\n")); + m_writer->set_object_end_str(std::string("M486 S-1\n")); } } } @@ -5453,7 +5411,7 @@ LayerResult GCode::process_layer( if (FILAMENT_CONFIG(retract_when_changing_layer)) { gcode += this->retract(false, false, auto_lift_type, true); } - m_writer.add_object_change_labels(gcode); + m_writer->add_object_change_labels(gcode); gcode += insert_timelapse_gcode(); } @@ -5465,7 +5423,7 @@ LayerResult GCode::process_layer( void GCode::apply_print_config(const PrintConfig &print_config) { - m_writer.apply_print_config(print_config); + m_writer->apply_print_config(print_config); m_config.apply(print_config); m_scaled_resolution = scaled(print_config.resolution.value); m_enable_exclude_object = m_config.exclude_object; @@ -5555,7 +5513,7 @@ void GCode::append_full_config(const Print &print, std::string &str) void GCode::set_extruders(const std::vector &extruder_ids) { - m_writer.set_extruders(extruder_ids); + m_writer->set_extruders(extruder_ids); // enable wipe path generation if any extruder has wipe enabled m_wipe.enable = false; @@ -5578,139 +5536,15 @@ void GCode::set_origin(const Vec2d &pointf) m_origin = pointf; } -void GCode::update_origin_snap(const PrintObject *obj, const Point &inst_shift) -{ - if (!m_origin_snap[0] && !m_origin_snap[1] && !m_origin_snap[2]) - return; - - // Clear existing snap so to_machine_coords gives raw machine coords for bbox computation. - for (int a = 0; a < 3; ++a) - m_writer.set_origin_snap(a, false, 0., 0.); - - // Reconstruct the belt pipeline transform for this object (same as - // PrintObjectSlice.cpp: z_shift * scale * shear * pre_remap). - const auto &cfg = m_config; - Transform3d belt = Transform3d::Identity(); - - // Pre-slice remap - int pre_rx = int(cfg.belt_preslice_remap_x.value); - int pre_ry = int(cfg.belt_preslice_remap_y.value); - int pre_rz = int(cfg.belt_preslice_remap_z.value); - if (pre_rx != int(BeltRemapAxis::PosX) || - pre_ry != int(BeltRemapAxis::PosY) || - pre_rz != int(BeltRemapAxis::PosZ)) { - auto remap_col = [](int r) -> Vec3d { - int a = r % 3; Vec3d c = Vec3d::Zero(); - c[a] = (r < 3) ? 1.0 : -1.0; - return c; - }; - Matrix3d lin; - lin.col(0) = remap_col(pre_rx); - lin.col(1) = remap_col(pre_ry); - lin.col(2) = remap_col(pre_rz); - Transform3d pre = Transform3d::Identity(); - pre.linear() = lin; - if (pre_rx >= 6 || pre_ry >= 6 || pre_rz >= 6) { - BoundingBoxf bb(cfg.printable_area.values); - Vec3d vm(bb.max.x(), bb.max.y(), cfg.printable_height.value); - Vec3d tr = Vec3d::Zero(); - if (pre_rx >= 6) tr[0] = vm[pre_rx % 3]; - if (pre_ry >= 6) tr[1] = vm[pre_ry % 3]; - if (pre_rz >= 6) tr[2] = vm[pre_rz % 3]; - pre.translation() = tr; - } - belt = pre * belt; - } - - // Shear - auto shear_f = [](BeltShearMode m, double a) -> double { - double r = Geometry::deg2rad(a), s = std::sin(r), c = std::cos(r); - switch (m) { - case BeltShearMode::PosCot: return (s > EPSILON) ? c/s : 0.; - case BeltShearMode::NegCot: return (s > EPSILON) ? -c/s : 0.; - case BeltShearMode::PosTan: return (c > EPSILON) ? s/c : 0.; - case BeltShearMode::NegTan: return (c > EPSILON) ? -s/c : 0.; - default: return 0.; - } - }; - struct AS { BeltShearMode m; double a; int f; }; - AS axes[3] = { - {cfg.belt_shear_x.value, cfg.belt_shear_x_angle.value, int(cfg.belt_shear_x_from.value)}, - {cfg.belt_shear_y.value, cfg.belt_shear_y_angle.value, int(cfg.belt_shear_y_from.value)}, - {cfg.belt_shear_z.value, cfg.belt_shear_z_angle.value, int(cfg.belt_shear_z_from.value)}, - }; - Transform3d shear = Transform3d::Identity(); - for (int i = 0; i < 3; ++i) - if (axes[i].m != BeltShearMode::None) { - double f = shear_f(axes[i].m, axes[i].a); - if (std::abs(f) > EPSILON) - shear.matrix()(i, axes[i].f) += f; - } - - // Scale - auto scale_f = [](BeltScaleMode m, double a) -> double { - if (m == BeltScaleMode::None) return 1.; - double r = Geometry::deg2rad(a), s = std::sin(r), c = std::cos(r); - switch (m) { - case BeltScaleMode::InvSin: return (s > EPSILON) ? 1./s : 1.; - case BeltScaleMode::InvCos: return (c > EPSILON) ? 1./c : 1.; - case BeltScaleMode::Sin: return s; - case BeltScaleMode::Cos: return c; - default: return 1.; - } - }; - Transform3d sc = Transform3d::Identity(); - sc.matrix()(0,0) = scale_f(cfg.belt_scale_x.value, cfg.belt_scale_x_angle.value); - sc.matrix()(1,1) = scale_f(cfg.belt_scale_y.value, cfg.belt_scale_y_angle.value); - sc.matrix()(2,2) = scale_f(cfg.belt_scale_z.value, cfg.belt_scale_z_angle.value); - - belt = sc * shear * belt; - - // Z-shift - double zs = (obj->belt_min_z() < 0.) ? -obj->belt_min_z() : 0.; - if (zs > 0.) { - Transform3d zsh = Transform3d::Identity(); - zsh.matrix()(2, 3) = zs; - belt = zsh * belt; - } - - // Full transform: belt * trafo_centered - Transform3d full = belt * obj->trafo_centered(); - - // Instance shift in slicer space + global Z offset - Vec3d shift(unscale(inst_shift.x()), - unscale(inst_shift.y()), - obj->belt_global_z_offset()); - - // Compute this instance's machine-space bbox min - BoundingBoxf3 bb = obj->model_object()->raw_bounding_box(); - Vec3d mn = bb.min.cast(), mx = bb.max.cast(); - Vec3d inst_min(std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max()); - 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 mc = m_writer.to_machine_coords(full * c + shift); - for (int a = 0; a < 3; ++a) - inst_min[a] = std::min(inst_min[a], mc[a]); - } - - // Update writer snap for each enabled axis - for (int a = 0; a < 3; ++a) - m_writer.set_origin_snap(a, m_origin_snap[a], m_origin_snap_offset[a], inst_min[a]); -} - std::string GCode::preamble() { - std::string gcode = m_writer.preamble(); + std::string gcode = m_writer->preamble(); /* Perform a *silent* move to z_offset: we need this to initialize the Z position of our writer object so that any initial lift taking place before the first layer change will raise the extruder from the correct initial Z instead of 0. */ - m_writer.travel_to_z(m_config.z_offset.value); + m_writer->travel_to_z(m_config.z_offset.value); return gcode; } @@ -5721,28 +5555,28 @@ std::string GCode::change_layer(coordf_t print_z) std::string gcode; if (m_layer_count > 0) // Increment a progress bar indicator. - gcode += m_writer.update_progress(++ m_layer_index, m_layer_count); + gcode += m_writer->update_progress(++ m_layer_index, m_layer_count); //BBS coordf_t z = print_z + m_config.z_offset.value; // in unscaled coordinates - if (FILAMENT_CONFIG(retract_when_changing_layer) && m_writer.will_move_z(z)) { + if (FILAMENT_CONFIG(retract_when_changing_layer) && m_writer->will_move_z(z)) { LiftType lift_type = this->to_lift_type(ZHopType(FILAMENT_CONFIG(z_hop_types))); //BBS: force to use SpiralLift when change layer if lift type is auto gcode += this->retract(false, false, ZHopType(FILAMENT_CONFIG(z_hop_types)) == ZHopType::zhtAuto ? LiftType::SpiralLift : lift_type); } - m_writer.add_object_change_labels(gcode); + m_writer->add_object_change_labels(gcode); if (m_spiral_vase) { //BBS: force to normal lift immediately in spiral vase mode std::ostringstream comment; comment << "move to next layer (" << m_layer_index << ")"; - gcode += m_writer.travel_to_z(z, comment.str()); + gcode += m_writer->travel_to_z(z, comment.str()); } m_need_change_layer_lift_z = true; m_nominal_z = z; - m_writer.get_position().z() = z; + m_writer->get_position().z() = z; // forget last wiping path as wiping after raising Z is pointless // BBS. Dont forget wiping path to reduce stringing. @@ -6044,7 +5878,7 @@ std::string GCode::extrude_loop(ExtrusionLoop loop, std::string description, dou //Point pt = ((nd * nd >= l2) ? (p1+v*0.4): (p1 + 0.2 * v * (nd / sqrt(l2)))).cast(); pt.rotate(angle, paths.front().polyline.points.front()); // generate the travel move - gcode += m_writer.extrude_to_xy(this->point_to_gcode(pt), 0,"move inwards before travel",true); + gcode += m_writer->extrude_to_xy(this->point_to_gcode(pt), 0,"move inwards before travel",true); } return gcode; @@ -6367,7 +6201,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, bool slope_need_z_travel = false; if (sloped != nullptr && !sloped->is_flat()) { auto target_z = get_sloped_z(sloped->slope_begin.z_ratio); - slope_need_z_travel = m_writer.will_move_z(target_z); + slope_need_z_travel = m_writer->will_move_z(target_z); } // Move to first point of extrusion path // path is 2D. But in slope lift case, lift z is done in travel_to function. @@ -6390,7 +6224,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // if needed, write the gcode_label_objects_end then gcode_label_objects_start // should be already done by travel_to, but just in case - m_writer.add_object_change_labels(gcode); + m_writer->add_object_change_labels(gcode); // compensate retraction gcode += this->unretract(); @@ -6444,12 +6278,12 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } } - if (m_writer.get_gcode_flavor() == gcfKlipper) { - gcode += m_writer.set_accel_and_jerk(acceleration_i, jerk); + if (m_writer->get_gcode_flavor() == gcfKlipper) { + gcode += m_writer->set_accel_and_jerk(acceleration_i, jerk); } else { - gcode += m_writer.set_print_acceleration(acceleration_i); - gcode += m_writer.set_jerk_xy(jerk); + gcode += m_writer->set_print_acceleration(acceleration_i); + gcode += m_writer->set_jerk_xy(jerk); } // calculate effective extrusion length per distance unit (e_per_mm) @@ -6494,8 +6328,8 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } // Effective extrusion length per distance unit = (filament_flow_ratio/cross_section) * mm3_per_mm / print flow ratio - // m_writer.extruder()->e_per_mm3() below is (filament flow ratio / cross-sectional area) - double e_per_mm = m_writer.filament()->e_per_mm3() * _mm3_per_mm; + // m_writer->extruder()->e_per_mm3() below is (filament flow ratio / cross-sectional area) + double e_per_mm = m_writer->filament()->e_per_mm3() * _mm3_per_mm; e_per_mm /= filament_flow_ratio; // set speed @@ -6721,7 +6555,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index + 1)); config.set_key_value("layer_z", new ConfigOptionFloat(m_layer == nullptr ? m_last_height : m_layer->print_z)); gcode += this->placeholder_parser_process("change_extrusion_role_gcode", - m_config.change_extrusion_role_gcode.value, m_writer.filament()->id(), &config) + m_config.change_extrusion_role_gcode.value, m_writer->filament()->id(), &config) + "\n"; } @@ -6776,7 +6610,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, if (m_multi_flow_segment_path_average_mm3_per_mm > 0) { sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer.filament()->id(), + m_writer->filament()->id(), m_multi_flow_segment_path_average_mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -6789,7 +6623,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // to issue a zero flow PA change command for this sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer.filament()->id(), + m_writer->filament()->id(), _mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -6895,7 +6729,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer.filament()->id(), + m_writer->filament()->id(), _mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -6910,7 +6744,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer.filament()->id(), + m_writer->filament()->id(), _mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -6925,7 +6759,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // ORCA: End of adaptive PA code segment } - gcode += m_writer.set_speed(F, "", comment); + gcode += m_writer->set_speed(F, "", comment); { if (m_enable_cooling_markers) { if (enable_overhang_bridge_fan) { @@ -6943,7 +6777,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } // BBS: use G1 if not enable arc fitting or has no arc fitting result or in spiral_mode mode or we are doing sloped extrusion // Attention: G2 and G3 is not supported in spiral_mode mode - if (!m_config.enable_arc_fitting || path.polyline.fitting_result.empty() || m_config.spiral_mode || sloped != nullptr || m_config.belt_printer) { + if (!m_config.enable_arc_fitting || path.polyline.fitting_result.empty() || m_config.spiral_mode || sloped != nullptr || this->should_disable_arc_fitting()) { double path_length = 0.; double total_length = sloped == nullptr ? 0. : path.polyline.length() * SCALING_FACTOR; for (const Line& line : path.polyline.lines()) { @@ -6963,7 +6797,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } if (sloped == nullptr) { // Normal extrusion - gcode += m_writer.extrude_to_xy( + gcode += m_writer->extrude_to_xy( this->point_to_gcode(line.b), dE, GCodeWriter::full_gcode_comment ? tempDescription : "", path.is_force_no_extrusion()); @@ -6972,7 +6806,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, const auto [z_ratio, e_ratio] = sloped->interpolate(path_length / total_length); Vec2d dest2d = this->point_to_gcode(line.b); Vec3d dest3d(dest2d(0), dest2d(1), get_sloped_z(z_ratio)); - gcode += m_writer.extrude_to_xyz( + gcode += m_writer->extrude_to_xyz( dest3d, dE * e_ratio, GCodeWriter::full_gcode_comment ? tempDescription : "", path.is_force_no_extrusion()); @@ -7002,7 +6836,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, tempDescription += Slic3r::format(" | Old Flow Value: %0.5f Length: %0.5f",oldE, line_length); } } - gcode += m_writer.extrude_to_xy( + gcode += m_writer->extrude_to_xy( this->point_to_gcode(line.b), dE, GCodeWriter::full_gcode_comment ? tempDescription : "", path.is_force_no_extrusion()); @@ -7025,7 +6859,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, tempDescription += Slic3r::format(" | Old Flow Value: %0.5f Length: %0.5f",oldE, arc_length); } } - gcode += m_writer.extrude_arc_to_xy( + gcode += m_writer->extrude_arc_to_xy( this->point_to_gcode(arc.end_point), center_offset, dE, @@ -7053,7 +6887,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, Polyline l(p); total_length = l.length() * SCALING_FACTOR; } - gcode += m_writer.set_speed(last_set_speed, "", comment); + gcode += m_writer->set_speed(last_set_speed, "", comment); Vec2d prev = this->point_to_gcode_quantized(new_points[0].p); bool pre_fan_enabled = false; bool cur_fan_enabled = false; @@ -7103,7 +6937,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer.filament()->id(), + m_writer->filament()->id(), _mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -7118,7 +6952,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer.filament()->id(), + m_writer->filament()->id(), _mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -7135,10 +6969,10 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // Ignore small speed variations - emit speed change if the delta between current and new is greater than 60mm/min / 1mm/sec // Reset speed to F if delta to F is less than 1mm/sec if ((std::abs(last_set_speed - new_speed) > 60)) { - gcode += m_writer.set_speed(new_speed, "", comment); + gcode += m_writer->set_speed(new_speed, "", comment); last_set_speed = new_speed; } else if ((std::abs(F - new_speed) <= 60)) { - gcode += m_writer.set_speed(F, "", comment); + gcode += m_writer->set_speed(F, "", comment); last_set_speed = F; } auto dE = e_per_mm * line_length; @@ -7152,12 +6986,12 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } if (sloped == nullptr) { // Normal extrusion - gcode += m_writer.extrude_to_xy(p, dE, GCodeWriter::full_gcode_comment ? tempDescription : ""); + gcode += m_writer->extrude_to_xy(p, dE, GCodeWriter::full_gcode_comment ? tempDescription : ""); } else { // Sloped extrusion const auto [z_ratio, e_ratio] = sloped->interpolate(path_length / total_length); Vec3d dest3d(p(0), p(1), get_sloped_z(z_ratio)); - gcode += m_writer.extrude_to_xyz(dest3d, dE * e_ratio, GCodeWriter::full_gcode_comment ? tempDescription : ""); + gcode += m_writer->extrude_to_xyz(dest3d, dE * e_ratio, GCodeWriter::full_gcode_comment ? tempDescription : ""); } prev = p; @@ -7298,18 +7132,18 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string } } - if (m_writer.get_gcode_flavor() == gcfKlipper) { - gcode += m_writer.set_accel_and_jerk(acceleration_to_set, jerk_to_set); + if (m_writer->get_gcode_flavor() == gcfKlipper) { + gcode += m_writer->set_accel_and_jerk(acceleration_to_set, jerk_to_set); } else { - gcode += m_writer.set_travel_acceleration(acceleration_to_set); - gcode += m_writer.set_jerk_xy(jerk_to_set); + gcode += m_writer->set_travel_acceleration(acceleration_to_set); + gcode += m_writer->set_jerk_xy(jerk_to_set); } // if a retraction would be needed, try to use reduce_crossing_wall to plan a // multi-hop travel path inside the configuration space if (m_config.reduce_crossing_wall && !m_avoid_crossing_perimeters.disabled_once() - && m_writer.is_current_position_clear()) + && m_writer->is_current_position_clear()) //BBS: don't generate detour travel paths when current position is unclea { travel = m_avoid_crossing_perimeters.travel_to(*this, point, &could_be_wipe_disabled); @@ -7356,7 +7190,7 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string // if needed, write the gcode_label_objects_end then gcode_label_objects_start - m_writer.add_object_change_labels(gcode); + m_writer->add_object_change_labels(gcode); // use G1 because we rely on paths being straight (G0 may make round paths) if (travel.size() >= 2) { @@ -7364,14 +7198,14 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string if (false/*m_spiral_vase*/) { // No lazy z lift for spiral vase mode for (size_t i = 1; i < travel.size(); ++i) { - gcode += m_writer.travel_to_xy(this->point_to_gcode(travel.points[i]), comment); + gcode += m_writer->travel_to_xy(this->point_to_gcode(travel.points[i]), comment); } } else { if (travel.size() == 2) { // No extra movements emitted by avoid_crossing_perimeters, simply move to the end point with z change const auto& dest2d = this->point_to_gcode(travel.points.back()); Vec3d dest3d(dest2d(0), dest2d(1), z == DBL_MAX ? m_nominal_z : z); - gcode += m_writer.travel_to_xyz(dest3d, comment, m_need_change_layer_lift_z); + gcode += m_writer->travel_to_xyz(dest3d, comment, m_need_change_layer_lift_z); m_need_change_layer_lift_z = false; } else { // Extra movements emitted by avoid_crossing_perimeters, lift the z to normal height at the beginning, then apply the z @@ -7381,16 +7215,16 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string // Lift to normal z at beginning Vec2d dest2d = this->point_to_gcode(travel.points[i]); Vec3d dest3d(dest2d(0), dest2d(1), m_nominal_z); - gcode += m_writer.travel_to_xyz(dest3d, comment, m_need_change_layer_lift_z); + gcode += m_writer->travel_to_xyz(dest3d, comment, m_need_change_layer_lift_z); m_need_change_layer_lift_z = false; } else if (z != DBL_MAX && i == travel.size() - 1) { // Apply z_ratio for the very last point Vec2d dest2d = this->point_to_gcode(travel.points[i]); Vec3d dest3d(dest2d(0), dest2d(1), z); - gcode += m_writer.travel_to_xyz(dest3d, comment); + gcode += m_writer->travel_to_xyz(dest3d, comment); } else { // For all points in between, no z change - gcode += m_writer.travel_to_xy(this->point_to_gcode(travel.points[i]), comment); + gcode += m_writer->travel_to_xy(this->point_to_gcode(travel.points[i]), comment); } } } @@ -7493,7 +7327,7 @@ bool GCode::needs_retraction(const Polyline &travel, ExtrusionRole role, LiftTyp if (clipped_travel.length() > travel_len_thresh) clipped_travel.points.back() = clipped_travel.points.front()+(clipped_travel.points.back() - clipped_travel.points.front()) * (travel_len_thresh / clipped_travel.length()); //BBS: translate to current plate coordinate system - clipped_travel.translate(Point::new_scale(double(m_origin.x() - m_writer.get_xy_offset().x()), double(m_origin.y() - m_writer.get_xy_offset().y()))); + clipped_travel.translate(Point::new_scale(double(m_origin.x() - m_writer->get_xy_offset().x()), double(m_origin.y() - m_writer->get_xy_offset().y()))); //BBS: force to retract when leave from external perimeter for a long travel //Better way is judging whether the travel move direction is same with last extrusion move. @@ -7545,13 +7379,13 @@ std::string GCode::retract(bool toolchange, bool is_last_retraction, LiftType li { std::string gcode; - if (m_writer.filament() == nullptr) + if (m_writer->filament() == nullptr) return gcode; // wipe (if it's enabled for this extruder and we have a stored wipe path and no-zero wipe distance) if (FILAMENT_CONFIG(wipe) && m_wipe.has_path() && scale_(FILAMENT_CONFIG(wipe_distance)) > SCALED_EPSILON) { Wipe::RetractionValues wipeRetractions = m_wipe.calculateWipeRetractionLengths(*this, toolchange); - gcode += toolchange ? m_writer.retract_for_toolchange(true,wipeRetractions.retractLengthBeforeWipe) : m_writer.retract(true, wipeRetractions.retractLengthBeforeWipe); + gcode += toolchange ? m_writer->retract_for_toolchange(true,wipeRetractions.retractLengthBeforeWipe) : m_writer->retract(true, wipeRetractions.retractLengthBeforeWipe); gcode += m_wipe.wipe(*this,wipeRetractions.retractLengthDuringWipe, toolchange, is_last_retraction); } @@ -7561,14 +7395,14 @@ std::string GCode::retract(bool toolchange, bool is_last_retraction, LiftType li length is honored in case wipe path was too short. */ if ((!this->on_first_layer() || this->config().bottom_surface_pattern != InfillPattern::ipHilbertCurve) && (role != erTopSolidInfill || this->config().top_surface_pattern != InfillPattern::ipHilbertCurve)) - gcode += toolchange ? m_writer.retract_for_toolchange() : m_writer.retract(); + gcode += toolchange ? m_writer->retract_for_toolchange() : m_writer->retract(); - gcode += m_writer.reset_e(); + gcode += m_writer->reset_e(); // Orca: check if should + can lift (roughly from SuperSlicer) RetractLiftEnforceType retract_lift_type = RetractLiftEnforceType(EXTRUDER_CONFIG(retract_lift_enforce)); bool needs_lift = toolchange - || m_writer.filament()->retraction_length() > 0 + || m_writer->filament()->retraction_length() > 0 || m_config.use_firmware_retraction; bool last_fill_extrusion_role_top_infill = (this->m_last_notgapfill_extrusion_role == ExtrusionRole::erTopSolidInfill || this->m_last_notgapfill_extrusion_role == ExtrusionRole::erIroning); @@ -7591,9 +7425,9 @@ std::string GCode::retract(bool toolchange, bool is_last_retraction, LiftType li if (needs_lift && can_lift) { if (apply_instantly) - gcode += m_writer.eager_lift(lift_type); + gcode += m_writer->eager_lift(lift_type); else - gcode += m_writer.lazy_lift(lift_type, m_spiral_vase != nullptr); + gcode += m_writer->lazy_lift(lift_type, m_spiral_vase != nullptr); } return gcode; @@ -7602,11 +7436,11 @@ std::string GCode::retract(bool toolchange, bool is_last_retraction, LiftType li std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bool by_object, int toolchange_temp_override) { int new_extruder_id = get_extruder_id(new_filament_id); - if (!m_writer.need_toolchange(new_filament_id)) + if (!m_writer->need_toolchange(new_filament_id)) return ""; // if we are running a single-extruder setup, just set the extruder and return nothing - if (!m_writer.multiple_extruders) { + if (!m_writer->multiple_extruders) { this->placeholder_parser().set("current_extruder", new_filament_id); this->placeholder_parser().set("retraction_distance_when_ec", m_config.retraction_distances_when_ec.get_at(new_filament_id)); this->placeholder_parser().set("long_retraction_when_ec", m_config.long_retractions_when_ec.get_at(new_filament_id)); @@ -7629,13 +7463,13 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo check_add_eol(gcode); } if (m_config.enable_pressure_advance.get_at(new_filament_id)) { - gcode += m_writer.set_pressure_advance(m_config.pressure_advance.get_at(new_filament_id)); + gcode += m_writer->set_pressure_advance(m_config.pressure_advance.get_at(new_filament_id)); // Orca: Adaptive PA // Reset Adaptive PA processor last PA value m_pa_processor->resetPreviousPA(m_config.pressure_advance.get_at(new_filament_id)); } - gcode += m_writer.toolchange(new_filament_id); + gcode += m_writer->toolchange(new_filament_id); return gcode; } @@ -7650,18 +7484,18 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo // BBS: insert skip object label before change filament while by object if (by_object) - m_writer.add_object_change_labels(gcode); + m_writer->add_object_change_labels(gcode); bool add_change_filament_624 = false; - if (m_writer.filament() != nullptr) { + if (m_writer->filament() != nullptr) { // Process the custom filament_end_gcode. set_extruder() is only called if there is no wipe tower // so it should not be injected twice. - unsigned int old_filament_id = m_writer.filament()->id(); + unsigned int old_filament_id = m_writer->filament()->id(); const std::string &filament_end_gcode = m_config.filament_end_gcode.get_at(old_filament_id); if (! filament_end_gcode.empty()) { DynamicConfig config; config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index)); - config.set_key_value("layer_z", new ConfigOptionFloat(m_writer.get_position().z() - m_config.z_offset.value)); + config.set_key_value("layer_z", new ConfigOptionFloat(m_writer->get_position().z() - m_config.z_offset.value)); config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); config.set_key_value("filament_extruder_id", new ConfigOptionInt(int(get_extruder_id(old_filament_id)))); if (!m_filament_instances_code.empty()) { @@ -7677,7 +7511,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo // If ooze prevention is enabled, park current extruder in the nearest // standby point and set it to the standby temperature. - if (m_ooze_prevention.enable && m_writer.filament() != nullptr) + if (m_ooze_prevention.enable && m_writer->filament() != nullptr) gcode += m_ooze_prevention.pre_toolchange(*this); // BBS @@ -7690,23 +7524,23 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo if (toolchange_temp_override > 0) new_filament_temp = toolchange_temp_override; - Vec3d nozzle_pos = m_writer.get_position(); + Vec3d nozzle_pos = m_writer->get_position(); float old_retract_length, old_retract_length_toolchange, wipe_volume; int old_filament_temp, old_filament_e_feedrate; float filament_area = float((M_PI / 4.f) * pow(m_config.filament_diameter.get_at(new_filament_id), 2)); //BBS: add handling for filament change in start gcode int old_filament_id = -1; - if (m_writer.filament() != nullptr || m_start_gcode_filament != -1) { + if (m_writer->filament() != nullptr || m_start_gcode_filament != -1) { std::vector flush_matrix(cast(get_flush_volumes_matrix(m_config.flush_volumes_matrix.values, new_extruder_id, m_config.nozzle_diameter.values.size()))); const unsigned int number_of_extruders = (unsigned int) (m_config.filament_colour.values.size()); // if is multi_extruder only use the fist extruder matrix - if (m_writer.filament() != nullptr) - assert(m_writer.filament()->id() < number_of_extruders); + if (m_writer->filament() != nullptr) + assert(m_writer->filament()->id() < number_of_extruders); else assert(m_start_gcode_filament < number_of_extruders); - old_filament_id = m_writer.filament() != nullptr ? m_writer.filament()->id() : m_start_gcode_filament; - int old_extruder_id = m_writer.filament() != nullptr ? m_writer.filament()->extruder_id() : get_extruder_id(m_start_gcode_filament); + old_filament_id = m_writer->filament() != nullptr ? m_writer->filament()->id() : m_start_gcode_filament; + int old_extruder_id = m_writer->filament() != nullptr ? m_writer->filament()->extruder_id() : get_extruder_id(m_start_gcode_filament); old_retract_length = m_config.retraction_length.get_at(old_filament_id); old_retract_length_toolchange = m_config.retract_length_toolchange.get_at(old_filament_id); @@ -7716,7 +7550,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo float grab_purge_volume = m_config.grab_length.get_at(new_extruder_id) * 2.4; if (old_extruder_id != new_extruder_id) { //calc flush volume between the same extruder id - int old_filament_id_in_new_extruder = m_writer.filament(new_extruder_id) != nullptr ? m_writer.filament(new_extruder_id)->id() : -1; + int old_filament_id_in_new_extruder = m_writer->filament(new_extruder_id) != nullptr ? m_writer->filament(new_extruder_id)->id() : -1; if (old_filament_id_in_new_extruder == -1) wipe_volume = 0; else { @@ -7852,13 +7686,13 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo //BBS: gcode writer doesn't know where the extruder is and whether fan speed is changed after inserting tool change gcode //Set this flag so that normal lift will be used the first time after tool change. gcode += ";_FORCE_RESUME_FAN_SPEED\n"; - m_writer.set_current_position_clear(false); + m_writer->set_current_position_clear(false); //BBS: check whether custom gcode changes the z position. Update if changed double temp_z_after_tool_change; if (GCodeProcessor::get_last_z_from_gcode(toolchange_gcode_parsed, temp_z_after_tool_change)) { - Vec3d pos = m_writer.get_position(); + Vec3d pos = m_writer->get_position(); pos(2) = temp_z_after_tool_change; - m_writer.set_position(pos); + m_writer->set_position(pos); } } } @@ -7866,13 +7700,13 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo // BBS. Reset old extruder E-value. // Keep retract length because Custom GCode will guarantee retract length be the same as toolchange if (m_config.single_extruder_multi_material) { - m_writer.reset_e(); + m_writer->reset_e(); } //BBS: don't add T[next extruder] if there is no T cmd on filament change //We inform the writer about what is happening, but we may not use the resulting gcode. - std::string toolchange_command = m_writer.toolchange(new_filament_id); - if (!custom_gcode_changes_tool(toolchange_gcode_parsed, m_writer.toolchange_prefix(), new_filament_id)) + std::string toolchange_command = m_writer->toolchange(new_filament_id); + if (!custom_gcode_changes_tool(toolchange_gcode_parsed, m_writer->toolchange_prefix(), new_filament_id)) gcode += toolchange_command; else { // user provided his own toolchange gcode, no need to do anything @@ -7883,7 +7717,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo int temp = (m_layer_index <= 0 ? m_config.nozzle_temperature_initial_layer.get_at(new_filament_id) : m_config.nozzle_temperature.get_at(new_filament_id)); - gcode += m_writer.set_temperature(temp, false); + gcode += m_writer->set_temperature(temp, false); } this->placeholder_parser().set("current_extruder", new_filament_id); @@ -7927,7 +7761,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo gcode += m_ooze_prevention.post_toolchange(*this); if (m_config.enable_pressure_advance.get_at(new_filament_id)) { - gcode += m_writer.set_pressure_advance(m_config.pressure_advance.get_at(new_filament_id)); + gcode += m_writer->set_pressure_advance(m_config.pressure_advance.get_at(new_filament_id)); // Orca: Adaptive PA // Reset Adaptive PA processor last PA value m_pa_processor->resetPreviousPA(m_config.pressure_advance.get_at(new_filament_id)); @@ -8013,7 +7847,7 @@ Vec2d GCode::point_to_gcode(const Point &point) const Point GCode::gcode_to_point(const Vec2d &point) const { Vec2d pt = point - m_origin; - if (const Extruder *extruder = m_writer.filament(); extruder) + if (const Extruder *extruder = m_writer->filament(); extruder) // This function may be called at the very start from toolchange G-code when the extruder is not assigned yet. pt += m_config.extruder_offset.get_at(extruder->extruder_id()); return scaled(pt); diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index dbec7d0315..9004f30fd3 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -4,6 +4,7 @@ #include "libslic3r.h" #include "ExPolygon.hpp" #include "GCodeWriter.hpp" +#include "BeltGCodeWriter.hpp" #include "Layer.hpp" #include "Point.hpp" #include "PlaceholderParser.hpp" @@ -206,16 +207,18 @@ public: m_last_obj_copy(nullptr, Point(std::numeric_limits::max(), std::numeric_limits::max())), // BBS m_toolchange_count(0), - m_nominal_z(0.) + m_nominal_z(0.), + m_writer(std::make_unique()) {} - ~GCode() = default; + virtual ~GCode() = default; +public: // throws std::runtime_exception on error, // throws CanceledException through print->throw_if_canceled(). void do_export(Print* print, const char* path, GCodeProcessorResult* result = nullptr, ThumbnailsGeneratorCallback thumbnail_cb = nullptr); void export_layer_filaments(GCodeProcessorResult* result); //BBS: set offset for gcode writer - void set_gcode_offset(double x, double y) { m_writer.set_xy_offset(x, y); m_processor.set_xy_offset(x, y);} + void set_gcode_offset(double x, double y) { m_writer->set_xy_offset(x, y); m_processor.set_xy_offset(x, y);} // Exported for the helper classes (OozePrevention, Wipe) and for the Perl binding for unit tests. const Vec2d& origin() const { return m_origin; } @@ -227,8 +230,8 @@ public: Vec2d point_to_gcode_quantized(const Point& point) const; const FullPrintConfig &config() const { return m_config; } const Layer* layer() const { return m_layer; } - GCodeWriter& writer() { return m_writer; } - const GCodeWriter& writer() const { return m_writer; } + GCodeWriter& writer() { return *m_writer; } + const GCodeWriter& writer() const { return *m_writer; } PlaceholderParser& placeholder_parser() { return m_placeholder_parser_integration.parser; } const PlaceholderParser& placeholder_parser() const { return m_placeholder_parser_integration.parser; } // Process a template through the placeholder parser, collect error messages to be reported @@ -248,7 +251,7 @@ public: std::string travel_to(const Point& point, ExtrusionRole role, std::string comment, double z = DBL_MAX); bool needs_retraction(const Polyline& travel, ExtrusionRole role, LiftType& lift_type); std::string retract(bool toolchange = false, bool is_last_retraction = false, LiftType lift_type = LiftType::NormalLift, bool apply_instantly = false, ExtrusionRole role = erNone); - std::string unretract() { return m_writer.unlift() + m_writer.unretract(); } + std::string unretract() { return m_writer->unlift() + m_writer->unretract(); } std::string set_extruder(unsigned int extruder_id, double print_z, bool by_object=false, int toolchange_temp_override = -1); bool is_BBL_Printer(); WipeTowerType wipe_tower_type(); @@ -300,7 +303,7 @@ public: } }; -private: +protected: class GCodeOutputStream { public: GCodeOutputStream(FILE *f, GCodeProcessor &processor) : f(f), m_processor(processor) {} @@ -328,9 +331,17 @@ private: FILE *f = nullptr; GCodeProcessor &m_processor; }; + + // Virtual hooks for belt printer subclass (BeltGCode). + // No-ops in base GCode; overridden in BeltGCode. + virtual void init_belt_writer(Print &print, bool is_bbl_printers) {} + virtual void write_belt_header(GCodeOutputStream &file, const Print &print) {} + virtual void on_set_origin(const PrintObject *obj, const Point &inst_shift) {} + virtual bool should_disable_arc_fitting() const { return false; } + void _do_export(Print &print, GCodeOutputStream &file, ThumbnailsGeneratorCallback thumbnail_cb); - static std::vector collect_layers_to_print(const PrintObject &object); + static std::vector collect_layers_to_print(const PrintObject &object, bool skip_empty_first_layer = false); static std::vector>> collect_layers_to_print(const Print &print); std::string generate_skirt(const Print &print, @@ -493,16 +504,11 @@ private: This affects the input arguments supplied to the extrude*() and travel_to() methods. */ Vec2d m_origin; - // Per-axis origin snap: shift G-code so each object's bbox min = offset. - bool m_origin_snap[3] = {false, false, false}; - double m_origin_snap_offset[3] = {0., 0., 0.}; - // Called when switching instances to recompute the writer's snap for this instance. - void update_origin_snap(const PrintObject *obj, const Point &inst_shift); FullPrintConfig m_config; DynamicConfig m_calib_config; // scaled G-code resolution double m_scaled_resolution; - GCodeWriter m_writer; + std::unique_ptr m_writer; struct PlaceholderParserIntegration { void reset(); diff --git a/src/libslic3r/GCode/BeltBackTransform.cpp b/src/libslic3r/GCode/BeltBackTransform.cpp index 7cd5bc6b53..2e2cd26f75 100644 --- a/src/libslic3r/GCode/BeltBackTransform.cpp +++ b/src/libslic3r/GCode/BeltBackTransform.cpp @@ -1,40 +1,8 @@ #include "BeltBackTransform.hpp" -#include "../Geometry.hpp" -#include +#include "../BeltTransform.hpp" namespace Slic3r { -// Keep in sync with PrintObjectSlice.cpp compute_shear_factor (lines ~147-157). -static double compute_shear_factor(BeltShearMode mode, double angle_deg) -{ - double angle_rad = Geometry::deg2rad(angle_deg); - double sin_a = std::sin(angle_rad); - double cos_a = std::cos(angle_rad); - switch (mode) { - case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; - case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; - case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; - case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; - default: return 0.; - } -} - -// Keep in sync with PrintObjectSlice.cpp compute_scale_factor (lines ~180-192). -static double compute_scale_factor(BeltScaleMode mode, double angle_deg) -{ - if (mode == BeltScaleMode::None) return 1.; - double angle_rad = Geometry::deg2rad(angle_deg); - double sin_a = std::sin(angle_rad); - double cos_a = std::cos(angle_rad); - switch (mode) { - case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; - case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; - case BeltScaleMode::Sin: return sin_a; - case BeltScaleMode::Cos: return cos_a; - default: return 1.; - } -} - bool BeltBackTransform::init_from_config(const PrintConfig &config) { m_active = false; @@ -43,99 +11,19 @@ bool BeltBackTransform::init_from_config(const PrintConfig &config) if (!config.belt_printer.value || !config.belt_gcode_back_transform.value) return false; - // --- Pre-slice axis remap (same as PrintObjectSlice.cpp) --- - int pre_rx = int(config.belt_preslice_remap_x.value); - int pre_ry = int(config.belt_preslice_remap_y.value); - int pre_rz = int(config.belt_preslice_remap_z.value); - - bool has_preslice_remap = (pre_rx != int(BeltRemapAxis::PosX) || - pre_ry != int(BeltRemapAxis::PosY) || - pre_rz != int(BeltRemapAxis::PosZ)); - // Require at least one active transform to proceed. bool has_global_shear = config.belt_shear_x_global.value || config.belt_shear_y_global.value || config.belt_shear_z_global.value; - if (!has_global_shear && !has_preslice_remap) + if (!has_global_shear && !BeltTransformPipeline::has_preslice_remap(config)) return false; - // Build pre-slice remap matrix. - Transform3d pre_remap = Transform3d::Identity(); - if (has_preslice_remap) { - auto remap_column = [](int r) -> Vec3d { - int axis = r % 3; - Vec3d col = Vec3d::Zero(); - if (r < 3) col[axis] = 1.0; - else if (r < 6) col[axis] = -1.0; - else col[axis] = -1.0; // Rev: max - pos - 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; - - // Rev mode translation (needs build volume extents). - Vec3d remap_trans = Vec3d::Zero(); - 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); - 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; - } - - // Build per-axis shear matrix (same as PrintObjectSlice.cpp). - struct AxisShear { BeltShearMode mode; double angle; int from; }; - AxisShear axes[3] = { - { config.belt_shear_x.value, config.belt_shear_x_angle.value, int(config.belt_shear_x_from.value) }, - { config.belt_shear_y.value, config.belt_shear_y_angle.value, int(config.belt_shear_y_from.value) }, - { config.belt_shear_z.value, config.belt_shear_z_angle.value, int(config.belt_shear_z_from.value) }, - }; - - Matrix3d shear = Matrix3d::Identity(); - bool has_shear = false; - for (int row = 0; row < 3; ++row) { - if (axes[row].mode != BeltShearMode::None) { - double factor = compute_shear_factor(axes[row].mode, axes[row].angle); - if (std::abs(factor) > EPSILON) { - shear(row, axes[row].from) += factor; - has_shear = true; - } - } - } - - // Build per-axis scale diagonal matrix (same as PrintObjectSlice.cpp). - double sx = compute_scale_factor(config.belt_scale_x.value, config.belt_scale_x_angle.value); - double sy = compute_scale_factor(config.belt_scale_y.value, config.belt_scale_y_angle.value); - double sz = compute_scale_factor(config.belt_scale_z.value, config.belt_scale_z_angle.value); - - Matrix3d scale = Matrix3d::Identity(); - bool has_scale = (std::abs(sx - 1.) > EPSILON || - std::abs(sy - 1.) > EPSILON || - std::abs(sz - 1.) > EPSILON); - if (has_scale) { - scale(0, 0) = sx; - scale(1, 1) = sy; - scale(2, 2) = sz; - } - - if (!has_shear && !has_scale && !has_preslice_remap) + // Build the forward pipeline (scale * shear * pre_remap) and store its inverse. + Transform3d forward = BeltTransformPipeline::build_forward_transform(config); + if (forward.isApprox(Transform3d::Identity())) return false; - // Forward pipeline: scale * shear * pre_remap (same order as PrintObjectSlice.cpp). - Transform3d combined = Transform3d::Identity(); - combined.linear() = scale * shear; - combined = combined * pre_remap; - m_inverse = combined.inverse(); + m_inverse = forward.inverse(); m_active = true; return true; } diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index b5a6402a14..51f82ea009 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -20,59 +20,6 @@ namespace Slic3r { bool GCodeWriter::full_gcode_comment = true; -void GCodeWriter::set_belt_angle(double angle_deg) -{ - m_belt_angle_rad = Geometry::deg2rad(angle_deg); -} - -void GCodeWriter::set_axis_remap(int rx, int ry, int rz) -{ - m_remap_x = rx; - m_remap_y = ry; - m_remap_z = rz; -} - -void GCodeWriter::set_build_volume_max(const Vec3d &max) -{ - m_build_vol_max = max; -} - -void GCodeWriter::set_belt_back_transform(const PrintConfig &config) -{ - m_belt_back_transform.init_from_config(config); -} - -void GCodeWriter::set_origin_snap(int axis, bool enable, double offset, double bbox_min) -{ - if (axis >= 0 && axis < 3) { - m_origin_snap[axis] = enable; - m_origin_offset[axis] = offset; - m_origin_bbox_min[axis] = bbox_min; - } -} - -Vec3d GCodeWriter::to_machine_coords(const Vec3d &pos) const -{ - if (!is_belt_printer()) - return pos; - // Step 1: Undo the shear/scale applied during slicing. - Vec3d p = m_belt_back_transform.apply(pos); - // Step 2: Apply axis remapping for the machine's coordinate convention. - // BeltRemapAxis: 0-2 = +X/+Y/+Z, 3-5 = -X/-Y/-Z, 6-8 = Rev X/Y/Z - auto remap = [this, &p](int r) -> double { - int axis = r % 3; - if (r < 3) return p[axis]; - if (r < 6) return -p[axis]; - return m_build_vol_max[axis] - p[axis]; - }; - Vec3d result = { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) }; - // Per-axis origin snap: shift so bbox min on each enabled axis = offset. - for (int i = 0; i < 3; ++i) - if (m_origin_snap[i]) - result[i] -= (m_origin_bbox_min[i] - m_origin_offset[i]); - return result; -} - bool GCodeWriter::supports_separate_travel_acceleration(GCodeFlavor flavor) { return (flavor == gcfRepetier || flavor == gcfMarlinFirmware || flavor == gcfRepRapFirmware); @@ -615,13 +562,7 @@ std::string GCodeWriter::travel_to_xy(const Vec2d &point, const std::string &com Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; GCodeG1Formatter w; - if (is_belt_printer()) { - // Belt printer: transform to machine coordinates (XY travel also needs Z due to YZ rotation) - Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); - w.emit_xyz(machine); - } else { - w.emit_xy(point_on_plate); - } + w.emit_xy(point_on_plate); auto speed = m_is_first_layer ? this->config.get_abs_value("initial_layer_travel_speed") : this->config.travel_speed.value; w.emit_f(speed * 60.0); @@ -635,11 +576,6 @@ it will not perform subsequent lifts, even if Z was raised manually (i.e. with travel_to_z()) and thus _lifted was reduced. */ std::string GCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase) { - // Belt printer: force NormalLift since SpiralLift and SlopeLift compute slope angles - // that don't account for the YZ coordinate rotation. - if (is_belt_printer()) - lift_type = LiftType::NormalLift; - // check whether the above/below conditions are met double target_lift = 0; { @@ -668,8 +604,7 @@ std::string GCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase) // BBS: immediately execute an undelayed lift move with a spiral lift pattern // designed specifically for subsequent gcode injection (e.g. timelapse) std::string GCodeWriter::eager_lift(const LiftType type) { - // Belt printer: force NormalLift (SpiralLift/SlopeLift don't account for YZ rotation). - const LiftType effective_type = is_belt_printer() ? LiftType::NormalLift : type; + const LiftType effective_type = type; std::string lift_move; double target_lift = 0; { @@ -756,8 +691,6 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co // / to make the z list early to avoid to hit some warping place when travel is long. Vec2d temp = delta_no_z.normalized() * delta(2) / tan(this->filament()->travel_slope()); Vec3d slope_top_point = Vec3d(temp(0), temp(1), delta(2)) + source; - if (is_belt_printer()) - slope_top_point = to_machine_coords(slope_top_point); GCodeG1Formatter w0; w0.emit_xyz(slope_top_point); w0.emit_f(travel_speed * 60.0); @@ -772,27 +705,18 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co std::string xy_z_move; { - Vec3d emit_target = is_belt_printer() ? to_machine_coords(target) : target; GCodeG1Formatter w0; if (this->is_current_position_clear()) { - w0.emit_xyz(emit_target); + w0.emit_xyz(target); w0.emit_f(travel_speed * 60.0); w0.emit_comment(GCodeWriter::full_gcode_comment, comment); xy_z_move = w0.string(); } else { - if (is_belt_printer()) { - // Belt mode: can't split XY and Z moves independently, emit full XYZ - w0.emit_xyz(emit_target); - w0.emit_f(travel_speed * 60.0); - w0.emit_comment(GCodeWriter::full_gcode_comment, comment); - xy_z_move = w0.string(); - } else { - w0.emit_xy(Vec2d(target.x(), target.y())); - w0.emit_f(travel_speed * 60.0); - w0.emit_comment(GCodeWriter::full_gcode_comment, comment); - xy_z_move = w0.string() + _travel_to_z(target.z(), comment); - } + w0.emit_xy(Vec2d(target.x(), target.y())); + w0.emit_f(travel_speed * 60.0); + w0.emit_comment(GCodeWriter::full_gcode_comment, comment); + xy_z_move = w0.string() + _travel_to_z(target.z(), comment); } } m_pos = dest_point; @@ -818,25 +742,15 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co //BBS: take plate offset into consider Vec3d point_on_plate = { dest_point(0) - m_x_offset, dest_point(1) - m_y_offset, dest_point(2) }; - if (is_belt_printer()) - point_on_plate = to_machine_coords(point_on_plate); std::string out_string; GCodeG1Formatter w; if (!this->is_current_position_clear()) { - if (is_belt_printer()) { - // Belt mode: emit full XYZ since Y and Z are coupled - w.emit_xyz(point_on_plate); - w.emit_f(this->config.travel_speed.value * 60.0); - w.emit_comment(GCodeWriter::full_gcode_comment, comment); - out_string = w.string(); - } else { - //force to move xy first then z after filament change - w.emit_xy(Vec2d(point_on_plate.x(), point_on_plate.y())); - w.emit_f(this->config.travel_speed.value * 60.0); - w.emit_comment(GCodeWriter::full_gcode_comment, comment); - out_string = w.string() + _travel_to_z(point_on_plate.z(), comment); - } + //force to move xy first then z after filament change + w.emit_xy(Vec2d(point_on_plate.x(), point_on_plate.y())); + w.emit_f(this->config.travel_speed.value * 60.0); + w.emit_comment(GCodeWriter::full_gcode_comment, comment); + out_string = w.string() + _travel_to_z(point_on_plate.z(), comment); } else { GCodeG1Formatter w; w.emit_xyz(point_on_plate); @@ -880,13 +794,7 @@ std::string GCodeWriter::_travel_to_z(double z, const std::string &comment) } GCodeG1Formatter w; - if (is_belt_printer()) { - // Belt printer: a Z-only move in slicing frame needs to emit both Y and Z in machine coords. - Vec3d machine = to_machine_coords(Vec3d(m_pos.x() - m_x_offset, m_pos.y() - m_y_offset, z)); - w.emit_xyz(machine); - } else { - w.emit_z(z); - } + w.emit_z(z); w.emit_f(speed * 60.0); //BBS w.emit_comment(GCodeWriter::full_gcode_comment, comment); @@ -991,12 +899,7 @@ std::string GCodeWriter::extrude_to_xy(const Vec2d &point, double dE, const std: Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; GCodeG1Formatter w; - if (is_belt_printer()) { - Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); - w.emit_xyz(machine); - } else { - w.emit_xy(point_on_plate); - } + w.emit_xy(point_on_plate); if (!force_no_extrusion) w.emit_e(filament()->E()); //BBS @@ -1037,8 +940,6 @@ std::string GCodeWriter::extrude_to_xyz(const Vec3d &point, double dE, const std Vec3d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset, point(2) }; GCodeG1Formatter w; - if (is_belt_printer()) - point_on_plate = to_machine_coords(point_on_plate); w.emit_xyz(point_on_plate); if (!force_no_extrusion) w.emit_e(filament()->E()); diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index 2334f5d416..c96c3ca2c1 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -8,12 +8,11 @@ #include "Point.hpp" #include "PrintConfig.hpp" #include "GCode/CoolingBuffer.hpp" -#include "GCode/BeltBackTransform.hpp" - namespace Slic3r { class GCodeWriter { public: + virtual ~GCodeWriter() = default; GCodeConfig config; bool multiple_extruders; @@ -74,21 +73,21 @@ public: std::string set_speed(double F, const std::string &comment = std::string(), const std::string &cooling_marker = std::string()); // SoftFever NOTE: the returned speed is mm/minute double get_current_speed() const { return m_current_speed;} - std::string travel_to_xy(const Vec2d &point, const std::string &comment = std::string()); - std::string travel_to_xyz(const Vec3d &point, const std::string &comment = std::string(), bool force_z = false); + virtual std::string travel_to_xy(const Vec2d &point, const std::string &comment = std::string()); + virtual std::string travel_to_xyz(const Vec3d &point, const std::string &comment = std::string(), bool force_z = false); std::string travel_to_z(double z, const std::string &comment = std::string(), bool force = false); bool will_move_z(double z) const; - std::string extrude_to_xy(const Vec2d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false); + virtual std::string extrude_to_xy(const Vec2d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false); //BBS: generate G2 or G3 extrude which moves by arc std::string extrude_arc_to_xy(const Vec2d &point, const Vec2d ¢er_offset, double dE, const bool is_ccw, const std::string &comment = std::string(), bool force_no_extrusion = false); - std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false); + virtual std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false); std::string retract(bool before_wipe = false, double retract_length = 0); std::string retract_for_toolchange(bool before_wipe = false, double retract_length = 0); std::string unretract(); // do lift instantly - std::string eager_lift(const LiftType type); + virtual std::string eager_lift(const LiftType type); // record a lift request, do realy lift in next travel - std::string lazy_lift(LiftType lift_type = LiftType::NormalLift, bool spiral_vase = false); + virtual std::string lazy_lift(LiftType lift_type = LiftType::NormalLift, bool spiral_vase = false); std::string unlift(); const Vec3d& get_position() const { return m_pos; } Vec3d& get_position() { return m_pos; } @@ -126,23 +125,23 @@ public: void set_is_first_layer(bool bval) { m_is_first_layer = bval; } GCodeFlavor get_gcode_flavor() const { return config.gcode_flavor; } - // Belt printer: set the belt angle and precompute sin/cos for coordinate transformation. - void set_belt_angle(double angle_deg); - bool is_belt_printer() const { return m_belt_angle_rad != 0.; } - // Set axis remap for G-code output (BeltRemapAxis enum values). - void set_axis_remap(int rx, int ry, int rz); - // Set build volume extents for Rev remap mode (max X, Y, Z). - void set_build_volume_max(const Vec3d &max); - // Initialize the belt back-transform that undoes slicing shear/scale. - void set_belt_back_transform(const PrintConfig &config); - // Set per-axis origin snap: shifts G-code so bbox min on this axis = offset. - void set_origin_snap(int axis, bool enable, double offset, double bbox_min); - // Transform a point from the slicing frame to machine coordinates. - Vec3d to_machine_coords(const Vec3d &pos) const; - // Returns whether this flavor supports separate print and travel acceleration. static bool supports_separate_travel_acceleration(GCodeFlavor flavor); - private: +protected: + // Position/lift/offset state — accessible to subclasses (e.g. BeltGCodeWriter) + Vec3d m_pos = Vec3d::Zero(); + double m_x_offset{ 0 }; + double m_y_offset{ 0 }; + double m_lifted; + double m_to_lift; + LiftType m_to_lift_type; + bool m_is_first_layer = true; + bool m_is_current_pos_clear = false; + double m_current_speed; + + virtual std::string _travel_to_z(double z, const std::string &comment); + +private: // Extruders are sorted by their ID, so that binary search is possible. std::vector m_filament_extruders; bool m_single_extruder_multi_material; @@ -170,48 +169,21 @@ public: unsigned int m_last_additional_fan_speed; int m_last_bed_temperature; bool m_last_bed_temperature_reached; - double m_lifted; - - // BBS - double m_to_lift; - LiftType m_to_lift_type; - Vec3d m_pos = Vec3d::Zero(); - //BBS: this flag is used to indicate whether the m_pos is real. - //A example that of the first move, the m_pos is zero, but the real position of extruder doesn't - //Pos must be clear after the first xyz travel move - bool m_is_current_pos_clear = false; - //BBS: x, y offset for gcode generated - double m_x_offset{ 0 }; - double m_y_offset{ 0 }; // Orca: slicing resolution in mm double m_resolution = 0.01; - + std::string m_gcode_label_objects_start; std::string m_gcode_label_objects_end; //SoftFever bool m_is_bbl_printers = false; - // Belt printer state - double m_belt_angle_rad = 0.; - int m_remap_x = 0; - int m_remap_y = 1; - int m_remap_z = 2; - Vec3d m_build_vol_max = Vec3d::Zero(); - BeltBackTransform m_belt_back_transform; - bool m_origin_snap[3] = {false, false, false}; - double m_origin_offset[3] = {0., 0., 0.}; // target coord for bbox min - double m_origin_bbox_min[3] = {0., 0., 0.}; // computed bbox min in machine space - double m_current_speed; - bool m_is_first_layer = true; - enum class Acceleration { Travel, Print }; - std::string _travel_to_z(double z, const std::string &comment); std::string _spiral_travel_to_z(double z, const Vec2d &ij_offset, const std::string &comment); std::string _retract(double length, double restart_extra, const std::string &comment); std::string set_acceleration_internal(Acceleration type, unsigned int acceleration); diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 0b7190e8d1..1f787a70cf 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -12,6 +12,7 @@ #include "Thread.hpp" #include "Time.hpp" #include "GCode.hpp" +#include "BeltGCode.hpp" #include "GCode/WipeTower.hpp" #include "GCode/WipeTower2.hpp" #include "Utils.hpp" @@ -2551,12 +2552,17 @@ std::string Print::export_gcode(const std::string& path_template, GCodeProcessor this->set_status(80, message); // The following line may die for multiple reasons. - GCode gcode; + // Factory: use BeltGCode for belt printers, plain GCode otherwise. + std::unique_ptr gcode; + if (m_config.belt_printer.value) + gcode = std::make_unique(); + else + gcode = std::make_unique(); //BBS: compute plate offset for gcode-generator const Vec3d origin = this->get_plate_origin(); - gcode.set_gcode_offset(origin(0), origin(1)); - gcode.do_export(this, path.c_str(), result, thumbnail_cb); - gcode.export_layer_filaments(result); + gcode->set_gcode_offset(origin(0), origin(1)); + gcode->do_export(this, path.c_str(), result, thumbnail_cb); + gcode->export_layer_filaments(result); //BBS result->conflict_result = m_conflict_result; return path.c_str(); diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index c13a1407c0..9a5d000e1c 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -17,6 +17,7 @@ #include "GCode/ThumbnailData.hpp" #include "GCode/GCodeProcessor.hpp" #include "MultiMaterialSegmentation.hpp" +#include "BeltTransform.hpp" #include "libslic3r.h" #include @@ -190,30 +191,7 @@ class ConstSupportLayerPtrsAdaptor : public ConstVectorOfPtrsAdaptor double { - int axis = r % 3; - if (r < 3) return v[axis]; - return -v[axis]; - }; - Vec3d mn = bb.min.cast(), mx = bb.max.cast(); - 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; + return BeltTransformPipeline::remap_bbox(model_object, config); } // Single instance of a PrintObject. diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index ba2ed408ce..be65c21c59 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1,5 +1,6 @@ #include "Exception.hpp" #include "Print.hpp" +#include "BeltTransform.hpp" #include "BoundingBox.hpp" #include "ClipperUtils.hpp" #include "ElephantFootCompensation.hpp" @@ -3398,76 +3399,22 @@ void PrintObject::update_slicing_parameters() // Orca: updated function call for XYZ shrinkage compensation if (!m_slicing_params.valid) { coordf_t object_height = this->model_object()->max_z(); - // Belt floor parameters for support clipping (populated below if belt Z-shear is active). - double belt_floor_shear_factor_out = 0.0; - int belt_floor_from_axis_out = 1; - double belt_floor_z_shift_out = 0.0; - // Belt shear/scale/pre-remap may change the effective Z height. + BeltTransformPipeline::BeltFloorParams belt_floor; const auto &pcfg = this->print()->config(); if (pcfg.belt_printer.value) { - BoundingBoxf3 bb = belt_remapped_bbox(*this->model_object(), pcfg); - bool has_preslice_remap = (int(pcfg.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || - int(pcfg.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || - int(pcfg.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ)); - if (has_preslice_remap) + BoundingBoxf3 bb = BeltTransformPipeline::remap_bbox(*this->model_object(), pcfg); + if (BeltTransformPipeline::has_preslice_remap(pcfg)) object_height = bb.size().z(); - - bool has_z_shear = pcfg.belt_shear_z.value != BeltShearMode::None; - bool has_z_scale = pcfg.belt_scale_z.value != BeltScaleMode::None; - if (has_z_shear || has_z_scale) { - auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { - double angle_rad = Geometry::deg2rad(angle_deg); - double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad); - switch (mode) { - case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; - case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; - case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; - case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; - default: return 0.; - } - }; - auto compute_scale_factor = [](BeltScaleMode mode, double angle_deg) -> double { - if (mode == BeltScaleMode::None) return 1.; - double angle_rad = Geometry::deg2rad(angle_deg); - double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad); - switch (mode) { - case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; - case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; - case BeltScaleMode::Sin: return sin_a; - case BeltScaleMode::Cos: return cos_a; - default: return 1.; - } - }; - double shear_factor = has_z_shear ? compute_shear_factor(pcfg.belt_shear_z.value, pcfg.belt_shear_z_angle.value) : 0.; - double scale_z = compute_scale_factor(pcfg.belt_scale_z.value, pcfg.belt_scale_z_angle.value); - if (has_z_shear && std::abs(shear_factor) > EPSILON) { - int from = int(pcfg.belt_shear_z_from.value); - double min_rz = std::numeric_limits::max(); - double max_rz = std::numeric_limits::lowest(); - for (double vz : {bb.min.z(), bb.max.z()}) - for (double vs : {bb.min(from), bb.max(from)}) { - double new_z = scale_z * (vz + shear_factor * vs); - min_rz = std::min(min_rz, new_z); - max_rz = std::max(max_rz, new_z); - } - object_height = max_rz - min_rz; - belt_floor_shear_factor_out = shear_factor; - belt_floor_from_axis_out = from; - // Belt contact surface starts at bb.min.z() pre-shear; add the - // slicing Z-shift that keeps the mesh above Z=0. - // Exact value is patched after slice_volumes() in posSlice. - belt_floor_z_shift_out = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.); - } else { - object_height *= scale_z; - } - } + auto hr = BeltTransformPipeline::compute_belt_height_and_floor(pcfg, bb, object_height); + object_height = hr.object_height; + belt_floor = hr.floor_params; } m_slicing_params = SlicingParameters::create_from_config(pcfg, m_config, object_height, this->object_extruders(), this->print()->shrinkage_compensation()); // Populate belt floor parameters into slicing params for support clipping. - m_slicing_params.belt_floor_shear_factor = belt_floor_shear_factor_out; - m_slicing_params.belt_floor_from_axis = belt_floor_from_axis_out; - m_slicing_params.belt_floor_z_shift = belt_floor_z_shift_out; + m_slicing_params.belt_floor_shear_factor = belt_floor.shear_factor; + m_slicing_params.belt_floor_from_axis = belt_floor.from_axis; + m_slicing_params.belt_floor_z_shift = belt_floor.z_shift; } } @@ -3505,75 +3452,23 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full sort_remove_duplicates(object_extruders); //FIXME add painting extruders - // Belt floor parameters for support clipping (populated below if belt Z-shear is active). - double belt_floor_shear_factor_out = 0.0; - int belt_floor_from_axis_out = 1; - double belt_floor_z_shift_out = 0.0; - + BeltTransformPipeline::BeltFloorParams belt_floor; if (object_max_z <= 0.f) { BoundingBoxf3 bb = model_object.raw_bounding_box(); object_max_z = (float)bb.size().z(); - // Belt pre-remap/shear/scale may change the effective Z height. if (print_config.belt_printer.value) { - bb = belt_remapped_bbox(model_object, print_config); - bool has_preslice_remap = (int(print_config.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || - int(print_config.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || - int(print_config.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ)); - if (has_preslice_remap) + bb = BeltTransformPipeline::remap_bbox(model_object, print_config); + if (BeltTransformPipeline::has_preslice_remap(print_config)) object_max_z = (float)bb.size().z(); - - bool has_z_shear = print_config.belt_shear_z.value != BeltShearMode::None; - bool has_z_scale = print_config.belt_scale_z.value != BeltScaleMode::None; - if (has_z_shear || has_z_scale) { - auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { - double angle_rad = Geometry::deg2rad(angle_deg); - double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad); - switch (mode) { - case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; - case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; - case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; - case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; - default: return 0.; - } - }; - auto compute_scale_factor = [](BeltScaleMode mode, double angle_deg) -> double { - if (mode == BeltScaleMode::None) return 1.; - double angle_rad = Geometry::deg2rad(angle_deg); - double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad); - switch (mode) { - case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; - case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; - case BeltScaleMode::Sin: return sin_a; - case BeltScaleMode::Cos: return cos_a; - default: return 1.; - } - }; - double shear_factor = has_z_shear ? compute_shear_factor(print_config.belt_shear_z.value, print_config.belt_shear_z_angle.value) : 0.; - double scale_z = compute_scale_factor(print_config.belt_scale_z.value, print_config.belt_scale_z_angle.value); - if (has_z_shear && std::abs(shear_factor) > EPSILON) { - int from = int(print_config.belt_shear_z_from.value); - double min_rz = std::numeric_limits::max(); - double max_rz = std::numeric_limits::lowest(); - for (double vz : {bb.min.z(), bb.max.z()}) - for (double vs : {bb.min(from), bb.max(from)}) { - double new_z = scale_z * (vz + shear_factor * vs); - min_rz = std::min(min_rz, new_z); - max_rz = std::max(max_rz, new_z); - } - object_max_z = (float)(max_rz - min_rz); - belt_floor_shear_factor_out = shear_factor; - belt_floor_from_axis_out = from; - belt_floor_z_shift_out = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.); - } else { - object_max_z *= (float)scale_z; - } - } + 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; } } SlicingParameters params = SlicingParameters::create_from_config(print_config, object_config, object_max_z, object_extruders, object_shrinkage_compensation); - params.belt_floor_shear_factor = belt_floor_shear_factor_out; - params.belt_floor_from_axis = belt_floor_from_axis_out; - params.belt_floor_z_shift = belt_floor_z_shift_out; + params.belt_floor_shear_factor = belt_floor.shear_factor; + params.belt_floor_from_axis = belt_floor.from_axis; + params.belt_floor_z_shift = belt_floor.z_shift; return params; } diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 4c767ea0ed..3067772f09 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -9,6 +9,8 @@ #include "Layer.hpp" #include "MultiMaterialSegmentation.hpp" #include "Print.hpp" +#include "BeltTransform.hpp" +#include "BeltSliceStrategy.hpp" #include "Geometry.hpp" //BBS #include "ShortestPath.hpp" @@ -142,147 +144,11 @@ static std::vector slice_volumes_inner( params_base.closing_radius = print_object_config.slice_closing_radius.value; params_base.extra_offset = 0; params_base.trafo = object_trafo; - if (print_config.belt_printer.value) { - // --- Pre-slice axis remap --- - // Permutes/negates model axes before slicing so the slicer's coordinate - // system matches the physical bed orientation (e.g. XZ bed instead of XY). - int pre_rx = int(print_config.belt_preslice_remap_x.value); - int pre_ry = int(print_config.belt_preslice_remap_y.value); - int pre_rz = int(print_config.belt_preslice_remap_z.value); - - bool has_preslice_remap = (pre_rx != int(BeltRemapAxis::PosX) || - pre_ry != int(BeltRemapAxis::PosY) || - pre_rz != int(BeltRemapAxis::PosZ)); - - if (has_preslice_remap) { - // Build volume extents for Rev mode. - BoundingBoxf bbox_bed(print_config.printable_area.values); - Vec3d vol_max(bbox_bed.max.x(), bbox_bed.max.y(), - print_config.printable_height.value); - - // Each remap value selects a source axis and sign. - // The column vector tells the matrix which input axis feeds this output. - 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); - - // Translation for Rev modes: output = max[src] - input[src]. - Vec3d remap_trans = Vec3d::Zero(); - auto add_rev_offset = [&](int r, int out_axis) { - if (r >= 6) { - int src_axis = r % 3; - remap_trans[out_axis] = vol_max[src_axis]; - } - }; - add_rev_offset(pre_rx, 0); - add_rev_offset(pre_ry, 1); - add_rev_offset(pre_rz, 2); - - Transform3d pre_remap = Transform3d::Identity(); - pre_remap.linear() = remap_lin; - pre_remap.translation() = remap_trans; - - params_base.trafo = pre_remap * params_base.trafo; - } - - // Build per-axis shear matrix from 3 independent axis configs. - auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { - double angle_rad = Geometry::deg2rad(angle_deg); - double sin_a = std::sin(angle_rad); - double cos_a = std::cos(angle_rad); - switch (mode) { - case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; - case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; - case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; - case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; - default: return 0.; - } - }; - - struct AxisShear { BeltShearMode mode; double angle; int from; }; - AxisShear axes[3] = { - { print_config.belt_shear_x.value, print_config.belt_shear_x_angle.value, int(print_config.belt_shear_x_from.value) }, - { print_config.belt_shear_y.value, print_config.belt_shear_y_angle.value, int(print_config.belt_shear_y_from.value) }, - { print_config.belt_shear_z.value, print_config.belt_shear_z_angle.value, int(print_config.belt_shear_z_from.value) }, - }; - - Transform3d belt_shear = Transform3d::Identity(); - bool has_shear = false; - for (int row = 0; row < 3; ++row) { - if (axes[row].mode != BeltShearMode::None) { - double factor = compute_shear_factor(axes[row].mode, axes[row].angle); - if (std::abs(factor) > EPSILON) { - belt_shear.matrix()(row, axes[row].from) += factor; - has_shear = true; - } - } - } - - // Build per-axis scale matrix. - auto compute_scale_factor = [](BeltScaleMode mode, double angle_deg) -> double { - if (mode == BeltScaleMode::None) return 1.; - double angle_rad = Geometry::deg2rad(angle_deg); - double sin_a = std::sin(angle_rad); - double cos_a = std::cos(angle_rad); - switch (mode) { - case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; - case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; - case BeltScaleMode::Sin: return sin_a; - case BeltScaleMode::Cos: return cos_a; - default: return 1.; - } - }; - - Transform3d belt_scale = Transform3d::Identity(); - bool has_scale = false; - double sx = compute_scale_factor(print_config.belt_scale_x.value, print_config.belt_scale_x_angle.value); - double sy = compute_scale_factor(print_config.belt_scale_y.value, print_config.belt_scale_y_angle.value); - double sz = compute_scale_factor(print_config.belt_scale_z.value, print_config.belt_scale_z_angle.value); - if (std::abs(sx - 1.) > EPSILON || std::abs(sy - 1.) > EPSILON || std::abs(sz - 1.) > EPSILON) { - belt_scale.matrix()(0, 0) = sx; - belt_scale.matrix()(1, 1) = sy; - belt_scale.matrix()(2, 2) = sz; - has_scale = true; - } - - // Apply: scale * shear * trafo (shear first, then scale). - if (has_shear || has_scale) - params_base.trafo = belt_scale * belt_shear * params_base.trafo; - - // After pre-remap/shear/scale, the mesh may clip through the build - // plate (Z < 0). Detect this and shift the mesh up along slicer Z. - if (has_preslice_remap || has_shear || has_scale) { - Transform3d combined = params_base.trafo; - double min_z = std::numeric_limits::max(); - for (const ModelVolume *mv : model_volumes) { - if (!mv->is_model_part()) continue; - for (const stl_vertex &v : mv->mesh().its.vertices) { - Vec3d pt = combined * v.cast(); - min_z = std::min(min_z, pt.z()); - } - } - double belt_z_shift_val = (min_z < 0. && min_z != std::numeric_limits::max()) ? -min_z : 0.; - BOOST_LOG_TRIVIAL(warning) << "Belt Z-shift: min_z=" << min_z - << " z_shift=" << belt_z_shift_val - << " trafo_z=" << object_trafo.matrix()(2, 3); - if (belt_z_shift_val > 0.) { - Transform3d z_shift = Transform3d::Identity(); - z_shift.matrix()(2, 3) = belt_z_shift_val; - params_base.trafo = z_shift * params_base.trafo; - } - if (out_belt_min_z) - *out_belt_min_z = (min_z != std::numeric_limits::max()) ? min_z : 0.; - } + { + // Belt printer: apply pre-slice transforms (remap, shear, scale, z-shift) via strategy. + auto belt_strategy = BeltSliceStrategy::create(print_config); + if (belt_strategy) + belt_strategy->apply_to_trafo(params_base.trafo, model_volumes, out_belt_min_z); } //BBS: 0.0025mm is safe enough to simplify the data to speed slicing up for high-resolution model. //Also has on influence on arc fitting which has default resolution 0.0125mm. @@ -968,11 +834,8 @@ void PrintObject::slice() // 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(); - bool has_preslice_remap = (int(pcfg.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || - int(pcfg.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || - int(pcfg.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ)); - double belt_surface_z = has_preslice_remap - ? belt_remapped_bbox(*this->model_object(), pcfg).min.z() : 0.; + 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; } @@ -1025,18 +888,6 @@ void PrintObject::slice() << " belt_shear_z_global=" << pcfg.belt_shear_z_global.value << " object=" << this->model_object()->name; if (pcfg.belt_printer.value) { - auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { - double angle_rad = Geometry::deg2rad(angle_deg); - double sin_a = std::sin(angle_rad); - double cos_a = std::cos(angle_rad); - switch (mode) { - case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; - case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; - case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; - case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; - default: return 0.; - } - }; Point inst_shift = this->instances().empty() ? Point(0, 0) : this->instances().front().shift - this->center_offset(); @@ -1058,7 +909,7 @@ void PrintObject::slice() // PrintObjects so the lowest-positioned object stays at Z=0. const auto &za = gaxes[2]; // Z row if (za.global && za.mode != BeltShearMode::None && za.from < 2) { - double factor = compute_shear_factor(za.mode, za.angle); + double factor = BeltTransformPipeline::compute_shear_factor(za.mode, za.angle); // The global Z offset accounts for the instance's position- // dependent shear contribution. m_belt_min_z is the minimum Z // of the mesh after pre_remap + shear + trafo_centered, which @@ -1066,12 +917,8 @@ void PrintObject::slice() // Subtract the belt surface's centered Z position so we get // only the shear-induced contribution (same correction as the // belt_floor_z_shift fix). - // Same pre-remap guard as belt_floor_z_shift above. - bool has_preslice_remap2 = (int(pcfg.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || - int(pcfg.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || - int(pcfg.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ)); - double belt_surface_z = has_preslice_remap2 - ? belt_remapped_bbox(*this->model_object(), this->print()->config()).min.z() : 0.; + double belt_surface_z = BeltTransformPipeline::has_preslice_remap(pcfg) + ? BeltTransformPipeline::remap_bbox(*this->model_object(), pcfg).min.z() : 0.; double shear_min_z = m_belt_min_z - belt_surface_z; Point phys = inst_shift; // already has center_offset subtracted double center_on_axis = (za.from == 0) ? unscale(phys.x()) : unscale(phys.y()); diff --git a/src/libslic3r/Support/BeltFloorContext.cpp b/src/libslic3r/Support/BeltFloorContext.cpp new file mode 100644 index 0000000000..d2ffcfd397 --- /dev/null +++ b/src/libslic3r/Support/BeltFloorContext.cpp @@ -0,0 +1,124 @@ +#include "BeltFloorContext.hpp" + +#include +#include + +namespace Slic3r { + +bool BeltFloorContext::init(const SlicingParameters &sp, const PrintConfig &pcfg) +{ + m_active = false; + m_shear_factor = sp.belt_floor_shear_factor; + m_from_axis = sp.belt_floor_from_axis; + m_z_shift = sp.belt_floor_z_shift; + m_floor_offset = pcfg.belt_support_floor_offset.value; + + if (std::abs(m_shear_factor) < EPSILON) + return false; + + m_active = true; + return true; +} + +bool BeltFloorContext::init_local(const SlicingParameters &sp, const PrintConfig &pcfg, + double global_z_offset) +{ + if (!init(sp, pcfg)) + return false; + // Local Z: subtract the global Z offset so polygon computation + // works in the object's local coordinate space. + m_z_shift -= global_z_offset; + return true; +} + +Polygons BeltFloorContext::surface_polygon(coordf_t print_z) const +{ + return half_plane(print_z, /*belt_surface=*/true); +} + +Polygons BeltFloorContext::valid_region_polygon(coordf_t print_z) const +{ + return half_plane(print_z, /*belt_surface=*/false); +} + +double BeltFloorContext::floor_print_z(const Point &pos_slicing) const +{ + if (!m_active) + return -std::numeric_limits::infinity(); + double pos = unscale(m_from_axis == 0 ? pos_slicing.x() : pos_slicing.y()); + return m_shear_factor * pos + m_floor_offset + m_z_shift; +} + +std::vector BeltFloorContext::compute_per_layer_floors( + size_t num_layers, + const std::function &layer_print_z) const +{ + std::vector result(num_layers); + if (!m_active) + return result; + for (size_t i = 0; i < num_layers; ++i) + result[i] = surface_polygon(layer_print_z(i)); + return result; +} + +Polygons BeltFloorContext::half_plane(coordf_t print_z, bool belt_surface) const +{ + if (!m_active) + return {}; + + const double cutoff = (print_z - m_z_shift - m_floor_offset) / m_shear_factor; + const coord_t cutoff_scaled = scale_(cutoff); + const coord_t large_bound = scale_(1e3); + + // The belt surface is on one side of the cutoff line; the valid region + // is on the other side. Which side depends on shear_factor sign. + // + // belt_surface=true → the belt side (where support should NOT exist) + // belt_surface=false → the valid side (where support IS allowed) + // + // For shear_factor > 0: belt surface is from_axis >= cutoff + // For shear_factor < 0: belt surface is from_axis <= cutoff + bool high_side = (m_shear_factor > 0) == belt_surface; + + Polygon poly; + if (m_from_axis == 0) { + if (high_side) { + // X >= cutoff + poly.points = { + Point(cutoff_scaled, -large_bound), + Point(large_bound, -large_bound), + Point(large_bound, large_bound), + Point(cutoff_scaled, large_bound) + }; + } else { + // X < cutoff + poly.points = { + Point(-large_bound, -large_bound), + Point(cutoff_scaled, -large_bound), + Point(cutoff_scaled, large_bound), + Point(-large_bound, large_bound) + }; + } + } else { + if (high_side) { + // Y >= cutoff + poly.points = { + Point(-large_bound, cutoff_scaled), + Point( large_bound, cutoff_scaled), + Point( large_bound, large_bound), + Point(-large_bound, large_bound) + }; + } else { + // Y < cutoff + poly.points = { + Point(-large_bound, -large_bound), + Point( large_bound, -large_bound), + Point( large_bound, cutoff_scaled), + Point(-large_bound, cutoff_scaled) + }; + } + } + return { poly }; +} + +} // namespace Slic3r diff --git a/src/libslic3r/Support/BeltFloorContext.hpp b/src/libslic3r/Support/BeltFloorContext.hpp new file mode 100644 index 0000000000..7b8b704ef6 --- /dev/null +++ b/src/libslic3r/Support/BeltFloorContext.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include "../libslic3r.h" +#include "../Point.hpp" +#include "../Polygon.hpp" +#include "../Slicing.hpp" +#include "../PrintConfig.hpp" + +namespace Slic3r { + +class PrintObject; + +// Belt floor context: encapsulates the parameters and polygon computation +// for belt printer floor clipping in support generation. +// +// All belt floor code across SupportMaterial, TreeSupport, TreeSupport3D, +// and TreeModelVolumes uses the same 4 parameters and the same 4-case +// polygon construction. This class consolidates that logic. +// +// Construct once per PrintObject, then call surface_polygon() or +// valid_region_polygon() per layer with the layer's print_z. +class BeltFloorContext +{ +public: + BeltFloorContext() = default; + + // Initialize from slicing parameters and print config. + // Uses global Z coordinates (for SupportMaterial, non-organic TreeSupport). + bool init(const SlicingParameters &sp, const PrintConfig &pcfg); + + // Initialize with a Z offset subtracted from z_shift. + // Uses local Z coordinates (for TreeSupport3D, TreeModelVolumes organic pipeline). + bool init_local(const SlicingParameters &sp, const PrintConfig &pcfg, + double global_z_offset); + + bool is_active() const { return m_active; } + + // Compute the belt-side half-plane polygon at a given print_z. + // This is the region where the belt surface exists. + Polygons surface_polygon(coordf_t print_z) const; + + // Compute the valid-region half-plane polygon at a given print_z. + // This is the complement: the region where support is allowed. + Polygons valid_region_polygon(coordf_t print_z) const; + + // Compute the belt floor Z position at a given XY position (in slicing coords). + // Returns -infinity if not active. + double floor_print_z(const Point &pos_slicing) const; + + // Pre-compute belt floor polygons for a range of layers. + // layer_print_z(i) returns the print_z for layer index i. + std::vector compute_per_layer_floors( + size_t num_layers, + const std::function &layer_print_z) const; + + // Accessors + double shear_factor() const { return m_shear_factor; } + int from_axis() const { return m_from_axis; } + double z_shift() const { return m_z_shift; } + double floor_offset() const { return m_floor_offset; } + +private: + bool m_active = false; + double m_shear_factor = 0.0; + int m_from_axis = 1; // 0=X, 1=Y + double m_z_shift = 0.0; + double m_floor_offset = 0.0; + + // Internal: compute the raw half-plane polygon. + // If belt_surface=true, returns the belt side; otherwise the valid (complement) side. + Polygons half_plane(coordf_t print_z, bool belt_surface) const; +}; + +} // namespace Slic3r diff --git a/src/libslic3r/Support/SupportMaterial.cpp b/src/libslic3r/Support/SupportMaterial.cpp index d70153a7dd..c1f732bc6d 100644 --- a/src/libslic3r/Support/SupportMaterial.cpp +++ b/src/libslic3r/Support/SupportMaterial.cpp @@ -5,6 +5,7 @@ #include "Print.hpp" #include "SupportMaterial.hpp" #include "SupportCommon.hpp" +#include "BeltFloorContext.hpp" #include "Geometry.hpp" #include "Point.hpp" #include "MutablePolygon.hpp" @@ -625,64 +626,10 @@ Polygons belt_floor_surface_polygon( const PrintObject &object, coordf_t print_z) { - const double shear_factor = slicing_params.belt_floor_shear_factor; - if (std::abs(shear_factor) < EPSILON) + BeltFloorContext ctx; + if (!ctx.init(slicing_params, print_config)) return {}; - - const int from_axis = slicing_params.belt_floor_from_axis; // 0=X, 1=Y - const double floor_offset = print_config.belt_support_floor_offset.value; - - // Belt floor line in slicing coordinates: Z = sf * Y + z_shift. - // z_shift accounts for the upward shift applied when post-shear geometry - // extends below the bed (overhangs). Solving for Y: - // cutoff = (print_z - z_shift - floor_offset) / sf - const double z_shift = slicing_params.belt_floor_z_shift; - const double cutoff = (print_z - z_shift - floor_offset) / shear_factor; - const coord_t cutoff_scaled = scale_(cutoff); - const coord_t large_bound = scale_(1e4); - - // Build the belt-side half-plane (inverted from the valid region). - // If shear_factor > 0: valid region is from_axis < cutoff, so belt surface is from_axis >= cutoff. - // If shear_factor < 0: valid region is from_axis > cutoff, so belt surface is from_axis <= cutoff. - Polygon belt_poly; - if (from_axis == 0) { - if (shear_factor > 0) { - // Belt surface: X >= cutoff - belt_poly.points = { - Point(cutoff_scaled, -large_bound), - Point(large_bound, -large_bound), - Point(large_bound, large_bound), - Point(cutoff_scaled, large_bound) - }; - } else { - // Belt surface: X <= cutoff - belt_poly.points = { - Point(-large_bound, -large_bound), - Point(cutoff_scaled, -large_bound), - Point(cutoff_scaled, large_bound), - Point(-large_bound, large_bound) - }; - } - } else { - if (shear_factor > 0) { - // Belt surface: Y >= cutoff - belt_poly.points = { - Point(-large_bound, cutoff_scaled), - Point( large_bound, cutoff_scaled), - Point( large_bound, large_bound), - Point(-large_bound, large_bound) - }; - } else { - // Belt surface: Y <= cutoff - belt_poly.points = { - Point(-large_bound, -large_bound), - Point( large_bound, -large_bound), - Point( large_bound, cutoff_scaled), - Point(-large_bound, cutoff_scaled) - }; - } - } - return { belt_poly }; + return ctx.surface_polygon(print_z); } // Belt printer: compute the valid-region half-plane polygon at a given print_z. @@ -694,58 +641,10 @@ static Polygons belt_floor_valid_region_polygon( const PrintObject &object, coordf_t print_z) { - const double shear_factor = slicing_params.belt_floor_shear_factor; - if (std::abs(shear_factor) < EPSILON) + BeltFloorContext ctx; + if (!ctx.init(slicing_params, print_config)) return {}; - - const int from_axis = slicing_params.belt_floor_from_axis; - const double floor_offset = print_config.belt_support_floor_offset.value; - - const double z_shift = slicing_params.belt_floor_z_shift; - const double cutoff = (print_z - z_shift - floor_offset) / shear_factor; - const coord_t cutoff_scaled = scale_(cutoff); - const coord_t large_bound = scale_(1e4); - - // Valid region: the complement of the belt surface polygon. - Polygon valid_poly; - if (from_axis == 0) { - if (shear_factor > 0) { - // Valid: X < cutoff - valid_poly.points = { - Point(-large_bound, -large_bound), - Point(cutoff_scaled, -large_bound), - Point(cutoff_scaled, large_bound), - Point(-large_bound, large_bound) - }; - } else { - // Valid: X > cutoff - valid_poly.points = { - Point(cutoff_scaled, -large_bound), - Point(large_bound, -large_bound), - Point(large_bound, large_bound), - Point(cutoff_scaled, large_bound) - }; - } - } else { - if (shear_factor > 0) { - // Valid: Y < cutoff - valid_poly.points = { - Point(-large_bound, -large_bound), - Point( large_bound, -large_bound), - Point( large_bound, cutoff_scaled), - Point(-large_bound, cutoff_scaled) - }; - } else { - // Valid: Y > cutoff - valid_poly.points = { - Point(-large_bound, cutoff_scaled), - Point( large_bound, cutoff_scaled), - Point( large_bound, large_bound), - Point(-large_bound, large_bound) - }; - } - } - return { valid_poly }; + return ctx.valid_region_polygon(print_z); } // Collect outer contours of all slices of this layer. @@ -3373,22 +3272,18 @@ static void trim_support_layers_by_belt_floor( const PrintObject &object, SupportGeneratorLayersPtr &support_layers) { - if (std::abs(slicing_params.belt_floor_shear_factor) < EPSILON) + 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(0, support_layers.size()), [&](const tbb::blocked_range &range) { - for (size_t i = range.begin(); i < range.end(); ++ i) { - SupportGeneratorLayer *layer = support_layers[i]; - if (layer->polygons.empty()) - continue; - Polygons belt_surface = belt_floor_surface_polygon( - slicing_params, print_config, object, layer->print_z); - if (! belt_surface.empty()) - layer->polygons = diff(layer->polygons, belt_surface); - } + for (size_t i = range.begin(); i < range.end(); ++i) + if (support_layers[i]) + support_layers[i]->polygons = diff(support_layers[i]->polygons, + ctx.surface_polygon(support_layers[i]->print_z)); }); } diff --git a/src/libslic3r/Support/TreeModelVolumes.cpp b/src/libslic3r/Support/TreeModelVolumes.cpp index ec38ffed73..ebc3d2c958 100644 --- a/src/libslic3r/Support/TreeModelVolumes.cpp +++ b/src/libslic3r/Support/TreeModelVolumes.cpp @@ -8,6 +8,7 @@ #include "TreeModelVolumes.hpp" #include "TreeSupportCommon.hpp" +#include "BeltFloorContext.hpp" #include "../BuildVolume.hpp" #include "../ClipperUtils.hpp" @@ -101,13 +102,11 @@ TreeModelVolumes::TreeModelVolumes( { const auto &sp = print_object.slicing_parameters(); const auto &pcfg = print_object.print()->config(); - const double sf = sp.belt_floor_shear_factor; - if (std::abs(sf) > EPSILON + 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) { - const int from_axis = sp.belt_floor_from_axis; - const double floor_off = pcfg.belt_support_floor_offset.value; - const double z_shift = sp.belt_floor_z_shift - print_object.belt_global_z_offset(); size_t num_layers_needed = print_object.layer_count(); // Ensure m_anti_overhang is large enough. if (m_anti_overhang.size() < num_layers_needed) @@ -115,22 +114,7 @@ TreeModelVolumes::TreeModelVolumes( for (size_t layer_idx = 0; layer_idx < num_layers_needed; ++layer_idx) { double print_z = print_object.get_layer(layer_idx)->print_z - print_object.belt_global_z_offset(); - double cutoff = (print_z - z_shift - floor_off) / sf; - coord_t cutoff_sc = scale_(cutoff); - coord_t big = scale_(1e4); - Polygon belt_poly; - if (from_axis == 0) { - if (sf > 0) - belt_poly.points = {{cutoff_sc,-big},{big,-big},{big,big},{cutoff_sc,big}}; - else - belt_poly.points = {{-big,-big},{cutoff_sc,-big},{cutoff_sc,big},{-big,big}}; - } else { - if (sf > 0) - belt_poly.points = {{-big,cutoff_sc},{big,cutoff_sc},{big,big},{-big,big}}; - else - belt_poly.points = {{-big,-big},{big,-big},{big,cutoff_sc},{-big,cutoff_sc}}; - } - append(m_anti_overhang[layer_idx], Polygons{belt_poly}); + append(m_anti_overhang[layer_idx], ctx.surface_polygon(print_z)); } } } @@ -180,40 +164,20 @@ TreeModelVolumes::TreeModelVolumes( // Branches grow toward the belt and their slices are clipped at the belt // surface in organic_draw_branches(). The organic pipeline works in LOCAL // Z (no global_z_offset), so use local z_shift and local print_z. - const auto &slicing_params = print_object.slicing_parameters(); - const auto &pcfg = print_object.print()->config(); - const double sf = slicing_params.belt_floor_shear_factor; - if (std::abs(sf) > EPSILON - && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { - const int from_axis = slicing_params.belt_floor_from_axis; - const double floor_off = pcfg.belt_support_floor_offset.value; - // Subtract global_z_offset to get the LOCAL z_shift — the organic - // pipeline's Z coordinates don't include the global offset. - const double z_shift = slicing_params.belt_floor_z_shift - - print_object.belt_global_z_offset(); - m_belt_floor.assign(num_layers, Polygons{}); - for (size_t layer_idx = 0; layer_idx < num_layers; ++layer_idx) { - // Use local print_z (subtract global offset from object layer). - double print_z = (layer_idx >= num_raft_layers) - ? print_object.get_layer(layer_idx - num_raft_layers)->print_z - - print_object.belt_global_z_offset() - : 0.; - double cutoff = (print_z - z_shift - floor_off) / sf; - coord_t cutoff_sc = scale_(cutoff); - coord_t big = scale_(1e4); - Polygon belt_poly; - if (from_axis == 0) { - if (sf > 0) - belt_poly.points = {{cutoff_sc,-big},{big,-big},{big,big},{cutoff_sc,big}}; - else - belt_poly.points = {{-big,-big},{cutoff_sc,-big},{cutoff_sc,big},{-big,big}}; - } else { - if (sf > 0) - belt_poly.points = {{-big,cutoff_sc},{big,cutoff_sc},{big,big},{-big,big}}; - else - belt_poly.points = {{-big,-big},{big,-big},{big,cutoff_sc},{-big,cutoff_sc}}; - } - m_belt_floor[layer_idx] = { belt_poly }; + { + const auto &slicing_params = print_object.slicing_parameters(); + 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) { + m_belt_floor = ctx.compute_per_layer_floors(num_layers, [&](size_t layer_idx) -> double { + // Use local print_z (subtract global offset from object layer). + return (layer_idx >= num_raft_layers) + ? print_object.get_layer(layer_idx - num_raft_layers)->print_z + - print_object.belt_global_z_offset() + : 0.; + }); } } } diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 60e681add9..6c359115da 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -14,6 +14,7 @@ #include "TreeSupportCommon.hpp" #include "TreeSupport.hpp" #include "TreeSupport3D.hpp" +#include "BeltFloorContext.hpp" #include #include @@ -639,16 +640,10 @@ TreeSupport::TreeSupport(PrintObject& object, const SlicingParameters &slicing_p double TreeSupport::belt_floor_print_z(const Point &pos_slicing) const { - double sf = m_slicing_params.belt_floor_shear_factor; - if (std::abs(sf) < EPSILON) - return -std::numeric_limits::max(); // no belt floor - int from = m_slicing_params.belt_floor_from_axis; - // Belt floor in slicing coords: Z = sf * Y + z_shift + floor_offset. - // Inverse of cutoff = (Z - z_shift - floor_offset) / sf. - double pos = unscale(from == 0 ? pos_slicing.x() : pos_slicing.y()); - double floor_offset = m_print_config->belt_support_floor_offset.value; - double z_shift = m_slicing_params.belt_floor_z_shift; - return sf * pos + floor_offset + z_shift; + BeltFloorContext ctx; + if (!ctx.init(m_slicing_params, *m_print_config)) + return -std::numeric_limits::infinity(); + return ctx.floor_print_z(pos_slicing); } #define SUPPORT_SURFACES_OFFSET_PARAMETERS ClipperLib::jtSquare, 0. @@ -1738,18 +1733,11 @@ void TreeSupport::generate() // layer and clipped at the belt surface. These layers bypass the tree // algorithm entirely — they're pure geometry added after draw_circles(). { - const auto &sp = m_slicing_params; - const auto &pcfg = *m_print_config; - const double sf = sp.belt_floor_shear_factor; - if (std::abs(sf) > EPSILON - && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly + BeltFloorContext ctx; + if (ctx.init(m_slicing_params, *m_print_config) + && m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly && m_object->support_layer_count() > 0) { - const int from_axis = sp.belt_floor_from_axis; - const double floor_off = pcfg.belt_support_floor_offset.value; - // Support layer print_z values are in GLOBAL Z (non-organic inherits - // from object layers which include global_z_offset). Use the GLOBAL - // belt_floor_z_shift to match. - const double z_shift = sp.belt_floor_z_shift; + const auto &sp = m_slicing_params; // Find the lowest non-empty, non-brim support layer. ExPolygons source_areas; double source_z = 0; @@ -1778,7 +1766,7 @@ void TreeSupport::generate() } if (!source_areas.empty()) { BoundingBoxf3 bb = belt_remapped_bbox(*m_object->model_object(), m_object->print()->config()); - double from_extent = std::abs(bb.min(from_axis)); + double from_extent = std::abs(bb.min(ctx.from_axis())); double bb_min_z = std::abs(bb.min.z()); double first_z = m_object->get_support_layer(0)->print_z; // Depth = from-axis extent + pre-shear bbox Z offset (ensure_on_bed @@ -1792,18 +1780,8 @@ void TreeSupport::generate() 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; - double cutoff = (print_z - z_shift - floor_off) / sf; - coord_t cutoff_sc = scale_(cutoff); - coord_t big = scale_(1e3); - Polygon belt_poly; - if (from_axis == 0) { - if (sf > 0) belt_poly.points = {{cutoff_sc,-big},{big,-big},{big,big},{cutoff_sc,big}}; - else belt_poly.points = {{-big,-big},{cutoff_sc,-big},{cutoff_sc,big},{-big,big}}; - } else { - if (sf > 0) belt_poly.points = {{-big,cutoff_sc},{big,cutoff_sc},{big,big},{-big,big}}; - else belt_poly.points = {{-big,-big},{big,-big},{big,cutoff_sc},{-big,cutoff_sc}}; - } - ExPolygons clipped = diff_ex(source_areas, Polygons{belt_poly}); + 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; @@ -2250,37 +2228,19 @@ void TreeSupport::draw_circles() // Belt floor: clip tree support polygons by the belt surface plane. // ts_layer->print_z is at LOCAL Z (global offset applied later in - // _generate_support_material), but belt_floor_z_shift includes - // global_z_offset — subtract it to get the cutoff in local coords. - if (std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON - && m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { - const double sf = m_slicing_params.belt_floor_shear_factor; - const int from_axis = m_slicing_params.belt_floor_from_axis; - const double floor_off = m_print_config->belt_support_floor_offset.value; - const double z_shift_local = m_slicing_params.belt_floor_z_shift - - m_object->belt_global_z_offset(); - const double cutoff = (ts_layer->print_z - z_shift_local - floor_off) / sf; - const coord_t cutoff_sc = scale_(cutoff); - const coord_t big = scale_(1e4); - - Polygon belt_poly; - if (from_axis == 0) { - if (sf > 0) - belt_poly.points = { {cutoff_sc,-big}, {big,-big}, {big,big}, {cutoff_sc,big} }; - else - belt_poly.points = { {-big,-big}, {cutoff_sc,-big}, {cutoff_sc,big}, {-big,big} }; - } else { - if (sf > 0) - belt_poly.points = { {-big,cutoff_sc}, {big,cutoff_sc}, {big,big}, {-big,big} }; - else - belt_poly.points = { {-big,-big}, {big,-big}, {big,cutoff_sc}, {-big,cutoff_sc} }; + // _generate_support_material), so use init_local to subtract + // belt_global_z_offset from z_shift. + { + BeltFloorContext ctx; + if (ctx.init_local(m_slicing_params, *m_print_config, m_object->belt_global_z_offset()) + && m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + 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); + roof_1st_layer = diff_ex(roof_1st_layer, belt_surface); + floor_areas = diff_ex(floor_areas, belt_surface); + roof_gap_areas = diff_ex(roof_gap_areas, belt_surface); } - Polygons belt_surface = { belt_poly }; - base_areas = diff_ex(base_areas, belt_surface); - roof_areas = diff_ex(roof_areas, belt_surface); - roof_1st_layer = diff_ex(roof_1st_layer, belt_surface); - floor_areas = diff_ex(floor_areas, belt_surface); - roof_gap_areas = diff_ex(roof_gap_areas, belt_surface); } if (SQUARE_SUPPORT) { @@ -3593,6 +3553,20 @@ TreeSupportData::TreeSupportData(const PrintObject &object, coordf_t xy_distance poly.simplify(scale_(m_radius_sample_resolution), &outline); } + // Belt floor: add belt surface polygon to layer outlines so the + // collision system treats the belt as a physical surface. + { + 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) { + Polygons belt_surface = ctx.surface_polygon(local_print_z); + for (auto &p : belt_surface) + outline.emplace_back(ExPolygon(p)); + } + } + if (layer_nr == 0) m_layer_outlines_below.push_back(outline); else diff --git a/src/libslic3r/Support/TreeSupport3D.cpp b/src/libslic3r/Support/TreeSupport3D.cpp index 9d35cb2dd8..8a07609930 100644 --- a/src/libslic3r/Support/TreeSupport3D.cpp +++ b/src/libslic3r/Support/TreeSupport3D.cpp @@ -19,6 +19,7 @@ #include "Polygon.hpp" #include "Polyline.hpp" #include "MutablePolygon.hpp" +#include "BeltFloorContext.hpp" #include "SupportCommon.hpp" #include "TriangleMeshSlicer.hpp" #include "TreeSupport.hpp" @@ -3373,8 +3374,9 @@ static void generate_support_areas(Print &print, TreeSupport* tree_support, cons PrintObject &po = *print.get_object(processing.second.front()); const auto &sp = po.slicing_parameters(); const auto &pcfg = po.print()->config(); - const double sf = sp.belt_floor_shear_factor; - if (std::abs(sf) > EPSILON && std::abs(po.belt_global_z_offset()) > EPSILON + 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 @@ -3598,32 +3600,16 @@ static void generate_support_areas(Print &print, TreeSupport* tree_support, cons // Compute the belt floor polygon directly from each layer's print_z // rather than mapping to a layer index (avoids index mismatch issues). { - const auto &sp = print_object.slicing_parameters(); - const double sf = sp.belt_floor_shear_factor; - const double z_shift = sp.belt_floor_z_shift - print_object.belt_global_z_offset(); - const double floor_off = print_object.print()->config().belt_support_floor_offset.value; - const int from_axis = sp.belt_floor_from_axis; - if (std::abs(sf) > EPSILON - && print_object.print()->config().belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + 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() + && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { tbb::parallel_for_each(layers_sorted.begin(), layers_sorted.end(), [&](SupportGeneratorLayer *layer) { if (!layer || layer->polygons.empty()) return; - double cutoff = (layer->print_z - z_shift - floor_off) / sf; - coord_t cutoff_sc = scale_(cutoff); - coord_t big = scale_(1e4); - Polygon belt_poly; - if (from_axis == 0) { - if (sf > 0) - belt_poly.points = {{cutoff_sc,-big},{big,-big},{big,big},{cutoff_sc,big}}; - else - belt_poly.points = {{-big,-big},{cutoff_sc,-big},{cutoff_sc,big},{-big,big}}; - } else { - if (sf > 0) - belt_poly.points = {{-big,cutoff_sc},{big,cutoff_sc},{big,big},{-big,big}}; - else - belt_poly.points = {{-big,-big},{big,-big},{big,cutoff_sc},{-big,cutoff_sc}}; - } - layer->polygons = diff(layer->polygons, Polygons{belt_poly}); + layer->polygons = diff(layer->polygons, ctx.surface_polygon(layer->print_z)); }); } } From c8a1bf3a99f61ec972d0691e2ad46077ce415945 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 28 Mar 2026 02:13:15 -0500 Subject: [PATCH 019/224] Part 3.2: decouple axis remapping, enable viewing settings in Developer mode or when Belt mode is active --- src/libslic3r/BeltGCode.cpp | 14 ++---- src/libslic3r/BeltGCodeWriter.cpp | 25 ++-------- src/libslic3r/BeltGCodeWriter.hpp | 8 +--- src/libslic3r/BeltSliceStrategy.cpp | 15 ++---- src/libslic3r/BeltSliceStrategy.hpp | 10 ++-- src/libslic3r/BeltTransform.cpp | 18 ++++---- src/libslic3r/BeltTransform.hpp | 14 +++--- src/libslic3r/GCode.cpp | 13 ++++++ src/libslic3r/GCode/BeltBackTransform.cpp | 2 +- src/libslic3r/GCode/BeltBackTransform.hpp | 2 +- src/libslic3r/GCode/GCodeProcessor.cpp | 34 +++++++------- src/libslic3r/GCode/GCodeProcessor.hpp | 12 ++--- src/libslic3r/GCodeWriter.cpp | 56 +++++++++++++++++++++-- src/libslic3r/GCodeWriter.hpp | 15 ++++++ src/libslic3r/Preset.cpp | 4 +- src/libslic3r/Print.cpp | 12 ++--- src/libslic3r/PrintConfig.cpp | 52 ++++++++++----------- src/libslic3r/PrintConfig.hpp | 18 ++++---- src/libslic3r/PrintObjectSlice.cpp | 24 +++++++++- src/slic3r/GUI/Tab.cpp | 21 +++++---- 20 files changed, 215 insertions(+), 154 deletions(-) diff --git a/src/libslic3r/BeltGCode.cpp b/src/libslic3r/BeltGCode.cpp index dc1b6ca439..40dabf98dc 100644 --- a/src/libslic3r/BeltGCode.cpp +++ b/src/libslic3r/BeltGCode.cpp @@ -15,13 +15,7 @@ void BeltGCode::init_belt_writer(Print &print, bool is_bbl_printers) auto belt_writer = std::make_unique(); belt_writer->set_is_bbl_machine(is_bbl_printers); belt_writer->set_belt_angle(print.config().belt_printer_angle.value); - belt_writer->set_axis_remap( - int(print.config().belt_gcode_remap_x.value), - int(print.config().belt_gcode_remap_y.value), - int(print.config().belt_gcode_remap_z.value)); - BoundingBoxf bbox_bed(print.config().printable_area.values); - belt_writer->set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(), - print.config().printable_height.value)); + // Axis remap and build volume max are set by base GCode after init_belt_writer returns. belt_writer->set_belt_back_transform(print.config()); m_writer = std::move(belt_writer); @@ -59,9 +53,9 @@ void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print) file.write_format("; belt_scale_z = %s\n", full_cfg.opt_serialize("belt_scale_z").c_str()); file.write_format("; belt_scale_z_angle = %.1f\n", print.config().belt_scale_z_angle.value); // Pre-slice remap configs - file.write_format("; belt_preslice_remap_x = %s\n", full_cfg.opt_serialize("belt_preslice_remap_x").c_str()); - file.write_format("; belt_preslice_remap_y = %s\n", full_cfg.opt_serialize("belt_preslice_remap_y").c_str()); - file.write_format("; belt_preslice_remap_z = %s\n", full_cfg.opt_serialize("belt_preslice_remap_z").c_str()); + file.write_format("; preslice_remap_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()); } void BeltGCode::on_set_origin(const PrintObject *obj, const Point &inst_shift) diff --git a/src/libslic3r/BeltGCodeWriter.cpp b/src/libslic3r/BeltGCodeWriter.cpp index 645bf85a5b..db2bc46fe1 100644 --- a/src/libslic3r/BeltGCodeWriter.cpp +++ b/src/libslic3r/BeltGCodeWriter.cpp @@ -10,18 +10,6 @@ void BeltGCodeWriter::set_belt_angle(double angle_deg) m_belt_angle_rad = Geometry::deg2rad(angle_deg); } -void BeltGCodeWriter::set_axis_remap(int rx, int ry, int rz) -{ - m_remap_x = rx; - m_remap_y = ry; - m_remap_z = rz; -} - -void BeltGCodeWriter::set_build_volume_max(const Vec3d &max) -{ - m_build_vol_max = max; -} - void BeltGCodeWriter::set_belt_back_transform(const PrintConfig &config) { m_belt_back_transform.init_from_config(config); @@ -40,16 +28,9 @@ Vec3d BeltGCodeWriter::to_machine_coords(const Vec3d &pos) const { // Step 1: Undo the shear/scale applied during slicing. Vec3d p = m_belt_back_transform.apply(pos); - // Step 2: Apply axis remapping for the machine's coordinate convention. - // BeltRemapAxis: 0-2 = +X/+Y/+Z, 3-5 = -X/-Y/-Z, 6-8 = Rev X/Y/Z - auto remap = [this, &p](int r) -> double { - int axis = r % 3; - if (r < 3) return p[axis]; - if (r < 6) return -p[axis]; - return m_build_vol_max[axis] - p[axis]; - }; - Vec3d result = { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) }; - // Per-axis origin snap: shift so bbox min on each enabled axis = offset. + // Step 2: Apply axis remap (uses inherited base class method). + Vec3d result = apply_axis_remap(p); + // Step 3: Per-axis origin snap. for (int i = 0; i < 3; ++i) if (m_origin_snap[i]) result[i] -= (m_origin_bbox_min[i] - m_origin_offset[i]); diff --git a/src/libslic3r/BeltGCodeWriter.hpp b/src/libslic3r/BeltGCodeWriter.hpp index 727135b578..a41f890503 100644 --- a/src/libslic3r/BeltGCodeWriter.hpp +++ b/src/libslic3r/BeltGCodeWriter.hpp @@ -15,11 +15,9 @@ class BeltGCodeWriter : public GCodeWriter public: BeltGCodeWriter() : GCodeWriter() {} - // Belt configuration + // Belt configuration (axis remap is inherited from GCodeWriter) void set_belt_angle(double angle_deg); bool is_belt_printer() const { return m_belt_angle_rad != 0.; } - void set_axis_remap(int rx, int ry, int rz); - void set_build_volume_max(const Vec3d &max); void set_belt_back_transform(const PrintConfig &config); void set_origin_snap(int axis, bool enable, double offset, double bbox_min); Vec3d to_machine_coords(const Vec3d &pos) const; @@ -37,10 +35,6 @@ protected: private: double m_belt_angle_rad = 0.; - int m_remap_x = 0; - int m_remap_y = 1; - int m_remap_z = 2; - Vec3d m_build_vol_max = Vec3d::Zero(); BeltBackTransform m_belt_back_transform; bool m_origin_snap[3] = {false, false, false}; double m_origin_offset[3] = {0., 0., 0.}; diff --git a/src/libslic3r/BeltSliceStrategy.cpp b/src/libslic3r/BeltSliceStrategy.cpp index b3adeec64a..c1a300869e 100644 --- a/src/libslic3r/BeltSliceStrategy.cpp +++ b/src/libslic3r/BeltSliceStrategy.cpp @@ -13,31 +13,24 @@ std::unique_ptr BeltSliceStrategy::create(const PrintConfig & BeltSliceStrategy::BeltSliceStrategy(const PrintConfig &config) { - m_has_remap = BeltTransformPipeline::has_preslice_remap(config); - if (m_has_remap) - m_pre_remap = BeltTransformPipeline::build_preslice_remap(config); - m_shear = BeltTransformPipeline::build_shear_matrix(config, &m_has_shear); m_scale = BeltTransformPipeline::build_scale_matrix(config, &m_has_scale); } void BeltSliceStrategy::apply_to_trafo(Transform3d &trafo, const ModelVolumePtrs &model_volumes, + bool has_remap, double *out_belt_min_z) const { - // Step 1: Pre-slice axis remap. - if (m_has_remap) - trafo = m_pre_remap * trafo; - - // Step 2: Shear + scale. + // Shear + scale (belt-only; pre-slice remap is handled separately). if (m_has_shear || m_has_scale) { Transform3d belt_xform = Transform3d::Identity(); belt_xform.linear() = m_scale * m_shear; trafo = belt_xform * trafo; } - // Step 3: Z-shift — detect if mesh clips below build plate after transforms. - if (m_has_remap || m_has_shear || m_has_scale) { + // Z-shift — detect if mesh clips below build plate after transforms. + if (has_remap || m_has_shear || m_has_scale) { double min_z = std::numeric_limits::max(); for (const ModelVolume *mv : model_volumes) { if (!mv->is_model_part()) continue; diff --git a/src/libslic3r/BeltSliceStrategy.hpp b/src/libslic3r/BeltSliceStrategy.hpp index bf19243fd2..ef6e038ee7 100644 --- a/src/libslic3r/BeltSliceStrategy.hpp +++ b/src/libslic3r/BeltSliceStrategy.hpp @@ -23,21 +23,19 @@ public: // Create a strategy if belt_printer is enabled; returns nullptr otherwise. static std::unique_ptr create(const PrintConfig &config); - // Apply belt transforms to the slicing trafo. - // Modifies trafo in-place: trafo = z_shift * scale * shear * pre_remap * trafo - // Scans all model_part volumes to detect minimum Z and add z-shift if needed. - // Sets *out_belt_min_z to the minimum Z of the mesh after transforms. + // Apply belt-specific transforms (shear + scale + z-shift) to the slicing trafo. + // Pre-slice remap is handled separately (standalone feature). + // has_remap: whether pre-slice remap was already applied (affects z-shift detection). void apply_to_trafo(Transform3d &trafo, const ModelVolumePtrs &model_volumes, + bool has_remap, double *out_belt_min_z) const; private: explicit BeltSliceStrategy(const PrintConfig &config); - bool m_has_remap = false; bool m_has_shear = false; bool m_has_scale = false; - Transform3d m_pre_remap = Transform3d::Identity(); Matrix3d m_shear = Matrix3d::Identity(); Matrix3d m_scale = Matrix3d::Identity(); }; diff --git a/src/libslic3r/BeltTransform.cpp b/src/libslic3r/BeltTransform.cpp index db7eea7d80..9bb66fcfb6 100644 --- a/src/libslic3r/BeltTransform.cpp +++ b/src/libslic3r/BeltTransform.cpp @@ -13,9 +13,9 @@ Transform3d BeltTransformPipeline::build_preslice_remap(const PrintConfig &confi if (!has_preslice_remap(config)) return pre_remap; - int pre_rx = int(config.belt_preslice_remap_x.value); - int pre_ry = int(config.belt_preslice_remap_y.value); - int pre_rz = int(config.belt_preslice_remap_z.value); + 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 { @@ -114,13 +114,13 @@ Transform3d BeltTransformPipeline::build_forward_transform(const PrintConfig &co BoundingBoxf3 BeltTransformPipeline::remap_bbox(const BoundingBoxf3 &bb, const PrintConfig &config) { - int pre_rx = int(config.belt_preslice_remap_x.value); - int pre_ry = int(config.belt_preslice_remap_y.value); - int pre_rz = int(config.belt_preslice_remap_z.value); + 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); - if (pre_rx == int(BeltRemapAxis::PosX) && - pre_ry == int(BeltRemapAxis::PosY) && - pre_rz == int(BeltRemapAxis::PosZ)) + if (pre_rx == int(RemapAxis::PosX) && + pre_ry == int(RemapAxis::PosY) && + pre_rz == int(RemapAxis::PosZ)) return bb; // Identity remap. auto remap_coord = [](int r, const Vec3d &v) -> double { diff --git a/src/libslic3r/BeltTransform.hpp b/src/libslic3r/BeltTransform.hpp index b9a60473e5..3c0665c252 100644 --- a/src/libslic3r/BeltTransform.hpp +++ b/src/libslic3r/BeltTransform.hpp @@ -58,21 +58,21 @@ public: static bool has_preslice_remap(const PrintConfig &config) { - return int(config.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || - int(config.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || - int(config.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ); + return 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) { auto get_int = [&](const char *key) -> int { - auto *opt = config.option>(key); + auto *opt = config.option>(key); return opt ? int(opt->value) : 0; }; - return get_int("belt_preslice_remap_x") != int(BeltRemapAxis::PosX) || - get_int("belt_preslice_remap_y") != int(BeltRemapAxis::PosY) || - get_int("belt_preslice_remap_z") != int(BeltRemapAxis::PosZ); + 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_shear(const PrintConfig &config) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index a330fad689..93b87cbba8 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2425,6 +2425,19 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Belt printer: initialize belt-specific writer via virtual hook. this->init_belt_writer(print, is_bbl_printers); + // Standalone axis remap (works with or without belt mode). + { + int rx = int(print.config().gcode_remap_x.value); + int ry = int(print.config().gcode_remap_y.value); + int rz = int(print.config().gcode_remap_z.value); + if (rx != 0 || ry != 1 || rz != 2) { + m_writer->set_axis_remap(rx, ry, rz); + BoundingBoxf bbox_bed(print.config().printable_area.values); + m_writer->set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(), + print.config().printable_height.value)); + } + } + // How many times will be change_layer() called? // change_layer() in turn increments the progress bar status. m_layer_count = 0; diff --git a/src/libslic3r/GCode/BeltBackTransform.cpp b/src/libslic3r/GCode/BeltBackTransform.cpp index 2e2cd26f75..b9cdac779f 100644 --- a/src/libslic3r/GCode/BeltBackTransform.cpp +++ b/src/libslic3r/GCode/BeltBackTransform.cpp @@ -8,7 +8,7 @@ bool BeltBackTransform::init_from_config(const PrintConfig &config) m_active = false; m_inverse = Transform3d::Identity(); - if (!config.belt_printer.value || !config.belt_gcode_back_transform.value) + if (!config.belt_printer.value || !config.gcode_back_transform.value) return false; // Require at least one active transform to proceed. diff --git a/src/libslic3r/GCode/BeltBackTransform.hpp b/src/libslic3r/GCode/BeltBackTransform.hpp index 5a75d94dc8..b98770c688 100644 --- a/src/libslic3r/GCode/BeltBackTransform.hpp +++ b/src/libslic3r/GCode/BeltBackTransform.hpp @@ -15,7 +15,7 @@ namespace Slic3r { // Initialized once from PrintConfig, then applied per-point in // GCodeWriter::to_machine_coords() before axis remapping. // -// Active when belt_gcode_back_transform is true AND at least one of: +// Active when gcode_back_transform is true AND at least one of: // - a shear axis has global mode enabled, or // - a pre-slice axis remap is non-identity. class BeltBackTransform diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 2288c9e670..e6b6403be5 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -3129,26 +3129,26 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers try { m_result.belt_scale_z_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; } // Pre-slice axis remap - auto parse_remap_axis = [](const std::string &s) -> BeltRemapAxis { - if (s == "pos_x") return BeltRemapAxis::PosX; - if (s == "pos_y") return BeltRemapAxis::PosY; - if (s == "pos_z") return BeltRemapAxis::PosZ; - if (s == "neg_x") return BeltRemapAxis::NegX; - if (s == "neg_y") return BeltRemapAxis::NegY; - if (s == "neg_z") return BeltRemapAxis::NegZ; - if (s == "rev_x") return BeltRemapAxis::RevX; - if (s == "rev_y") return BeltRemapAxis::RevY; - if (s == "rev_z") return BeltRemapAxis::RevZ; - return BeltRemapAxis::PosX; + 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, " belt_preslice_remap_x = ")) { - m_result.belt_preslice_remap_x = parse_remap_axis(trim(std::string(comment.substr(25)))); return; + if (boost::starts_with(comment, " preslice_remap_x = ")) { + m_result.preslice_remap_x = parse_remap_axis(trim(std::string(comment.substr(25)))); return; } - if (boost::starts_with(comment, " belt_preslice_remap_y = ")) { - m_result.belt_preslice_remap_y = parse_remap_axis(trim(std::string(comment.substr(25)))); return; + if (boost::starts_with(comment, " preslice_remap_y = ")) { + m_result.preslice_remap_y = parse_remap_axis(trim(std::string(comment.substr(25)))); return; } - if (boost::starts_with(comment, " belt_preslice_remap_z = ")) { - m_result.belt_preslice_remap_z = parse_remap_axis(trim(std::string(comment.substr(25)))); return; + if (boost::starts_with(comment, " preslice_remap_z = ")) { + m_result.preslice_remap_z = parse_remap_axis(trim(std::string(comment.substr(25)))); return; } } // wipe start tag diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index 7d08a9cdfd..4c6e4f6618 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -246,9 +246,9 @@ class Print; float belt_scale_y_angle{ 45.f }; BeltScaleMode belt_scale_z{ BeltScaleMode::None }; float belt_scale_z_angle{ 45.f }; - BeltRemapAxis belt_preslice_remap_x{ BeltRemapAxis::PosX }; - BeltRemapAxis belt_preslice_remap_y{ BeltRemapAxis::PosY }; - BeltRemapAxis belt_preslice_remap_z{ BeltRemapAxis::PosZ }; + 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; @@ -324,9 +324,9 @@ class Print; belt_scale_y_angle = other.belt_scale_y_angle; belt_scale_z = other.belt_scale_z; belt_scale_z_angle = other.belt_scale_z_angle; - belt_preslice_remap_x = other.belt_preslice_remap_x; - belt_preslice_remap_y = other.belt_preslice_remap_y; - belt_preslice_remap_z = other.belt_preslice_remap_z; + preslice_remap_x = other.preslice_remap_x; + preslice_remap_y = other.preslice_remap_y; + preslice_remap_z = other.preslice_remap_z; #if ENABLE_GCODE_VIEWER_STATISTICS time = other.time; #endif diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index 51f82ea009..424d0b722d 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -20,6 +20,36 @@ namespace Slic3r { bool GCodeWriter::full_gcode_comment = true; +void GCodeWriter::set_axis_remap(int rx, int ry, int rz) +{ + m_remap_x = rx; + m_remap_y = ry; + m_remap_z = rz; +} + +void GCodeWriter::set_build_volume_max(const Vec3d &max) +{ + m_build_vol_max = max; +} + +bool GCodeWriter::has_axis_remap() const +{ + return m_remap_x != 0 || m_remap_y != 1 || m_remap_z != 2; +} + +Vec3d GCodeWriter::apply_axis_remap(const Vec3d &pos) const +{ + if (!has_axis_remap()) + return pos; + auto remap = [this, &pos](int r) -> double { + int axis = r % 3; + if (r < 3) return pos[axis]; + if (r < 6) return -pos[axis]; + return m_build_vol_max[axis] - pos[axis]; + }; + return { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) }; +} + bool GCodeWriter::supports_separate_travel_acceleration(GCodeFlavor flavor) { return (flavor == gcfRepetier || flavor == gcfMarlinFirmware || flavor == gcfRepRapFirmware); @@ -562,7 +592,13 @@ std::string GCodeWriter::travel_to_xy(const Vec2d &point, const std::string &com Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; GCodeG1Formatter w; - w.emit_xy(point_on_plate); + if (has_axis_remap()) { + // Axis remap may couple XY with Z; emit full XYZ in machine coordinates. + Vec3d machine = apply_axis_remap(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); + w.emit_xyz(machine); + } else { + w.emit_xy(point_on_plate); + } auto speed = m_is_first_layer ? this->config.get_abs_value("initial_layer_travel_speed") : this->config.travel_speed.value; w.emit_f(speed * 60.0); @@ -794,7 +830,13 @@ std::string GCodeWriter::_travel_to_z(double z, const std::string &comment) } GCodeG1Formatter w; - w.emit_z(z); + if (has_axis_remap()) { + // Remap may couple Z with other axes; emit full XYZ. + Vec3d machine = apply_axis_remap(Vec3d(m_pos.x() - m_x_offset, m_pos.y() - m_y_offset, z)); + w.emit_xyz(machine); + } else { + w.emit_z(z); + } w.emit_f(speed * 60.0); //BBS w.emit_comment(GCodeWriter::full_gcode_comment, comment); @@ -899,7 +941,12 @@ std::string GCodeWriter::extrude_to_xy(const Vec2d &point, double dE, const std: Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; GCodeG1Formatter w; - w.emit_xy(point_on_plate); + if (has_axis_remap()) { + Vec3d machine = apply_axis_remap(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); + w.emit_xyz(machine); + } else { + w.emit_xy(point_on_plate); + } if (!force_no_extrusion) w.emit_e(filament()->E()); //BBS @@ -939,6 +986,9 @@ std::string GCodeWriter::extrude_to_xyz(const Vec3d &point, double dE, const std //BBS: take plate offset into consider Vec3d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset, point(2) }; + if (has_axis_remap()) + point_on_plate = apply_axis_remap(point_on_plate); + GCodeG1Formatter w; w.emit_xyz(point_on_plate); if (!force_no_extrusion) diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index c96c3ca2c1..6aa2ee8ffd 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -125,6 +125,12 @@ public: void set_is_first_layer(bool bval) { m_is_first_layer = bval; } GCodeFlavor get_gcode_flavor() const { return config.gcode_flavor; } + // Axis remap: permute/negate/reverse axes in G-code output. + // Works standalone (without belt mode) for printers with non-standard axis conventions. + void set_axis_remap(int rx, int ry, int rz); + void set_build_volume_max(const Vec3d &max); + bool has_axis_remap() const; + // Returns whether this flavor supports separate print and travel acceleration. static bool supports_separate_travel_acceleration(GCodeFlavor flavor); protected: @@ -141,6 +147,15 @@ protected: virtual std::string _travel_to_z(double z, const std::string &comment); + // Axis remap state — accessible to subclasses. + int m_remap_x = 0; // RemapAxis: 0=+X, 1=+Y, 2=+Z, 3=-X, etc. + int m_remap_y = 1; + int m_remap_z = 2; + Vec3d m_build_vol_max = Vec3d::Zero(); + + // Apply axis remap to a point. Returns pos unchanged if remap is identity. + Vec3d apply_axis_remap(const Vec3d &pos) const; + private: // Extruders are sorted by their ID, so that binary search is possible. std::vector m_filament_extruders; diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 737ee2d663..399480075e 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1014,8 +1014,8 @@ static std::vector s_Preset_printer_options { "belt_shear_y", "belt_shear_y_angle", "belt_shear_y_from", "belt_shear_y_global", "belt_shear_z", "belt_shear_z_angle", "belt_shear_z_from", "belt_shear_z_global", "belt_scale_x", "belt_scale_x_angle", "belt_scale_y", "belt_scale_y_angle", "belt_scale_z", "belt_scale_z_angle", - "belt_preslice_remap_x", "belt_preslice_remap_y", "belt_preslice_remap_z", - "belt_gcode_remap_x", "belt_gcode_remap_y", "belt_gcode_remap_z", "belt_gcode_back_transform", + "preslice_remap_x", "preslice_remap_y", "preslice_remap_z", + "gcode_remap_x", "gcode_remap_y", "gcode_remap_z", "gcode_back_transform", "belt_origin_snap_x", "belt_origin_offset_x", "belt_origin_snap_y", "belt_origin_offset_y", "belt_origin_snap_z", "belt_origin_offset_z", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 1f787a70cf..aca40e4952 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -101,9 +101,9 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n // or they are only notes not influencing the generated G-code. static std::unordered_set steps_gcode = { // Belt printer G-code axis remap (only affects G-code output, not slicing). - "belt_gcode_remap_x", - "belt_gcode_remap_y", - "belt_gcode_remap_z", + "gcode_remap_x", + "gcode_remap_y", + "gcode_remap_z", "belt_origin_snap_x", "belt_origin_offset_x", "belt_origin_snap_y", "belt_origin_offset_y", "belt_origin_snap_z", "belt_origin_offset_z", @@ -308,9 +308,9 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "belt_scale_y_angle" || opt_key == "belt_scale_z" || opt_key == "belt_scale_z_angle" - || opt_key == "belt_preslice_remap_x" - || opt_key == "belt_preslice_remap_y" - || opt_key == "belt_preslice_remap_z") { + || opt_key == "preslice_remap_x" + || opt_key == "preslice_remap_y" + || opt_key == "preslice_remap_z") { osteps.emplace_back(posSlice); } else if ( opt_key == "belt_support_floor_offset" diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 8c637b7f7c..0cb46c02f6 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -313,18 +313,18 @@ static t_config_enum_values s_keys_map_BeltScaleMode { }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltScaleMode) -static t_config_enum_values s_keys_map_BeltRemapAxis { - { "pos_x", int(BeltRemapAxis::PosX) }, - { "pos_y", int(BeltRemapAxis::PosY) }, - { "pos_z", int(BeltRemapAxis::PosZ) }, - { "neg_x", int(BeltRemapAxis::NegX) }, - { "neg_y", int(BeltRemapAxis::NegY) }, - { "neg_z", int(BeltRemapAxis::NegZ) }, - { "rev_x", int(BeltRemapAxis::RevX) }, - { "rev_y", int(BeltRemapAxis::RevY) }, - { "rev_z", int(BeltRemapAxis::RevZ) }, +static t_config_enum_values s_keys_map_RemapAxis { + { "pos_x", int(RemapAxis::PosX) }, + { "pos_y", int(RemapAxis::PosY) }, + { "pos_z", int(RemapAxis::PosZ) }, + { "neg_x", int(RemapAxis::NegX) }, + { "neg_y", int(RemapAxis::NegY) }, + { "neg_z", int(RemapAxis::NegZ) }, + { "rev_x", int(RemapAxis::RevX) }, + { "rev_y", int(RemapAxis::RevY) }, + { "rev_z", int(RemapAxis::RevZ) }, }; -CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltRemapAxis) +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(RemapAxis) static t_config_enum_values s_keys_map_BeltSupportFloorMode { { "none", int(BeltSupportFloorMode::None) }, @@ -6124,49 +6124,49 @@ void PrintConfigDef::init_fff_params() add_belt_scale_angle("belt_scale_z_angle", "Angle"); // G-code axis remap with sign - auto add_belt_remap = [this](const char *key, const char *label, const char *tooltip, BeltRemapAxis default_axis) { + auto add_belt_remap = [this](const char *key, const char *label, const char *tooltip, RemapAxis default_axis) { auto def = this->add(key, coEnum); def->label = L(label); def->category = L("Printable space"); def->tooltip = L(tooltip); - def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); def->enum_values = {"pos_x", "pos_y", "pos_z", "neg_x", "neg_y", "neg_z", "rev_x", "rev_y", "rev_z"}; def->enum_labels = {L("+X"), L("+Y"), L("+Z"), L("-X"), L("-Y"), L("-Z"), L("Rev X"), L("Rev Y"), L("Rev Z")}; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionEnum(default_axis)); + def->mode = comSimple; // Visibility controlled by toggle_line in Tab.cpp + def->set_default_value(new ConfigOptionEnum(default_axis)); }; - add_belt_remap("belt_preslice_remap_x", "X", + add_belt_remap("preslice_remap_x", "X", "Before slicing, which model-space axis becomes the slicer's X axis. " "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.", - BeltRemapAxis::PosX); - add_belt_remap("belt_preslice_remap_y", "Y", + RemapAxis::PosX); + 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.", - BeltRemapAxis::PosY); - add_belt_remap("belt_preslice_remap_z", "Z", + RemapAxis::PosY); + 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.", - BeltRemapAxis::PosZ); + RemapAxis::PosZ); - add_belt_remap("belt_gcode_remap_x", "X", "Which slicing axis maps to machine X in G-code output. Applied AFTER slicing, during G-code generation.", BeltRemapAxis::PosX); - add_belt_remap("belt_gcode_remap_y", "Y", "Which slicing axis maps to machine Y in G-code output. Applied AFTER slicing, during G-code generation.", BeltRemapAxis::PosY); - add_belt_remap("belt_gcode_remap_z", "Z", "Which slicing axis maps to machine Z in G-code output. Applied AFTER slicing, during G-code generation.", BeltRemapAxis::PosZ); + add_belt_remap("gcode_remap_x", "X", "Which slicing axis maps to machine X in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosX); + add_belt_remap("gcode_remap_y", "Y", "Which slicing axis maps to machine Y in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosY); + add_belt_remap("gcode_remap_z", "Z", "Which slicing axis maps to machine Z in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosZ); - def = this->add("belt_gcode_back_transform", coBool); + def = this->add("gcode_back_transform", coBool); def->label = L("G-code back-transform"); def->category = L("Printable space"); def->tooltip = L("Reverse the shear/scale transform applied during slicing so G-code " "coordinates are in the machine's physical coordinate space. " "Requires at least one shear axis with global mode enabled."); - def->mode = comAdvanced; + def->mode = comSimple; // Visibility controlled by toggle_line in Tab.cpp def->set_default_value(new ConfigOptionBool(false)); auto add_belt_origin_snap = [this](const char *key_snap, const char *key_offset, diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 13202657ef..5d9f977ed1 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -181,7 +181,7 @@ enum class BeltAxis Z = 2, }; -enum class BeltRemapAxis +enum class RemapAxis { PosX = 0, PosY = 1, PosZ = 2, NegX = 3, NegY = 4, NegZ = 5, @@ -549,7 +549,7 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SlicingMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltShearMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltScaleMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltAxis) -CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltRemapAxis) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(RemapAxis) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltSupportFloorMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltSupportZOffsetMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialPattern) @@ -1487,13 +1487,13 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionFloat, belt_scale_y_angle)) ((ConfigOptionEnum, belt_scale_z)) ((ConfigOptionFloat, belt_scale_z_angle)) - ((ConfigOptionEnum, belt_preslice_remap_x)) - ((ConfigOptionEnum, belt_preslice_remap_y)) - ((ConfigOptionEnum, belt_preslice_remap_z)) - ((ConfigOptionEnum, belt_gcode_remap_x)) - ((ConfigOptionEnum, belt_gcode_remap_y)) - ((ConfigOptionEnum, belt_gcode_remap_z)) - ((ConfigOptionBool, belt_gcode_back_transform)) + ((ConfigOptionEnum, preslice_remap_x)) + ((ConfigOptionEnum, preslice_remap_y)) + ((ConfigOptionEnum, preslice_remap_z)) + ((ConfigOptionEnum, gcode_remap_x)) + ((ConfigOptionEnum, gcode_remap_y)) + ((ConfigOptionEnum, gcode_remap_z)) + ((ConfigOptionBool, gcode_back_transform)) ((ConfigOptionBool, belt_origin_snap_x)) ((ConfigOptionFloat, belt_origin_offset_x)) ((ConfigOptionBool, belt_origin_snap_y)) diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 3067772f09..7279b83d14 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -144,11 +144,31 @@ static std::vector slice_volumes_inner( params_base.closing_radius = print_object_config.slice_closing_radius.value; params_base.extra_offset = 0; params_base.trafo = object_trafo; + // Standalone pre-slice axis remap (works without belt mode). + bool has_remap = BeltTransformPipeline::has_preslice_remap(print_config); + if (has_remap) + params_base.trafo = BeltTransformPipeline::build_preslice_remap(print_config) * params_base.trafo; { - // Belt printer: apply pre-slice transforms (remap, shear, scale, z-shift) via strategy. + // Belt-specific transforms (shear, scale, z-shift) via strategy. auto belt_strategy = BeltSliceStrategy::create(print_config); if (belt_strategy) - belt_strategy->apply_to_trafo(params_base.trafo, model_volumes, out_belt_min_z); + belt_strategy->apply_to_trafo(params_base.trafo, model_volumes, has_remap, out_belt_min_z); + } + // If only remap (no belt), still need z-shift detection. + if (has_remap && !print_config.belt_printer.value) { + double min_z = std::numeric_limits::max(); + for (const ModelVolume *mv : model_volumes) { + if (!mv->is_model_part()) continue; + for (const stl_vertex &v : mv->mesh().its.vertices) { + Vec3d pt = params_base.trafo * v.cast(); + min_z = std::min(min_z, pt.z()); + } + } + if (min_z < 0. && min_z != std::numeric_limits::max()) { + Transform3d z_shift = Transform3d::Identity(); + z_shift.matrix()(2, 3) = -min_z; + params_base.trafo = z_shift * params_base.trafo; + } } //BBS: 0.0025mm is safe enough to simplify the data to speed slicing up for high-resolution model. //Also has on influence on arc fitting which has default resolution 0.0125mm. diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 202c28c121..e43e979955 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4436,19 +4436,19 @@ void TabPrinter::build_fff() 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.append_option(optgroup->get_option("belt_preslice_remap_x")); - line.append_option(optgroup->get_option("belt_preslice_remap_y")); - line.append_option(optgroup->get_option("belt_preslice_remap_z")); + line.append_option(optgroup->get_option("preslice_remap_x")); + line.append_option(optgroup->get_option("preslice_remap_y")); + line.append_option(optgroup->get_option("preslice_remap_z")); optgroup->append_line(line); } { Line line = { L("G-code axis remap (post-slice)"), L("Remap slicing-frame axes to machine axes in G-code output. Applied AFTER slicing, during G-code generation.") }; - line.append_option(optgroup->get_option("belt_gcode_remap_x")); - line.append_option(optgroup->get_option("belt_gcode_remap_y")); - line.append_option(optgroup->get_option("belt_gcode_remap_z")); + line.append_option(optgroup->get_option("gcode_remap_x")); + line.append_option(optgroup->get_option("gcode_remap_y")); + line.append_option(optgroup->get_option("gcode_remap_z")); optgroup->append_line(line); } - optgroup->append_single_option_line("belt_gcode_back_transform"); + optgroup->append_single_option_line("gcode_back_transform"); { Line line = { L("Origin snap X"), L("Snap object bbox min X to offset in G-code output") }; line.append_option(optgroup->get_option("belt_origin_snap_x")); @@ -5321,11 +5321,14 @@ void TabPrinter::toggle_options() toggle_line("belt_printer_infinite_y", is_belt); for (auto el : {"belt_shear_x", "belt_shear_y", "belt_shear_z", "belt_scale_x", "belt_scale_y", "belt_scale_z", - "belt_preslice_remap_x", - "belt_gcode_remap_x", "belt_gcode_back_transform", "belt_origin_snap_x", "belt_origin_snap_y", "belt_origin_snap_z"}) toggle_line(el, is_belt); + // Remap options: visible when belt mode is on OR when UI is in developer mode. + bool show_remap = is_belt || (m_mode >= comDevelop); + for (auto el : {"preslice_remap_x", "gcode_remap_x", "gcode_back_transform"}) + toggle_line(el, show_remap); + // Gray out angle/from sub-options when their parent shear/scale mode is None. auto sx = m_config->option>("belt_shear_x")->value; toggle_option("belt_shear_x_angle", is_belt && sx != BeltShearMode::None); From 783acd932a1d97343f9467373036f48cfac324f4 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 28 Mar 2026 04:03:39 -0500 Subject: [PATCH 020/224] revert CLAUDE.md --- CLAUDE.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9bd162c42c..25aa516a56 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,7 +23,27 @@ cmake --build build/arm64 --config RelWithDebInfo --target all -- ### Building on Linux **Always use this command to build the project when testing build issues on Linux.** ```bash -systemd-run --user --scope -p MemoryMax=48G cmake --build build --config RelWithDebInfo --target all -- -j18 -l 24 +cmake --build build/arm64 --config RelWithDebInfo --target all -- + +``` +### Build test: + +**Always use this command to build the project when testing build issues on Windows.** +```bash +cmake --build . --config %build_type% --target ALL_BUILD -- -m +``` + +### Building on macOS +**Always use this command to build the project when testing build issues on macOS.** +```bash +cmake --build build/arm64 --config RelWithDebInfo --target all -- +``` + +### Building on Linux + **Always use this command to build the project when testing build issues on Linux.** +```bash +cmake --build build --config RelWithDebInfo --target all -- + ``` From e9a579b604ba3f937b22cfbf48986cab834a0b03 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sun, 29 Mar 2026 13:14:25 -0500 Subject: [PATCH 021/224] switch default shear axis, swap to tan(a) instead of cot(a) --- src/libslic3r/PrintConfig.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 0cb46c02f6..a601a35a17 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -6066,13 +6066,13 @@ void PrintConfigDef::init_fff_params() def->set_default_value(new ConfigOptionEnum(default_axis)); }; - auto add_belt_shear_global = [this](const char *key, const char *label) { + auto add_belt_shear_global = [this](const char *key, const char *label, bool default_val = false) { auto def = this->add(key, coBool); def->label = L(label); def->category = L("Printable space"); def->tooltip = L("Apply shear in global coordinates (position-aware) rather than object-local coordinates."); def->mode = comAdvanced; - def->set_default_value(new ConfigOptionBool(false)); + def->set_default_value(new ConfigOptionBool(default_val)); }; add_belt_shear_mode("belt_shear_x", "Function", BeltShearMode::None); @@ -6080,15 +6080,15 @@ void PrintConfigDef::init_fff_params() add_belt_axis_enum("belt_shear_x_from", "From", "Source axis for X shear.", BeltAxis::Z); add_belt_shear_global("belt_shear_x_global", "Global"); - add_belt_shear_mode("belt_shear_y", "Function", BeltShearMode::PosCot); + add_belt_shear_mode("belt_shear_y", "Function", BeltShearMode::None); add_belt_shear_angle("belt_shear_y_angle", "Angle"); add_belt_axis_enum("belt_shear_y_from", "From", "Source axis for Y shear.", BeltAxis::Z); add_belt_shear_global("belt_shear_y_global", "Global"); - add_belt_shear_mode("belt_shear_z", "Function", BeltShearMode::None); + add_belt_shear_mode("belt_shear_z", "Function", BeltShearMode::PosTan); add_belt_shear_angle("belt_shear_z_angle", "Angle"); add_belt_axis_enum("belt_shear_z_from", "From", "Source axis for Z shear.", BeltAxis::Y); - add_belt_shear_global("belt_shear_z_global", "Global"); + add_belt_shear_global("belt_shear_z_global", "Global", true); // Per-axis scale controls for belt printer auto add_belt_scale_mode = [this](const char *key, const char *label, BeltScaleMode default_mode) { From 1e9ee0c12050d4458d399447a3d00df983b60bc3 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Tue, 7 Apr 2026 10:37:34 +0800 Subject: [PATCH 022/224] add a generic belt printer --- resources/profiles/Custom.json | 26 ++++- .../Custom/Generic Belt Printer_cover.png | Bin 0 -> 30205 bytes .../machine/MyBeltPrinter 0.2 nozzle.json | 26 +++++ .../machine/MyBeltPrinter 0.4 nozzle.json | 20 ++++ .../machine/MyBeltPrinter 0.6 nozzle.json | 26 +++++ .../machine/MyBeltPrinter 0.8 nozzle.json | 26 +++++ .../Custom/machine/MyBeltPrinter.json | 12 +++ .../Custom/machine/fdm_belt_common.json | 98 ++++++++++++++++++ 8 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 resources/profiles/Custom/Generic Belt Printer_cover.png create mode 100644 resources/profiles/Custom/machine/MyBeltPrinter 0.2 nozzle.json create mode 100644 resources/profiles/Custom/machine/MyBeltPrinter 0.4 nozzle.json create mode 100644 resources/profiles/Custom/machine/MyBeltPrinter 0.6 nozzle.json create mode 100644 resources/profiles/Custom/machine/MyBeltPrinter 0.8 nozzle.json create mode 100644 resources/profiles/Custom/machine/MyBeltPrinter.json create mode 100644 resources/profiles/Custom/machine/fdm_belt_common.json diff --git a/resources/profiles/Custom.json b/resources/profiles/Custom.json index 37927a90fe..4c64f0a205 100644 --- a/resources/profiles/Custom.json +++ b/resources/profiles/Custom.json @@ -1,9 +1,13 @@ { "name": "Custom Printer", - "version": "02.03.02.60", + "version": "02.03.02.61", "force_update": "0", "description": "My configurations", "machine_model_list": [ + { + "name": "Generic Belt Printer", + "sub_path": "machine/MyBeltPrinter.json" + }, { "name": "Generic Klipper Printer", "sub_path": "machine/MyKlipper.json" @@ -246,6 +250,10 @@ "name": "MyKlipper 0.8 nozzle", "sub_path": "machine/MyKlipper 0.8 nozzle.json" }, + { + "name": "fdm_belt_common", + "sub_path": "machine/fdm_belt_common.json" + }, { "name": "fdm_toolchanger_common", "sub_path": "machine/fdm_toolchanger_common.json" @@ -254,6 +262,22 @@ "name": "MyRRF 0.4 nozzle", "sub_path": "machine/MyRRF 0.4 nozzle.json" }, + { + "name": "MyBeltPrinter 0.2 nozzle", + "sub_path": "machine/MyBeltPrinter 0.2 nozzle.json" + }, + { + "name": "MyBeltPrinter 0.4 nozzle", + "sub_path": "machine/MyBeltPrinter 0.4 nozzle.json" + }, + { + "name": "MyBeltPrinter 0.6 nozzle", + "sub_path": "machine/MyBeltPrinter 0.6 nozzle.json" + }, + { + "name": "MyBeltPrinter 0.8 nozzle", + "sub_path": "machine/MyBeltPrinter 0.8 nozzle.json" + }, { "name": "MyToolChanger 0.2 nozzle", "sub_path": "machine/MyToolChanger 0.2 nozzle.json" diff --git a/resources/profiles/Custom/Generic Belt Printer_cover.png b/resources/profiles/Custom/Generic Belt Printer_cover.png new file mode 100644 index 0000000000000000000000000000000000000000..d42f2ef228b2cfcdb0d16a525b4fde4d68b3010d GIT binary patch literal 30205 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 Date: Thu, 9 Apr 2026 23:48:43 -0500 Subject: [PATCH 023/224] add global mesh transform option --- resources/profiles/Custom.json | 2 +- .../Custom/machine/fdm_belt_common.json | 2 + src/libslic3r/BeltGCode.cpp | 35 +++++++++ src/libslic3r/GCode/BeltBackTransform.cpp | 4 +- src/libslic3r/Preset.cpp | 3 +- src/libslic3r/Print.hpp | 3 + src/libslic3r/PrintApply.cpp | 12 ++-- src/libslic3r/PrintConfig.cpp | 19 +++++ src/libslic3r/PrintConfig.hpp | 2 + src/libslic3r/PrintObjectSlice.cpp | 71 ++++++++++++------- src/slic3r/GUI/Tab.cpp | 22 ++++-- 11 files changed, 135 insertions(+), 40 deletions(-) diff --git a/resources/profiles/Custom.json b/resources/profiles/Custom.json index 4c64f0a205..2e697a7b54 100644 --- a/resources/profiles/Custom.json +++ b/resources/profiles/Custom.json @@ -1,6 +1,6 @@ { "name": "Custom Printer", - "version": "02.03.02.61", + "version": "02.03.02.62", "force_update": "0", "description": "My configurations", "machine_model_list": [ diff --git a/resources/profiles/Custom/machine/fdm_belt_common.json b/resources/profiles/Custom/machine/fdm_belt_common.json index dd8b166805..46603f7faf 100644 --- a/resources/profiles/Custom/machine/fdm_belt_common.json +++ b/resources/profiles/Custom/machine/fdm_belt_common.json @@ -84,11 +84,13 @@ ], "preslice_remap_y": "pos_z", "preslice_remap_z": "pos_y", + "preslice_remap_global": "1", "printer_extruder_id": [ "1" ], "belt_origin_snap_y": "1", "belt_printer": "1", + "belt_preslice_global": "1", "belt_shear_z": "pos_cot", "belt_shear_z_angle": "30", "build_plate_tilt_x": "45", diff --git a/src/libslic3r/BeltGCode.cpp b/src/libslic3r/BeltGCode.cpp index 40dabf98dc..a34acf95bd 100644 --- a/src/libslic3r/BeltGCode.cpp +++ b/src/libslic3r/BeltGCode.cpp @@ -56,10 +56,45 @@ void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print) 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); } 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). + // This replaces the bbox-based axis snap with an exact formula. + // + // Two flags trigger this path: + // belt_preslice_global — full pipeline (scale * shear * remap) is global + // preslice_remap_global — only the pre-slice remap is global + // The XY origin adjustment uses the FULL forward transform either way, + // because the back_transform applied during G-code emission is always the + // inverse of the full pipeline. When only the remap is configured, both + // flags produce identical math (T == R). + bool use_global = m_config.belt_preslice_global.value + || (m_config.preslice_remap_global.value + && BeltTransformPipeline::has_preslice_remap(m_config)); + if (use_global && m_config.belt_printer.value) { + auto *belt_writer = dynamic_cast(m_writer.get()); + if (belt_writer) { + // Clear snap — not needed with computed corrections + for (int a = 0; a < 3; ++a) + belt_writer->set_origin_snap(a, false, 0., 0.); + } + + // Adjust origin: transform through belt forward pipeline so that + // the back-transform correctly recovers model-space positions. + Transform3d T = BeltTransformPipeline::build_forward_transform(m_config); + Vec2d cur_origin = this->origin(); + Vec3d origin3d(cur_origin.x(), cur_origin.y(), 0.); + Vec3d adjusted = T.linear() * origin3d; + this->set_origin(Vec2d(adjusted.x(), adjusted.y())); + return; + } + if (!m_origin_snap[0] && !m_origin_snap[1] && !m_origin_snap[2]) return; diff --git a/src/libslic3r/GCode/BeltBackTransform.cpp b/src/libslic3r/GCode/BeltBackTransform.cpp index b9cdac779f..893c46e52b 100644 --- a/src/libslic3r/GCode/BeltBackTransform.cpp +++ b/src/libslic3r/GCode/BeltBackTransform.cpp @@ -15,7 +15,9 @@ bool BeltBackTransform::init_from_config(const PrintConfig &config) bool has_global_shear = config.belt_shear_x_global.value || config.belt_shear_y_global.value || config.belt_shear_z_global.value; - if (!has_global_shear && !BeltTransformPipeline::has_preslice_remap(config)) + bool has_preslice_global = config.belt_preslice_global.value + || config.preslice_remap_global.value; + if (!has_global_shear && !has_preslice_global && !BeltTransformPipeline::has_preslice_remap(config)) return false; // Build the forward pipeline (scale * shear * pre_remap) and store its inverse. diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 399480075e..4d01757826 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1014,8 +1014,9 @@ static std::vector s_Preset_printer_options { "belt_shear_y", "belt_shear_y_angle", "belt_shear_y_from", "belt_shear_y_global", "belt_shear_z", "belt_shear_z_angle", "belt_shear_z_from", "belt_shear_z_global", "belt_scale_x", "belt_scale_x_angle", "belt_scale_y", "belt_scale_y_angle", "belt_scale_z", "belt_scale_z_angle", - "preslice_remap_x", "preslice_remap_y", "preslice_remap_z", + "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_preslice_global", "belt_origin_snap_x", "belt_origin_offset_x", "belt_origin_snap_y", "belt_origin_offset_y", "belt_origin_snap_z", "belt_origin_offset_z", diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index 9a5d000e1c..074ff52bf3 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -585,9 +585,12 @@ private: double m_belt_global_z_offset { 0.0 }; // 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. + Vec2d m_belt_global_xy_correction { Vec2d::Zero() }; public: double belt_global_z_offset() const { return m_belt_global_z_offset; } double belt_min_z() const { return m_belt_min_z; } + Vec2d belt_global_xy_correction() const { return m_belt_global_xy_correction; } private: diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index 7f3b41b0f1..a68c390477 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1,6 +1,7 @@ #include "ClipperUtils.hpp" #include "Model.hpp" #include "Print.hpp" +#include "BeltTransform.hpp" #include #include @@ -1520,8 +1521,10 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ // 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_shear_z_global.value - && m_config.belt_shear_z.value != BeltShearMode::None; + bool belt_force_separate = m_config.belt_printer.value && ( + (m_config.belt_shear_z_global.value && m_config.belt_shear_z.value != BeltShearMode::None) + || m_config.belt_preslice_global.value + || (m_config.preslice_remap_global.value && BeltTransformPipeline::has_preslice_remap(m_config))); model_object_status.print_instances = print_objects_from_model_object(*model_object, this->shrinkage_compensation(), belt_force_separate); std::vector old; old.reserve(print_object_status_db.count(*model_object)); @@ -1612,8 +1615,9 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ // min_shift across all objects, so one move affects everyone). if (belt_instances_shifted && m_config.belt_printer.value - && m_config.belt_shear_z_global.value - && m_config.belt_shear_z.value != BeltShearMode::None) { + && ((m_config.belt_shear_z_global.value && m_config.belt_shear_z.value != BeltShearMode::None) + || m_config.belt_preslice_global.value + || (m_config.preslice_remap_global.value && BeltTransformPipeline::has_preslice_remap(m_config)))) { for (PrintObject *object : m_objects) update_apply_status(object->invalidate_step(posSlice)); } diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index a601a35a17..47d0b9c549 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -6156,6 +6156,16 @@ void PrintConfigDef::init_fff_params() "Rev mode mirrors relative to the build volume maximum. Default +Z: no change.", RemapAxis::PosZ); + 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 per-axis 'Global' option on belt mesh shears, but for the remap."); + def->mode = comAdvanced; + 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); add_belt_remap("gcode_remap_y", "Y", "Which slicing axis maps to machine Y in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosY); add_belt_remap("gcode_remap_z", "Z", "Which slicing axis maps to machine Z in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosZ); @@ -6169,6 +6179,15 @@ void PrintConfigDef::init_fff_params() def->mode = comSimple; // Visibility controlled by toggle_line in Tab.cpp def->set_default_value(new ConfigOptionBool(false)); + 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 = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + auto add_belt_origin_snap = [this](const char *key_snap, const char *key_offset, const char *axis_label) { auto def = this->add(key_snap, coBool); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 5d9f977ed1..d17c46f0aa 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1490,10 +1490,12 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionEnum, preslice_remap_x)) ((ConfigOptionEnum, preslice_remap_y)) ((ConfigOptionEnum, preslice_remap_z)) + ((ConfigOptionBool, preslice_remap_global)) ((ConfigOptionEnum, gcode_remap_x)) ((ConfigOptionEnum, gcode_remap_y)) ((ConfigOptionEnum, gcode_remap_z)) ((ConfigOptionBool, gcode_back_transform)) + ((ConfigOptionBool, belt_preslice_global)) ((ConfigOptionBool, belt_origin_snap_x)) ((ConfigOptionFloat, belt_origin_offset_x)) ((ConfigOptionBool, belt_origin_snap_y)) diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 7279b83d14..7711610004 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -906,6 +906,7 @@ void PrintObject::slice() BOOST_LOG_TRIVIAL(warning) << "Belt global check: belt_printer=" << pcfg.belt_printer.value << " belt_shear_z=" << int(pcfg.belt_shear_z.value) << " belt_shear_z_global=" << pcfg.belt_shear_z_global.value + << " belt_preslice_global=" << pcfg.belt_preslice_global.value << " object=" << this->model_object()->name; if (pcfg.belt_printer.value) { @@ -916,37 +917,53 @@ void PrintObject::slice() << " shift=(" << unscale(inst_shift.x()) << ", " << unscale(inst_shift.y()) << ")"; double global_z_offset = 0.; - struct GAxis { BeltShearMode mode; double angle; int from; bool global; }; - GAxis gaxes[3] = { - { pcfg.belt_shear_x.value, pcfg.belt_shear_x_angle.value, int(pcfg.belt_shear_x_from.value), pcfg.belt_shear_x_global.value }, - { pcfg.belt_shear_y.value, pcfg.belt_shear_y_angle.value, int(pcfg.belt_shear_y_from.value), pcfg.belt_shear_y_global.value }, - { pcfg.belt_shear_z.value, pcfg.belt_shear_z_angle.value, int(pcfg.belt_shear_z_from.value), pcfg.belt_shear_z_global.value }, - }; + 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. + Transform3d T = BeltTransformPipeline::build_forward_transform(pcfg); + Vec3d d(unscale(inst_shift.x()), unscale(inst_shift.y()), 0.); + Vec3d c = T.linear() * d - d; - // Only the Z-row shear contributes a Z offset from global mode. - // (X/Y row shears with global would offset X/Y, not Z — not useful here.) - // Offsets are RELATIVE: we subtract the minimum shift across all - // PrintObjects so the lowest-positioned object stays at Z=0. - const auto &za = gaxes[2]; // Z row - if (za.global && za.mode != BeltShearMode::None && za.from < 2) { - double factor = BeltTransformPipeline::compute_shear_factor(za.mode, za.angle); - // The global Z offset accounts for the instance's position- - // dependent shear contribution. m_belt_min_z is the minimum Z - // of the mesh after pre_remap + shear + trafo_centered, which - // includes the centering offset on the remapped Z axis. - // Subtract the belt surface's centered Z position so we get - // only the shear-induced contribution (same correction as the - // belt_floor_z_shift fix). - double belt_surface_z = BeltTransformPipeline::has_preslice_remap(pcfg) - ? BeltTransformPipeline::remap_bbox(*this->model_object(), pcfg).min.z() : 0.; - double shear_min_z = m_belt_min_z - belt_surface_z; - Point phys = inst_shift; // already has center_offset subtracted - double center_on_axis = (za.from == 0) ? unscale(phys.x()) : unscale(phys.y()); - global_z_offset += center_on_axis * factor + shear_min_z; + global_z_offset = c.z(); + m_belt_global_xy_correction = Vec2d(c.x(), c.y()); + + BOOST_LOG_TRIVIAL(warning) << "Belt preslice_global: correction=(" + << c.x() << ", " << c.y() << ", " << c.z() << ")"; + } else { + struct GAxis { BeltShearMode mode; double angle; int from; bool global; }; + GAxis gaxes[3] = { + { pcfg.belt_shear_x.value, pcfg.belt_shear_x_angle.value, int(pcfg.belt_shear_x_from.value), pcfg.belt_shear_x_global.value }, + { pcfg.belt_shear_y.value, pcfg.belt_shear_y_angle.value, int(pcfg.belt_shear_y_from.value), pcfg.belt_shear_y_global.value }, + { pcfg.belt_shear_z.value, pcfg.belt_shear_z_angle.value, int(pcfg.belt_shear_z_from.value), pcfg.belt_shear_z_global.value }, + }; + + // Only the Z-row shear contributes a Z offset from global mode. + // (X/Y row shears with global would offset X/Y, not Z — not useful here.) + const auto &za = gaxes[2]; // Z row + if (za.global && za.mode != BeltShearMode::None && za.from < 2) { + double factor = BeltTransformPipeline::compute_shear_factor(za.mode, za.angle); + double belt_surface_z = BeltTransformPipeline::has_preslice_remap(pcfg) + ? BeltTransformPipeline::remap_bbox(*this->model_object(), pcfg).min.z() : 0.; + double shear_min_z = m_belt_min_z - belt_surface_z; + Point phys = inst_shift; + double center_on_axis = (za.from == 0) ? unscale(phys.x()) : unscale(phys.y()); + global_z_offset += center_on_axis * factor + shear_min_z; + } + + // 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(inst_shift.x()), unscale(inst_shift.y()), 0.); + Vec3d remap_correction = R.linear() * d - d; + global_z_offset += remap_correction.z(); + } } BOOST_LOG_TRIVIAL(warning) << "Belt global: z_offset=" << global_z_offset - << " za.global=" << za.global << " za.mode=" << int(za.mode) << " za.from=" << za.from << " (relative to min across " << this->print()->objects().size() << " objects)"; m_belt_global_z_offset = global_z_offset; if (std::abs(global_z_offset) > EPSILON) { diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index e43e979955..986ff614f6 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4439,6 +4439,7 @@ void TabPrinter::build_fff() line.append_option(optgroup->get_option("preslice_remap_x")); line.append_option(optgroup->get_option("preslice_remap_y")); line.append_option(optgroup->get_option("preslice_remap_z")); + line.append_option(optgroup->get_option("preslice_remap_global")); optgroup->append_line(line); } { @@ -4448,6 +4449,7 @@ void TabPrinter::build_fff() line.append_option(optgroup->get_option("gcode_remap_z")); optgroup->append_line(line); } + optgroup->append_single_option_line("belt_preslice_global"); optgroup->append_single_option_line("gcode_back_transform"); { Line line = { L("Origin snap X"), L("Snap object bbox min X to offset in G-code output") }; @@ -5328,22 +5330,29 @@ void TabPrinter::toggle_options() bool show_remap = is_belt || (m_mode >= comDevelop); for (auto el : {"preslice_remap_x", "gcode_remap_x", "gcode_back_transform"}) toggle_line(el, show_remap); + toggle_line("belt_preslice_global", show_remap); + + 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", show_remap && !belt_global); // Gray out angle/from sub-options when their parent shear/scale mode is None. + // Per-axis globals are superseded when belt_preslice_global is on. auto sx = m_config->option>("belt_shear_x")->value; toggle_option("belt_shear_x_angle", is_belt && sx != BeltShearMode::None); toggle_option("belt_shear_x_from", is_belt && sx != BeltShearMode::None); - toggle_option("belt_shear_x_global", is_belt && sx != BeltShearMode::None); + toggle_option("belt_shear_x_global", is_belt && sx != BeltShearMode::None && !belt_global); auto sy = m_config->option>("belt_shear_y")->value; toggle_option("belt_shear_y_angle", is_belt && sy != BeltShearMode::None); toggle_option("belt_shear_y_from", is_belt && sy != BeltShearMode::None); - toggle_option("belt_shear_y_global", is_belt && sy != BeltShearMode::None); + toggle_option("belt_shear_y_global", is_belt && sy != BeltShearMode::None && !belt_global); auto sz = m_config->option>("belt_shear_z")->value; toggle_option("belt_shear_z_angle", is_belt && sz != BeltShearMode::None); toggle_option("belt_shear_z_from", is_belt && sz != BeltShearMode::None); - toggle_option("belt_shear_z_global", is_belt && sz != BeltShearMode::None); + toggle_option("belt_shear_z_global", is_belt && sz != BeltShearMode::None && !belt_global); auto scx = m_config->option>("belt_scale_x")->value; toggle_option("belt_scale_x_angle", is_belt && scx != BeltScaleMode::None); @@ -5354,9 +5363,10 @@ void TabPrinter::toggle_options() auto scz = m_config->option>("belt_scale_z")->value; toggle_option("belt_scale_z_angle", is_belt && scz != BeltScaleMode::None); - toggle_option("belt_origin_offset_x", is_belt && m_config->opt_bool("belt_origin_snap_x")); - toggle_option("belt_origin_offset_y", is_belt && m_config->opt_bool("belt_origin_snap_y")); - toggle_option("belt_origin_offset_z", is_belt && m_config->opt_bool("belt_origin_snap_z")); + // Origin snap is superseded by belt_preslice_global + toggle_option("belt_origin_offset_x", is_belt && m_config->opt_bool("belt_origin_snap_x") && !belt_global); + toggle_option("belt_origin_offset_y", is_belt && m_config->opt_bool("belt_origin_snap_y") && !belt_global); + toggle_option("belt_origin_offset_z", is_belt && m_config->opt_bool("belt_origin_snap_z") && !belt_global); toggle_line("belt_support_floor_mode", is_belt); } From bc6d0ef0fbb4755c9f323bcf58bcd2756803979a Mon Sep 17 00:00:00 2001 From: Joseph Robertson <32232925+HarrierPigeon@users.noreply.github.com> Date: Mon, 13 Apr 2026 22:29:23 -0500 Subject: [PATCH 024/224] Add first layer detection and fan control - prototype --- .claude/scheduled_tasks.lock | 1 + src/libslic3r/BeltGCodeWriter.cpp | 36 +++- src/libslic3r/BeltGCodeWriter.hpp | 15 ++ src/libslic3r/CMakeLists.txt | 2 + src/libslic3r/FirstLayerPlane.cpp | 226 ++++++++++++++++++++++++++ src/libslic3r/FirstLayerPlane.hpp | 76 +++++++++ src/libslic3r/GCode.cpp | 72 +++++++- src/libslic3r/GCode.hpp | 33 ++++ src/libslic3r/GCode/CoolingBuffer.cpp | 223 +++++++++++++++++++++++++ src/libslic3r/GCode/CoolingBuffer.hpp | 21 ++- src/libslic3r/Preset.cpp | 1 + src/libslic3r/PrintConfig.cpp | 54 ++++++ src/libslic3r/PrintConfig.hpp | 24 +++ src/slic3r/GUI/Tab.cpp | 20 +++ 14 files changed, 792 insertions(+), 12 deletions(-) create mode 100644 .claude/scheduled_tasks.lock create mode 100644 src/libslic3r/FirstLayerPlane.cpp create mode 100644 src/libslic3r/FirstLayerPlane.hpp diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock new file mode 100644 index 0000000000..cce3208834 --- /dev/null +++ b/.claude/scheduled_tasks.lock @@ -0,0 +1 @@ +{"sessionId":"597ab29b-cd70-40f4-95d9-fee98b3968ad","pid":1159657,"acquiredAt":1775936463783} \ No newline at end of file diff --git a/src/libslic3r/BeltGCodeWriter.cpp b/src/libslic3r/BeltGCodeWriter.cpp index db2bc46fe1..ea90f2588e 100644 --- a/src/libslic3r/BeltGCodeWriter.cpp +++ b/src/libslic3r/BeltGCodeWriter.cpp @@ -1,8 +1,27 @@ #include "BeltGCodeWriter.hpp" +#include "FirstLayerPlane.hpp" #include "Geometry.hpp" namespace Slic3r { +namespace { + +// Decide whether a particular destination point gets first-layer treatment. +// When the plane evaluator is active, distance from the plane wins; otherwise +// fall back to the layer-coarse m_is_first_layer flag set by the caller. +inline bool belt_point_on_first_layer( + const FirstLayerPlane *plane, + double first_layer_thickness_mm, + bool layer_first_flag, + const Vec3d &point_slicing_mm) +{ + if (plane && plane->is_active()) + return plane->is_first_layer(point_slicing_mm, first_layer_thickness_mm); + return layer_first_flag; +} + +} // namespace + // ---- Belt configuration --------------------------------------------------- void BeltGCodeWriter::set_belt_angle(double angle_deg) @@ -52,7 +71,10 @@ std::string BeltGCodeWriter::travel_to_xy(const Vec2d &point, const std::string GCodeG1Formatter w; w.emit_xyz(machine); - auto speed = m_is_first_layer + const bool first_layer_for_point = belt_point_on_first_layer( + m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer, + Vec3d(point.x(), point.y(), m_pos.z())); + auto speed = first_layer_for_point ? this->config.get_abs_value("initial_layer_travel_speed") : this->config.travel_speed.value; w.emit_f(speed * 60.0); w.emit_comment(GCodeWriter::full_gcode_comment, comment); @@ -78,8 +100,11 @@ std::string BeltGCodeWriter::_travel_to_z(double z, const std::string &comment) double speed = this->config.travel_speed_z.value; if (speed == 0.) { - speed = m_is_first_layer ? this->config.get_abs_value("initial_layer_travel_speed") - : this->config.travel_speed.value; + const bool first_layer_for_point = belt_point_on_first_layer( + m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer, + Vec3d(m_pos.x(), m_pos.y(), z)); + speed = first_layer_for_point ? this->config.get_abs_value("initial_layer_travel_speed") + : this->config.travel_speed.value; } // Belt printer: a Z-only move in slicing frame needs to emit both Y and Z in machine coords. @@ -142,8 +167,11 @@ std::string BeltGCodeWriter::travel_to_xyz(const Vec3d &point, const std::string // 3. Lift type forced to NormalLift (handled by lazy_lift/eager_lift overrides) Vec3d dest_point = point; + const bool first_layer_for_point = belt_point_on_first_layer( + m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer, point); auto travel_speed = - m_is_first_layer ? this->config.get_abs_value("initial_layer_travel_speed") : this->config.travel_speed.value; + first_layer_for_point ? this->config.get_abs_value("initial_layer_travel_speed") + : this->config.travel_speed.value; // Handle pending z_hop if (std::abs(m_to_lift) > EPSILON) { diff --git a/src/libslic3r/BeltGCodeWriter.hpp b/src/libslic3r/BeltGCodeWriter.hpp index a41f890503..828dd43bb3 100644 --- a/src/libslic3r/BeltGCodeWriter.hpp +++ b/src/libslic3r/BeltGCodeWriter.hpp @@ -5,6 +5,8 @@ namespace Slic3r { +class FirstLayerPlane; + // Belt-printer-specific GCode writer. // // Inherits from GCodeWriter and overrides movement methods to apply @@ -22,6 +24,16 @@ public: void set_origin_snap(int axis, bool enable, double offset, double bbox_min); Vec3d to_machine_coords(const Vec3d &pos) const; + // First-layer plane: when set to a non-null active evaluator, travel + // speed selection consults the plane per-move and uses + // initial_layer_travel_speed for points within first_layer_height_mm + // of the plane (regardless of slicing layer index). + void set_first_layer_plane(const FirstLayerPlane *plane, + double first_layer_height_mm) { + m_first_layer_plane = plane; + m_first_layer_thickness_mm = first_layer_height_mm; + } + // Overridden movement methods std::string travel_to_xy(const Vec2d &point, const std::string &comment = std::string()) override; std::string travel_to_xyz(const Vec3d &point, const std::string &comment = std::string(), bool force_z = false) override; @@ -39,6 +51,9 @@ private: bool m_origin_snap[3] = {false, false, false}; double m_origin_offset[3] = {0., 0., 0.}; double m_origin_bbox_min[3] = {0., 0., 0.}; + // Borrowed pointer; lifetime owned by GCode. null = inactive. + const FirstLayerPlane *m_first_layer_plane = nullptr; + double m_first_layer_thickness_mm = 0.; }; } // namespace Slic3r diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index a3e813b1da..689dee8c83 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -87,6 +87,8 @@ set(lisbslic3r_sources BeltSliceStrategy.hpp BeltTransform.cpp BeltTransform.hpp + FirstLayerPlane.cpp + FirstLayerPlane.hpp Brim.cpp BrimEarsPoint.hpp Brim.hpp diff --git a/src/libslic3r/FirstLayerPlane.cpp b/src/libslic3r/FirstLayerPlane.cpp new file mode 100644 index 0000000000..23327aea7a --- /dev/null +++ b/src/libslic3r/FirstLayerPlane.cpp @@ -0,0 +1,226 @@ +#include "FirstLayerPlane.hpp" +#include "BeltTransform.hpp" + +#include +#include +#include + +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) { + // BeltGCodeWriter 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) { + if (config.belt_printer.value && + config.belt_shear_z.value != BeltShearMode::None) { + mode = FirstLayerPlaneMode::BeltShear; + } else { + mode = 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::BeltShear: { + // 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 diff --git a/src/libslic3r/FirstLayerPlane.hpp b/src/libslic3r/FirstLayerPlane.hpp new file mode 100644 index 0000000000..35b8346c82 --- /dev/null +++ b/src/libslic3r/FirstLayerPlane.hpp @@ -0,0 +1,76 @@ +#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_ diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 93b87cbba8..a9122d9bfd 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -24,6 +24,7 @@ #include "Time.hpp" #include "GCode/ExtrusionProcessor.hpp" #include +#include #include #include #include @@ -2438,6 +2439,17 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } } + // 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(print.config()); + if (auto *belt_writer = dynamic_cast(m_writer.get())) { + belt_writer->set_first_layer_plane( + m_first_layer_plane.get(), + print.config().initial_layer_print_height.value); + } + // How many times will be change_layer() called? // change_layer() in turn increments the progress bar status. m_layer_count = 0; @@ -4588,7 +4600,38 @@ LayerResult GCode::process_layer( } } - if (!first_layer && !m_second_layer_things_done) { + // 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. + bool past_first_layer_band = !first_layer; + 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(bb.min.x()), unscale(bb.min.y())), + Vec2d(unscale(bb.max.x()), unscale(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 const auto plr_mode = print.config().enable_power_loss_recovery.value; gcode += m_writer->enable_power_loss_recovery(plr_mode); @@ -6204,6 +6247,18 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, if (is_bridge(path.role())) description += " (bridge)"; + // First-layer plane evaluation: compute the path's slicing-frame point + // once and reuse for every per-path call site below. When the plane + // evaluator is inactive (non-belt printers, or belt printers without + // a Z-axis shear) `path_on_first_layer` falls back to the legacy + // layer-id check, so behavior is bit-identical to the pre-feature path. + const Vec3d path_point_mm{ + unscale(path.first_point().x()), + unscale(path.first_point().y()), + m_layer ? m_layer->print_z : 0.0 + }; + const bool path_on_first_layer = this->on_first_layer(path_point_mm); + const ExtrusionPathSloped* sloped = dynamic_cast(&path); const auto get_sloped_z = [&sloped, this](double z_ratio) { @@ -6249,7 +6304,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // adjust acceleration if (m_config.default_acceleration.value > 0) { double acceleration; - if (this->on_first_layer() && m_config.initial_layer_acceleration.value > 0) { + if (path_on_first_layer && m_config.initial_layer_acceleration.value > 0) { acceleration = m_config.initial_layer_acceleration.value; #if 0 } else if (this->object_layer_over_raft() && m_config.first_layer_acceleration_over_raft.value > 0) { @@ -6275,7 +6330,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // adjust X Y jerk if (m_config.default_jerk.value > 0) { - if (this->on_first_layer() && m_config.initial_layer_jerk.value > 0) { + if (path_on_first_layer && m_config.initial_layer_jerk.value > 0) { jerk = m_config.initial_layer_jerk.value; } else if (m_config.outer_wall_jerk.value > 0 && is_external_perimeter(path.role())) { jerk = m_config.outer_wall_jerk.value; @@ -6335,7 +6390,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } // Additionally, adjust the value if we are on the first layer (except for brims and skirts) - if (this->on_first_layer() && (path.role() != erBrim && path.role() != erSkirt)) { + if (path_on_first_layer && (path.role() != erBrim && path.role() != erSkirt)) { _mm3_per_mm *= m_config.first_layer_flow_ratio; } } @@ -6393,14 +6448,17 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, if (speed == 0) speed = filament_max_volumetric_speed / _mm3_per_mm; - if (this->on_first_layer()) { + if (path_on_first_layer) { //BBS: for solid infill of first layer, speed can be higher as long as //wall lines have be attached if (path.role() != erBottomSurface) speed = m_config.get_abs_value("initial_layer_speed"); } else if(m_config.slow_down_layers > 1){ - const auto _layer = layer_id(); + // Use the FirstLayerPlane-aware effective layer index when active 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); if (_layer > 0 && _layer < m_config.slow_down_layers) { const auto first_layer_speed = is_perimeter(path.role()) @@ -6473,7 +6531,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, bool variable_speed = false; std::vector new_points {}; - if (m_config.enable_overhang_speed && !this->on_first_layer() && + if (m_config.enable_overhang_speed && !path_on_first_layer && (is_bridge(path.role()) || is_perimeter(path.role()))) { bool is_external = is_external_perimeter(path.role()); double ref_speed = is_external ? m_config.get_abs_value("outer_wall_speed") : m_config.get_abs_value("inner_wall_speed"); diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 9004f30fd3..99d95eeb61 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -5,6 +5,7 @@ #include "ExPolygon.hpp" #include "GCodeWriter.hpp" #include "BeltGCodeWriter.hpp" +#include "FirstLayerPlane.hpp" #include "Layer.hpp" #include "Point.hpp" #include "PlaceholderParser.hpp" @@ -303,6 +304,14 @@ public: } }; + // Public accessor for the first-layer plane evaluator. Used by + // CoolingBuffer (which is constructed with a GCode reference and needs + // to read the plane for per-segment fan re-evaluation). All other + // 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 { public: @@ -598,6 +607,11 @@ protected: std::unique_ptr m_cooling_buffer; std::unique_ptr m_spiral_vase; + // First-layer plane evaluator. Constructed once per print from the + // 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 m_first_layer_plane; std::unique_ptr m_pressure_equalizer; @@ -657,6 +671,25 @@ 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. + bool on_first_layer(const Vec3d &point_slicing_mm) const { + 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. + int effective_layer_index_for_point(const Vec3d &point_slicing_mm) const { + 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(); + } int layer_id() const { if (m_layer == nullptr) return -1; diff --git a/src/libslic3r/GCode/CoolingBuffer.cpp b/src/libslic3r/GCode/CoolingBuffer.cpp index 41e612fdab..407163b3dd 100644 --- a/src/libslic3r/GCode/CoolingBuffer.cpp +++ b/src/libslic3r/GCode/CoolingBuffer.cpp @@ -1,10 +1,14 @@ #include "../GCode.hpp" +#include "../FirstLayerPlane.hpp" #include "CoolingBuffer.hpp" #include #include #include +#include +#include #include #include +#include #include #include @@ -28,6 +32,12 @@ CoolingBuffer::CoolingBuffer(GCode &gcodegen) : m_config(gcodegen.config()), m_t m_num_extruders = std::max(ex.id() + 1, m_num_extruders); m_extruder_ids.emplace_back(ex.id()); } + + // Borrow the first-layer plane from the GCode generator. When inactive + // (non-belt printers and belt printers without Z shear), per-line fan + // re-evaluation is skipped and behavior is bit-identical to the legacy + // per-layer path. + m_first_layer_plane = gcodegen.first_layer_plane(); } void CoolingBuffer::reset(const Vec3d &position) @@ -328,6 +338,13 @@ std::string CoolingBuffer::process_layer(std::string &&gcode, size_t layer_id, b std::vector per_extruder_adjustments = this->parse_layer_gcode(m_gcode, m_current_pos); float layer_time_stretched = this->calculate_layer_slowdown(per_extruder_adjustments); out = this->apply_layer_cooldown(m_gcode, layer_id, layer_time_stretched, per_extruder_adjustments); + // First-layer plane: per-segment fan re-evaluation post-pass. Walks + // the cooled-down gcode and inserts inline M106 commands at band + // crossings (where the path's perpendicular distance to the plane + // crosses close_fan_the_first_x_layers thresholds). No-op when + // the evaluator is inactive. + if (m_first_layer_plane && m_first_layer_plane->is_active()) + out = this->apply_first_layer_plane_fan_eval(std::move(out), layer_id, layer_time_stretched); m_gcode.clear(); } return out; @@ -1011,4 +1028,210 @@ std::string CoolingBuffer::apply_layer_cooldown( return new_gcode; } +// Pure helper: compute the main fan speed for a given effective layer index. +// Mirrors the inline logic in change_extruder_set_fan but is callable from +// per-line code in apply_first_layer_plane_fan_eval. +int CoolingBuffer::compute_main_fan_speed(int effective_layer_id, float layer_time, + unsigned int extruder_id) const +{ +#define EXTRUDER_CFG(opt) m_config.opt.get_at(extruder_id) + float fan_min_speed = EXTRUDER_CFG(fan_min_speed); + float fan_max_speed = EXTRUDER_CFG(fan_max_speed); + bool reduce_fan_stop_start_freq = EXTRUDER_CFG(reduce_fan_stop_start_freq); + int close_fan_the_first_x_layers = EXTRUDER_CFG(close_fan_the_first_x_layers); + int full_fan_speed_layer = EXTRUDER_CFG(full_fan_speed_layer); + float slow_down_layer_time = float(EXTRUDER_CFG(slow_down_layer_time)); + float fan_cooling_layer_time = float(EXTRUDER_CFG(fan_cooling_layer_time)); +#undef EXTRUDER_CFG + + if (close_fan_the_first_x_layers <= 0 && full_fan_speed_layer > 0) + close_fan_the_first_x_layers = 1; + + float fan_speed_new = reduce_fan_stop_start_freq ? fan_min_speed : 0.f; + if (effective_layer_id >= close_fan_the_first_x_layers) { + if (layer_time < slow_down_layer_time) { + fan_speed_new = fan_max_speed; + } else if (layer_time < fan_cooling_layer_time) { + double t = (layer_time - slow_down_layer_time) / + (fan_cooling_layer_time - slow_down_layer_time); + fan_speed_new = float(int(floor(t * fan_min_speed + + (1. - t) * fan_max_speed) + 0.5)); + } + if (effective_layer_id + 1 < full_fan_speed_layer) { + float factor = float(effective_layer_id + 1 - close_fan_the_first_x_layers) + / float(full_fan_speed_layer - close_fan_the_first_x_layers); + fan_speed_new = float(std::clamp(int(fan_speed_new * factor + 0.5f), 0, 255)); + } + } else { + fan_speed_new = 0.f; + } + return int(fan_speed_new); +} + +// Post-pass: walk the cooled-down gcode line by line, track XYZ position, +// and insert M106 commands at first-layer-plane band crossings so the fan +// follows perpendicular distance to the plane rather than the slicing-layer +// index. Only invoked when the FirstLayerPlane evaluator is active. +// +// This implementation is intentionally minimal: it overrides only the MAIN +// fan (the one set by GCodeWriter::set_fan); overhang/internal-bridge/etc +// special fans remain at their layer-level values from apply_layer_cooldown. +// That keeps the per-line logic small while still giving the user precise +// fan control near the belt surface, which is the main quality concern. +std::string CoolingBuffer::apply_first_layer_plane_fan_eval( + std::string &&gcode_in, size_t /*layer_id*/, float layer_time) +{ + if (!m_first_layer_plane || !m_first_layer_plane->is_active()) + return std::move(gcode_in); + + const std::string &gcode = gcode_in; + std::string out; + out.reserve(gcode.size() + 256); + + // Track position in slicing-frame mm. Seed from m_current_pos which the + // CoolingBuffer keeps up-to-date across layers. + Vec3d cur_pos_mm(m_current_pos[0], m_current_pos[1], m_current_pos[2]); + + // Track current main fan speed by parsing M106 commands as we walk so + // we can restore it after a band exit. + int current_main_fan = m_fan_speed; + int pre_band_main_fan = current_main_fan; + // Implicit initial state: assume the layer started "out of the band" + // (i.e., the layer-level fan setting from apply_layer_cooldown is in + // effect). The first movement we encounter will reconcile this. + bool in_first_layer_band = false; + unsigned int active_extruder = m_current_extruder; + + auto parse_xyz_into = [](const std::string_view &line_sv, Vec3d &p) { + if (line_sv.size() < 3) return false; + if (line_sv[0] != 'G') return false; + if (line_sv[1] != '0' && line_sv[1] != '1') return false; + if (line_sv[2] != ' ' && line_sv[2] != '\t') return false; + const char *c = line_sv.data() + 3; + const char *end = line_sv.data() + line_sv.size(); + bool any = false; + while (c < end && *c != ';') { + while (c < end && (*c == ' ' || *c == '\t')) ++c; + if (c >= end || *c == ';' || *c == '\n' || *c == '\r') break; + char axis = *c; + ++c; + if (axis == 'X' || axis == 'Y' || axis == 'Z') { + char *next; + double v = std::strtod(c, &next); + if (next != c) { + if (axis == 'X') p.x() = v; + else if (axis == 'Y') p.y() = v; + else p.z() = v; + c = next; + any = true; + continue; + } + } + // Skip unrecognized word. + while (c < end && *c != ' ' && *c != '\t' && *c != ';' && *c != '\n') + ++c; + } + return any; + }; + + auto parse_m106 = [](const std::string_view &line_sv) -> int { + // Returns -1 if not an M106, otherwise the S value (0..255). + if (line_sv.size() < 4 || line_sv[0] != 'M') return -1; + if (!(line_sv[1] == '1' && line_sv[2] == '0' && line_sv[3] == '6')) + return -1; + // Find S + size_t s_pos = line_sv.find('S'); + if (s_pos == std::string_view::npos) return -1; + const char *c = line_sv.data() + s_pos + 1; + char *next; + long v = std::strtol(c, &next, 10); + if (next == c) return -1; + return int(std::clamp(v, 0, 255)); + }; + + auto parse_m107 = [](const std::string_view &line_sv) -> bool { + return line_sv.size() >= 4 && line_sv[0] == 'M' && + line_sv[1] == '1' && line_sv[2] == '0' && line_sv[3] == '7'; + }; + + auto parse_tool_change = [this](const std::string_view &line_sv) -> int { + // Returns the new extruder id, or -1 if not a toolchange. + if (line_sv.size() < m_toolchange_prefix.size() + 1) return -1; + if (line_sv.compare(0, m_toolchange_prefix.size(), m_toolchange_prefix) != 0) + return -1; + const char *c = line_sv.data() + m_toolchange_prefix.size(); + char *next; + long v = std::strtol(c, &next, 10); + if (next == c) return -1; + return int(v); + }; + + const char *p = gcode.c_str(); + const char *end = gcode.c_str() + gcode.size(); + while (p < end) { + const char *line_end = p; + while (line_end < end && *line_end != '\n') ++line_end; + const char *next_line = line_end; + if (next_line < end) ++next_line; // include the '\n' + + std::string_view line_sv(p, line_end - p); + + // Track tool changes so the per-line fan eval uses the right extruder. + int new_tool = parse_tool_change(line_sv); + if (new_tool >= 0) + active_extruder = unsigned(new_tool); + + // Track existing fan commands so we can restore the right value when + // exiting a band. + int m106_speed = parse_m106(line_sv); + if (m106_speed >= 0) { + current_main_fan = m106_speed; + if (!in_first_layer_band) + pre_band_main_fan = m106_speed; + } else if (parse_m107(line_sv)) { + current_main_fan = 0; + if (!in_first_layer_band) + pre_band_main_fan = 0; + } + + // Movement line: parse XYZ, evaluate plane, possibly emit a fan + // change BEFORE this line. + bool moved = parse_xyz_into(line_sv, cur_pos_mm); + if (moved) { + const int eff_idx = m_first_layer_plane->effective_layer_index(cur_pos_mm); + const int close_n = m_config.close_fan_the_first_x_layers.get_at(active_extruder); + const bool now_in_band = eff_idx < std::max(close_n, 1); + if (now_in_band != in_first_layer_band) { + // Band crossing: emit a M106 with the appropriate speed. + int target_fan; + if (now_in_band) { + // Entering the first-layer band: fan off. + pre_band_main_fan = current_main_fan; + target_fan = compute_main_fan_speed(eff_idx, layer_time, active_extruder); + } else { + // Exiting the band: restore the layer's normal fan speed. + // Use compute_main_fan_speed with the effective index so + // the linear ramp factor (close_fan→full_fan_speed_layer) + // also follows distance from the plane. + target_fan = compute_main_fan_speed(eff_idx, layer_time, active_extruder); + if (target_fan == 0) + target_fan = pre_band_main_fan; + } + if (target_fan != current_main_fan) { + out += GCodeWriter::set_fan(m_config.gcode_flavor, target_fan); + current_main_fan = target_fan; + m_fan_speed = target_fan; + m_current_fan_speed = target_fan; + } + in_first_layer_band = now_in_band; + } + } + + out.append(p, next_line - p); + p = next_line; + } + + return out; +} + } // namespace Slic3r diff --git a/src/libslic3r/GCode/CoolingBuffer.hpp b/src/libslic3r/GCode/CoolingBuffer.hpp index fba27b289b..4bb8f26476 100644 --- a/src/libslic3r/GCode/CoolingBuffer.hpp +++ b/src/libslic3r/GCode/CoolingBuffer.hpp @@ -10,6 +10,7 @@ namespace Slic3r { class GCode; class Layer; +class FirstLayerPlane; struct PerExtruderAdjustments; // A standalone G-code filter, to control cooling of the print. @@ -18,7 +19,7 @@ struct PerExtruderAdjustments; // // The simple it sounds, the actual implementation is significantly more complex. // Namely, for a multi-extruder print, each material may require a different cooling logic. -// For example, some materials may not like to print too slowly, while with some materials +// For example, some materials may not like to print too slowly, while with some materials // we may slow down significantly. // class CoolingBuffer { @@ -36,6 +37,21 @@ private: // Returns the adjusted G-code. std::string apply_layer_cooldown(const std::string &gcode, size_t layer_id, float layer_time, std::vector &per_extruder_adjustments); + // First-layer plane: per-line fan re-evaluation post-pass. Walks the + // post-cooldown gcode, tracks XYZ position, and inserts M106 commands at + // band-crossing transitions in slicing-frame coordinates. Only runs + // when m_first_layer_plane is active. + std::string apply_first_layer_plane_fan_eval(std::string &&gcode_in, + size_t layer_id, + float layer_time); + + // Pure helper: compute the main fan speed for a given effective layer + // index (layer-id units, mapped through the plane evaluator) and the + // current extruder. Mirrors the inline logic in the change_extruder_set_fan + // lambda but is callable from per-line code. + int compute_main_fan_speed(int effective_layer_id, float layer_time, + unsigned int extruder_id) const; + // G-code snippet cached for the support layers preceding an object layer. std::string m_gcode; // Internal data. @@ -57,6 +73,9 @@ private: unsigned int m_current_extruder; //BBS: current fan speed int m_current_fan_speed; + // First-layer plane evaluator, borrowed from GCode. Null = inactive + // (legacy per-layer fan control). + const FirstLayerPlane *m_first_layer_plane = nullptr; }; } diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 4d01757826..f76ea93e55 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1017,6 +1017,7 @@ static std::vector s_Preset_printer_options { "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_preslice_global", + "first_layer_plane", "first_layer_plane_offset", "first_layer_plane_thickness", "belt_origin_snap_x", "belt_origin_offset_x", "belt_origin_snap_y", "belt_origin_offset_y", "belt_origin_snap_z", "belt_origin_offset_z", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 47d0b9c549..4ec0547ddb 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -341,6 +341,15 @@ static t_config_enum_values s_keys_map_BeltSupportZOffsetMode { }; 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_shear", int(FirstLayerPlaneMode::BeltShear) }, +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(FirstLayerPlaneMode) + static t_config_enum_values s_keys_map_SupportMaterialPattern { { "rectilinear", smpRectilinear }, { "rectilinear-grid", smpRectilinearGrid }, @@ -6188,6 +6197,51 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionBool(false)); + // 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 BeltShear when belt_shear_z != None, 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 shear plane when belt_shear_z is non-None, " + "otherwise XY (legacy). Pick XY explicitly to opt out and force the " + "legacy slicing-layer-0 detection."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values = {"auto", "xy", "yz", "xz", "belt_shear"}; + def->enum_labels = {L("Auto"), L("XY (machine bed)"), L("YZ"), L("XZ"), L("Belt shear plane")}; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(FirstLayerPlaneMode::Auto)); + + def = this->add("first_layer_plane_offset", coFloat); + def->label = L("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)); + auto add_belt_origin_snap = [this](const char *key_snap, const char *key_offset, const char *axis_label) { auto def = this->add(key_snap, coBool); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index d17c46f0aa..afe5ec58ac 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -203,6 +203,26 @@ enum class BeltSupportZOffsetMode 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 Z-axis shear. +// - BeltShear for belt printers with belt_shear_z != None. +// +// 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, + BeltShear, +}; + enum SupportMaterialPattern { smpDefault, smpRectilinear, smpRectilinearGrid, smpHoneycomb, @@ -552,6 +572,7 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltAxis) 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) @@ -1496,6 +1517,9 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionEnum, gcode_remap_z)) ((ConfigOptionBool, gcode_back_transform)) ((ConfigOptionBool, belt_preslice_global)) + ((ConfigOptionEnum, first_layer_plane)) + ((ConfigOptionFloat, first_layer_plane_offset)) + ((ConfigOptionFloat, first_layer_plane_thickness)) ((ConfigOptionBool, belt_origin_snap_x)) ((ConfigOptionFloat, belt_origin_offset_x)) ((ConfigOptionBool, belt_origin_snap_y)) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 986ff614f6..4eaa6c5788 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4451,6 +4451,18 @@ void TabPrinter::build_fff() } optgroup->append_single_option_line("belt_preslice_global"); optgroup->append_single_option_line("gcode_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.append_option(optgroup->get_option("first_layer_plane")); + line.append_option(optgroup->get_option("first_layer_plane_offset")); + line.append_option(optgroup->get_option("first_layer_plane_thickness")); + optgroup->append_line(line); + } { Line line = { L("Origin snap X"), L("Snap object bbox min X to offset in G-code output") }; line.append_option(optgroup->get_option("belt_origin_snap_x")); @@ -5368,6 +5380,14 @@ void TabPrinter::toggle_options() toggle_option("belt_origin_offset_y", is_belt && m_config->opt_bool("belt_origin_snap_y") && !belt_global); toggle_option("belt_origin_offset_z", is_belt && m_config->opt_bool("belt_origin_snap_z") && !belt_global); + // First-layer plane: visible alongside the rest of belt-printer settings. + // The Auto default keeps legacy XY behavior on non-belt printers, so it's + // safe to also show it in the developer mode for non-belt printers. + bool show_first_layer_plane = is_belt || (m_mode >= comDevelop); + toggle_line("first_layer_plane", show_first_layer_plane); + toggle_option("first_layer_plane_offset", show_first_layer_plane); + toggle_option("first_layer_plane_thickness", show_first_layer_plane); + toggle_line("belt_support_floor_mode", is_belt); } From 8fa6a4602b054c6a1a5fa59f31e7545b1a6cfa17 Mon Sep 17 00:00:00 2001 From: RF47 <162915171+RF47@users.noreply.github.com> Date: Sat, 9 May 2026 19:51:18 -0300 Subject: [PATCH 025/224] fix profile indentation --- resources/profiles/Custom.json | 592 +++++++++--------- .../machine/MyBeltPrinter 0.2 nozzle.json | 50 +- .../machine/MyBeltPrinter 0.4 nozzle.json | 38 +- .../machine/MyBeltPrinter 0.6 nozzle.json | 50 +- .../machine/MyBeltPrinter 0.8 nozzle.json | 50 +- .../Custom/machine/MyBeltPrinter.json | 22 +- .../Custom/machine/fdm_belt_common.json | 198 +++--- 7 files changed, 500 insertions(+), 500 deletions(-) diff --git a/resources/profiles/Custom.json b/resources/profiles/Custom.json index 790e66c6dc..16c02eda4d 100644 --- a/resources/profiles/Custom.json +++ b/resources/profiles/Custom.json @@ -1,298 +1,298 @@ { - "name": "Custom Printer", - "version": "02.03.02.62", - "force_update": "0", - "description": "My configurations", - "machine_model_list": [ - { - "name": "Generic Belt Printer", - "sub_path": "machine/MyBeltPrinter.json" - }, - { - "name": "Generic Klipper Printer", - "sub_path": "machine/MyKlipper.json" - }, - { - "name": "Generic Marlin Printer", - "sub_path": "machine/MyMarlin.json" - }, - { - "name": "Generic RRF Printer", - "sub_path": "machine/MyRRF.json" - }, - { - "name": "Generic ToolChanger Printer", - "sub_path": "machine/MyToolChanger.json" - } - ], - "process_list": [ - { - "name": "fdm_process_common", - "sub_path": "process/fdm_process_common.json" - }, - { - "name": "fdm_process_klipper_common", - "sub_path": "process/fdm_process_klipper_common.json" - }, - { - "name": "fdm_process_marlin_common", - "sub_path": "process/fdm_process_marlin_common.json" - }, - { - "name": "fdm_process_rrf_common", - "sub_path": "process/fdm_process_rrf_common.json" - }, - { - "name": "0.08mm Extra Fine @MyKlipper", - "sub_path": "process/0.08mm Extra Fine @MyKlipper.json" - }, - { - "name": "0.12mm Fine @MyKlipper", - "sub_path": "process/0.12mm Fine @MyKlipper.json" - }, - { - "name": "0.15mm Optimal @MyKlipper", - "sub_path": "process/0.15mm Optimal @MyKlipper.json" - }, - { - "name": "0.16mm Optimal @MyKlipper", - "sub_path": "process/0.16mm Optimal @MyKlipper.json" - }, - { - "name": "0.20mm Standard @MyKlipper", - "sub_path": "process/0.20mm Standard @MyKlipper.json" - }, - { - "name": "0.24mm Draft @MyKlipper", - "sub_path": "process/0.24mm Draft @MyKlipper.json" - }, - { - "name": "0.28mm Extra Draft @MyKlipper", - "sub_path": "process/0.28mm Extra Draft @MyKlipper.json" - }, - { - "name": "0.32mm Standard @MyKlipper", - "sub_path": "process/0.32mm Extra Draft @MyKlipper.json" - }, - { - "name": "0.40mm Standard @MyKlipper", - "sub_path": "process/0.40mm Extra Draft @MyKlipper.json" - }, - { - "name": "0.56mm Standard @MyKlipper", - "sub_path": "process/0.56mm Extra Draft @MyKlipper.json" - }, - { - "name": "fdm_process_mytoolchanger_common", - "sub_path": "process/fdm_process_mytoolchanger_common.json" - }, - { - "name": "0.08mm Extra Fine @MyMarlin", - "sub_path": "process/0.08mm Extra Fine @MyMarlin.json" - }, - { - "name": "0.12mm Fine @MyMarlin", - "sub_path": "process/0.12mm Fine @MyMarlin.json" - }, - { - "name": "0.15mm Optimal @MyMarlin", - "sub_path": "process/0.15mm Optimal @MyMarlin.json" - }, - { - "name": "0.20mm Standard @MyMarlin", - "sub_path": "process/0.20mm Standard @MyMarlin.json" - }, - { - "name": "0.24mm Draft @MyMarlin", - "sub_path": "process/0.24mm Draft @MyMarlin.json" - }, - { - "name": "0.28mm Extra Draft @MyMarlin", - "sub_path": "process/0.28mm Extra Draft @MyMarlin.json" - }, - { - "name": "0.08mm Extra Fine @MyRRF", - "sub_path": "process/0.08mm Extra Fine @MyRRF.json" - }, - { - "name": "0.12mm Fine @MyRRF", - "sub_path": "process/0.12mm Fine @MyRRF.json" - }, - { - "name": "0.15mm Optimal @MyRRF", - "sub_path": "process/0.15mm Optimal @MyRRF.json" - }, - { - "name": "0.20mm Standard @MyRRF", - "sub_path": "process/0.20mm Standard @MyRRF.json" - }, - { - "name": "0.24mm Draft @MyRRF", - "sub_path": "process/0.24mm Draft @MyRRF.json" - }, - { - "name": "0.28mm Extra Draft @MyRRF", - "sub_path": "process/0.28mm Extra Draft @MyRRF.json" - }, - { - "name": "0.08mm Extra Fine @MyToolChanger", - "sub_path": "process/0.08mm Extra Fine @MyToolChanger.json" - }, - { - "name": "0.12mm Fine @MyToolChanger", - "sub_path": "process/0.12mm Fine @MyToolChanger.json" - }, - { - "name": "0.15mm Optimal @MyToolChanger", - "sub_path": "process/0.15mm Optimal @MyToolChanger.json" - }, - { - "name": "0.16mm Optimal @MyToolChanger", - "sub_path": "process/0.16mm Optimal @MyToolChanger.json" - }, - { - "name": "0.20mm Standard @MyToolChanger", - "sub_path": "process/0.20mm Standard @MyToolChanger.json" - }, - { - "name": "0.24mm Draft @MyToolChanger", - "sub_path": "process/0.24mm Draft @MyToolChanger.json" - }, - { - "name": "0.28mm Extra Draft @MyToolChanger", - "sub_path": "process/0.28mm Extra Draft @MyToolChanger.json" - }, - { - "name": "0.32mm Extra Draft @MyToolChanger", - "sub_path": "process/0.32mm Extra Draft @MyToolChanger.json" - }, - { - "name": "0.40mm Standard @MyToolChanger", - "sub_path": "process/0.40mm Extra Draft @MyToolChanger.json" - }, - { - "name": "0.56mm Standard @MyToolChanger", - "sub_path": "process/0.56mm Extra Draft @MyToolChanger.json" - } - ], - "filament_list": [ - { - "name": "Generic ABS @MyToolChanger", - "sub_path": "filament/Generic ABS @MyToolChanger.json" - }, - { - "name": "Generic ASA @MyToolChanger", - "sub_path": "filament/Generic ASA @MyToolChanger.json" - }, - { - "name": "Generic PA @MyToolChanger", - "sub_path": "filament/Generic PA @MyToolChanger.json" - }, - { - "name": "Generic PA-CF @MyToolChanger", - "sub_path": "filament/Generic PA-CF @MyToolChanger.json" - }, - { - "name": "Generic PC @MyToolChanger", - "sub_path": "filament/Generic PC @MyToolChanger.json" - }, - { - "name": "Generic PETG @MyToolChanger", - "sub_path": "filament/Generic PETG @MyToolChanger.json" - }, - { - "name": "Generic PLA @MyToolChanger", - "sub_path": "filament/Generic PLA @MyToolChanger.json" - }, - { - "name": "Generic PLA-CF @MyToolChanger", - "sub_path": "filament/Generic PLA-CF @MyToolChanger.json" - }, - { - "name": "Generic PVA @MyToolChanger", - "sub_path": "filament/Generic PVA @MyToolChanger.json" - }, - { - "name": "Generic TPU @MyToolChanger", - "sub_path": "filament/Generic TPU @MyToolChanger.json" - } - ], - "machine_list": [ - { - "name": "fdm_machine_common", - "sub_path": "machine/fdm_machine_common.json" - }, - { - "name": "MyMarlin 0.4 nozzle", - "sub_path": "machine/MyMarlin 0.4 nozzle.json" - }, - { - "name": "fdm_klipper_common", - "sub_path": "machine/fdm_klipper_common.json" - }, - { - "name": "fdm_rrf_common", - "sub_path": "machine/fdm_rrf_common.json" - }, - { - "name": "MyKlipper 0.2 nozzle", - "sub_path": "machine/MyKlipper 0.2 nozzle.json" - }, - { - "name": "MyKlipper 0.4 nozzle", - "sub_path": "machine/MyKlipper 0.4 nozzle.json" - }, - { - "name": "MyKlipper 0.6 nozzle", - "sub_path": "machine/MyKlipper 0.6 nozzle.json" - }, - { - "name": "MyKlipper 0.8 nozzle", - "sub_path": "machine/MyKlipper 0.8 nozzle.json" - }, - { - "name": "fdm_belt_common", - "sub_path": "machine/fdm_belt_common.json" - }, - { - "name": "fdm_toolchanger_common", - "sub_path": "machine/fdm_toolchanger_common.json" - }, - { - "name": "MyRRF 0.4 nozzle", - "sub_path": "machine/MyRRF 0.4 nozzle.json" - }, - { - "name": "MyBeltPrinter 0.2 nozzle", - "sub_path": "machine/MyBeltPrinter 0.2 nozzle.json" - }, - { - "name": "MyBeltPrinter 0.4 nozzle", - "sub_path": "machine/MyBeltPrinter 0.4 nozzle.json" - }, - { - "name": "MyBeltPrinter 0.6 nozzle", - "sub_path": "machine/MyBeltPrinter 0.6 nozzle.json" - }, - { - "name": "MyBeltPrinter 0.8 nozzle", - "sub_path": "machine/MyBeltPrinter 0.8 nozzle.json" - }, - { - "name": "MyToolChanger 0.2 nozzle", - "sub_path": "machine/MyToolChanger 0.2 nozzle.json" - }, - { - "name": "MyToolChanger 0.4 nozzle", - "sub_path": "machine/MyToolChanger 0.4 nozzle.json" - }, - { - "name": "MyToolChanger 0.6 nozzle", - "sub_path": "machine/MyToolChanger 0.6 nozzle.json" - }, - { - "name": "MyToolChanger 0.8 nozzle", - "sub_path": "machine/MyToolChanger 0.8 nozzle.json" - } - ] + "name": "Custom Printer", + "version": "02.03.02.62", + "force_update": "0", + "description": "My configurations", + "machine_model_list": [ + { + "name": "Generic Belt Printer", + "sub_path": "machine/MyBeltPrinter.json" + }, + { + "name": "Generic Klipper Printer", + "sub_path": "machine/MyKlipper.json" + }, + { + "name": "Generic Marlin Printer", + "sub_path": "machine/MyMarlin.json" + }, + { + "name": "Generic RRF Printer", + "sub_path": "machine/MyRRF.json" + }, + { + "name": "Generic ToolChanger Printer", + "sub_path": "machine/MyToolChanger.json" + } + ], + "process_list": [ + { + "name": "fdm_process_common", + "sub_path": "process/fdm_process_common.json" + }, + { + "name": "fdm_process_klipper_common", + "sub_path": "process/fdm_process_klipper_common.json" + }, + { + "name": "fdm_process_marlin_common", + "sub_path": "process/fdm_process_marlin_common.json" + }, + { + "name": "fdm_process_rrf_common", + "sub_path": "process/fdm_process_rrf_common.json" + }, + { + "name": "0.08mm Extra Fine @MyKlipper", + "sub_path": "process/0.08mm Extra Fine @MyKlipper.json" + }, + { + "name": "0.12mm Fine @MyKlipper", + "sub_path": "process/0.12mm Fine @MyKlipper.json" + }, + { + "name": "0.15mm Optimal @MyKlipper", + "sub_path": "process/0.15mm Optimal @MyKlipper.json" + }, + { + "name": "0.16mm Optimal @MyKlipper", + "sub_path": "process/0.16mm Optimal @MyKlipper.json" + }, + { + "name": "0.20mm Standard @MyKlipper", + "sub_path": "process/0.20mm Standard @MyKlipper.json" + }, + { + "name": "0.24mm Draft @MyKlipper", + "sub_path": "process/0.24mm Draft @MyKlipper.json" + }, + { + "name": "0.28mm Extra Draft @MyKlipper", + "sub_path": "process/0.28mm Extra Draft @MyKlipper.json" + }, + { + "name": "0.32mm Standard @MyKlipper", + "sub_path": "process/0.32mm Extra Draft @MyKlipper.json" + }, + { + "name": "0.40mm Standard @MyKlipper", + "sub_path": "process/0.40mm Extra Draft @MyKlipper.json" + }, + { + "name": "0.56mm Standard @MyKlipper", + "sub_path": "process/0.56mm Extra Draft @MyKlipper.json" + }, + { + "name": "fdm_process_mytoolchanger_common", + "sub_path": "process/fdm_process_mytoolchanger_common.json" + }, + { + "name": "0.08mm Extra Fine @MyMarlin", + "sub_path": "process/0.08mm Extra Fine @MyMarlin.json" + }, + { + "name": "0.12mm Fine @MyMarlin", + "sub_path": "process/0.12mm Fine @MyMarlin.json" + }, + { + "name": "0.15mm Optimal @MyMarlin", + "sub_path": "process/0.15mm Optimal @MyMarlin.json" + }, + { + "name": "0.20mm Standard @MyMarlin", + "sub_path": "process/0.20mm Standard @MyMarlin.json" + }, + { + "name": "0.24mm Draft @MyMarlin", + "sub_path": "process/0.24mm Draft @MyMarlin.json" + }, + { + "name": "0.28mm Extra Draft @MyMarlin", + "sub_path": "process/0.28mm Extra Draft @MyMarlin.json" + }, + { + "name": "0.08mm Extra Fine @MyRRF", + "sub_path": "process/0.08mm Extra Fine @MyRRF.json" + }, + { + "name": "0.12mm Fine @MyRRF", + "sub_path": "process/0.12mm Fine @MyRRF.json" + }, + { + "name": "0.15mm Optimal @MyRRF", + "sub_path": "process/0.15mm Optimal @MyRRF.json" + }, + { + "name": "0.20mm Standard @MyRRF", + "sub_path": "process/0.20mm Standard @MyRRF.json" + }, + { + "name": "0.24mm Draft @MyRRF", + "sub_path": "process/0.24mm Draft @MyRRF.json" + }, + { + "name": "0.28mm Extra Draft @MyRRF", + "sub_path": "process/0.28mm Extra Draft @MyRRF.json" + }, + { + "name": "0.08mm Extra Fine @MyToolChanger", + "sub_path": "process/0.08mm Extra Fine @MyToolChanger.json" + }, + { + "name": "0.12mm Fine @MyToolChanger", + "sub_path": "process/0.12mm Fine @MyToolChanger.json" + }, + { + "name": "0.15mm Optimal @MyToolChanger", + "sub_path": "process/0.15mm Optimal @MyToolChanger.json" + }, + { + "name": "0.16mm Optimal @MyToolChanger", + "sub_path": "process/0.16mm Optimal @MyToolChanger.json" + }, + { + "name": "0.20mm Standard @MyToolChanger", + "sub_path": "process/0.20mm Standard @MyToolChanger.json" + }, + { + "name": "0.24mm Draft @MyToolChanger", + "sub_path": "process/0.24mm Draft @MyToolChanger.json" + }, + { + "name": "0.28mm Extra Draft @MyToolChanger", + "sub_path": "process/0.28mm Extra Draft @MyToolChanger.json" + }, + { + "name": "0.32mm Extra Draft @MyToolChanger", + "sub_path": "process/0.32mm Extra Draft @MyToolChanger.json" + }, + { + "name": "0.40mm Standard @MyToolChanger", + "sub_path": "process/0.40mm Extra Draft @MyToolChanger.json" + }, + { + "name": "0.56mm Standard @MyToolChanger", + "sub_path": "process/0.56mm Extra Draft @MyToolChanger.json" + } + ], + "filament_list": [ + { + "name": "Generic ABS @MyToolChanger", + "sub_path": "filament/Generic ABS @MyToolChanger.json" + }, + { + "name": "Generic ASA @MyToolChanger", + "sub_path": "filament/Generic ASA @MyToolChanger.json" + }, + { + "name": "Generic PA @MyToolChanger", + "sub_path": "filament/Generic PA @MyToolChanger.json" + }, + { + "name": "Generic PA-CF @MyToolChanger", + "sub_path": "filament/Generic PA-CF @MyToolChanger.json" + }, + { + "name": "Generic PC @MyToolChanger", + "sub_path": "filament/Generic PC @MyToolChanger.json" + }, + { + "name": "Generic PETG @MyToolChanger", + "sub_path": "filament/Generic PETG @MyToolChanger.json" + }, + { + "name": "Generic PLA @MyToolChanger", + "sub_path": "filament/Generic PLA @MyToolChanger.json" + }, + { + "name": "Generic PLA-CF @MyToolChanger", + "sub_path": "filament/Generic PLA-CF @MyToolChanger.json" + }, + { + "name": "Generic PVA @MyToolChanger", + "sub_path": "filament/Generic PVA @MyToolChanger.json" + }, + { + "name": "Generic TPU @MyToolChanger", + "sub_path": "filament/Generic TPU @MyToolChanger.json" + } + ], + "machine_list": [ + { + "name": "fdm_machine_common", + "sub_path": "machine/fdm_machine_common.json" + }, + { + "name": "MyMarlin 0.4 nozzle", + "sub_path": "machine/MyMarlin 0.4 nozzle.json" + }, + { + "name": "fdm_klipper_common", + "sub_path": "machine/fdm_klipper_common.json" + }, + { + "name": "fdm_rrf_common", + "sub_path": "machine/fdm_rrf_common.json" + }, + { + "name": "MyKlipper 0.2 nozzle", + "sub_path": "machine/MyKlipper 0.2 nozzle.json" + }, + { + "name": "MyKlipper 0.4 nozzle", + "sub_path": "machine/MyKlipper 0.4 nozzle.json" + }, + { + "name": "MyKlipper 0.6 nozzle", + "sub_path": "machine/MyKlipper 0.6 nozzle.json" + }, + { + "name": "MyKlipper 0.8 nozzle", + "sub_path": "machine/MyKlipper 0.8 nozzle.json" + }, + { + "name": "fdm_belt_common", + "sub_path": "machine/fdm_belt_common.json" + }, + { + "name": "fdm_toolchanger_common", + "sub_path": "machine/fdm_toolchanger_common.json" + }, + { + "name": "MyRRF 0.4 nozzle", + "sub_path": "machine/MyRRF 0.4 nozzle.json" + }, + { + "name": "MyBeltPrinter 0.2 nozzle", + "sub_path": "machine/MyBeltPrinter 0.2 nozzle.json" + }, + { + "name": "MyBeltPrinter 0.4 nozzle", + "sub_path": "machine/MyBeltPrinter 0.4 nozzle.json" + }, + { + "name": "MyBeltPrinter 0.6 nozzle", + "sub_path": "machine/MyBeltPrinter 0.6 nozzle.json" + }, + { + "name": "MyBeltPrinter 0.8 nozzle", + "sub_path": "machine/MyBeltPrinter 0.8 nozzle.json" + }, + { + "name": "MyToolChanger 0.2 nozzle", + "sub_path": "machine/MyToolChanger 0.2 nozzle.json" + }, + { + "name": "MyToolChanger 0.4 nozzle", + "sub_path": "machine/MyToolChanger 0.4 nozzle.json" + }, + { + "name": "MyToolChanger 0.6 nozzle", + "sub_path": "machine/MyToolChanger 0.6 nozzle.json" + }, + { + "name": "MyToolChanger 0.8 nozzle", + "sub_path": "machine/MyToolChanger 0.8 nozzle.json" + } + ] } diff --git a/resources/profiles/Custom/machine/MyBeltPrinter 0.2 nozzle.json b/resources/profiles/Custom/machine/MyBeltPrinter 0.2 nozzle.json index 576965a6ba..5e7655fbe1 100644 --- a/resources/profiles/Custom/machine/MyBeltPrinter 0.2 nozzle.json +++ b/resources/profiles/Custom/machine/MyBeltPrinter 0.2 nozzle.json @@ -1,26 +1,26 @@ { - "type": "machine", - "name": "MyBeltPrinter 0.2 nozzle", - "inherits": "fdm_belt_common", - "from": "system", - "setting_id": "GM_BELT_001", - "instantiation": "true", - "printer_model": "Generic Belt Printer", - "nozzle_diameter": [ - "0.2" - ], - "max_layer_height": [ - "0.16" - ], - "min_layer_height": [ - "0.04" - ], - "printer_variant": "0.2", - "printable_area": [ - "0x0", - "350x0", - "350x350", - "0x350" - ], - "printable_height": "300" -} \ No newline at end of file + "type": "machine", + "name": "MyBeltPrinter 0.2 nozzle", + "inherits": "fdm_belt_common", + "from": "system", + "setting_id": "GM_BELT_001", + "instantiation": "true", + "printer_model": "Generic Belt Printer", + "nozzle_diameter": [ + "0.2" + ], + "max_layer_height": [ + "0.16" + ], + "min_layer_height": [ + "0.04" + ], + "printer_variant": "0.2", + "printable_area": [ + "0x0", + "350x0", + "350x350", + "0x350" + ], + "printable_height": "300" +} diff --git a/resources/profiles/Custom/machine/MyBeltPrinter 0.4 nozzle.json b/resources/profiles/Custom/machine/MyBeltPrinter 0.4 nozzle.json index b066b929cc..73be69ba61 100644 --- a/resources/profiles/Custom/machine/MyBeltPrinter 0.4 nozzle.json +++ b/resources/profiles/Custom/machine/MyBeltPrinter 0.4 nozzle.json @@ -1,20 +1,20 @@ { - "type": "machine", - "name": "MyBeltPrinter 0.4 nozzle", - "inherits": "fdm_belt_common", - "from": "system", - "setting_id": "GM_BELT_002", - "instantiation": "true", - "printer_model": "Generic Belt Printer", - "nozzle_diameter": [ - "0.4" - ], - "printer_variant": "0.4", - "printable_area": [ - "0x0", - "350x0", - "350x350", - "0x350" - ], - "printable_height": "300" -} \ No newline at end of file + "type": "machine", + "name": "MyBeltPrinter 0.4 nozzle", + "inherits": "fdm_belt_common", + "from": "system", + "setting_id": "GM_BELT_002", + "instantiation": "true", + "printer_model": "Generic Belt Printer", + "nozzle_diameter": [ + "0.4" + ], + "printer_variant": "0.4", + "printable_area": [ + "0x0", + "350x0", + "350x350", + "0x350" + ], + "printable_height": "300" +} diff --git a/resources/profiles/Custom/machine/MyBeltPrinter 0.6 nozzle.json b/resources/profiles/Custom/machine/MyBeltPrinter 0.6 nozzle.json index e45fa70489..49646db06d 100644 --- a/resources/profiles/Custom/machine/MyBeltPrinter 0.6 nozzle.json +++ b/resources/profiles/Custom/machine/MyBeltPrinter 0.6 nozzle.json @@ -1,26 +1,26 @@ { - "type": "machine", - "name": "MyBeltPrinter 0.6 nozzle", - "inherits": "fdm_belt_common", - "from": "system", - "setting_id": "GM_BELT_003", - "instantiation": "true", - "printer_model": "Generic Belt Printer", - "nozzle_diameter": [ - "0.6" - ], - "max_layer_height": [ - "0.4" - ], - "min_layer_height": [ - "0.12" - ], - "printer_variant": "0.6", - "printable_area": [ - "0x0", - "350x0", - "350x350", - "0x350" - ], - "printable_height": "300" -} \ No newline at end of file + "type": "machine", + "name": "MyBeltPrinter 0.6 nozzle", + "inherits": "fdm_belt_common", + "from": "system", + "setting_id": "GM_BELT_003", + "instantiation": "true", + "printer_model": "Generic Belt Printer", + "nozzle_diameter": [ + "0.6" + ], + "max_layer_height": [ + "0.4" + ], + "min_layer_height": [ + "0.12" + ], + "printer_variant": "0.6", + "printable_area": [ + "0x0", + "350x0", + "350x350", + "0x350" + ], + "printable_height": "300" +} diff --git a/resources/profiles/Custom/machine/MyBeltPrinter 0.8 nozzle.json b/resources/profiles/Custom/machine/MyBeltPrinter 0.8 nozzle.json index 90133edd29..412291754e 100644 --- a/resources/profiles/Custom/machine/MyBeltPrinter 0.8 nozzle.json +++ b/resources/profiles/Custom/machine/MyBeltPrinter 0.8 nozzle.json @@ -1,26 +1,26 @@ { - "type": "machine", - "name": "MyBeltPrinter 0.8 nozzle", - "inherits": "fdm_belt_common", - "from": "system", - "setting_id": "GM_BELT_004", - "instantiation": "true", - "printer_model": "Generic Belt Printer", - "nozzle_diameter": [ - "0.8" - ], - "max_layer_height": [ - "0.6" - ], - "min_layer_height": [ - "0.2" - ], - "printer_variant": "0.8", - "printable_area": [ - "0x0", - "350x0", - "350x350", - "0x350" - ], - "printable_height": "300" -} \ No newline at end of file + "type": "machine", + "name": "MyBeltPrinter 0.8 nozzle", + "inherits": "fdm_belt_common", + "from": "system", + "setting_id": "GM_BELT_004", + "instantiation": "true", + "printer_model": "Generic Belt Printer", + "nozzle_diameter": [ + "0.8" + ], + "max_layer_height": [ + "0.6" + ], + "min_layer_height": [ + "0.2" + ], + "printer_variant": "0.8", + "printable_area": [ + "0x0", + "350x0", + "350x350", + "0x350" + ], + "printable_height": "300" +} diff --git a/resources/profiles/Custom/machine/MyBeltPrinter.json b/resources/profiles/Custom/machine/MyBeltPrinter.json index 2761644788..0f6e0c5891 100644 --- a/resources/profiles/Custom/machine/MyBeltPrinter.json +++ b/resources/profiles/Custom/machine/MyBeltPrinter.json @@ -1,12 +1,12 @@ { - "type": "machine_model", - "name": "Generic Belt Printer", - "model_id": "my_belt_01", - "nozzle_diameter": "0.4;0.2;0.6;0.8", - "machine_tech": "FFF", - "family": "MyPrinter", - "bed_model": "Custom_350_bed.stl", - "bed_texture": "orcaslicer_bed_texture.svg", - "hotend_model": "", - "default_materials": "Generic PLA @System;Generic PLA-CF @System;Generic PETG @System;Generic TPU @System;Generic PC @System;Generic PVA @System;Generic PA @System;Generic PA-CF @System" -} \ No newline at end of file + "type": "machine_model", + "name": "Generic Belt Printer", + "model_id": "my_belt_01", + "nozzle_diameter": "0.4;0.2;0.6;0.8", + "machine_tech": "FFF", + "family": "MyPrinter", + "bed_model": "Custom_350_bed.stl", + "bed_texture": "orcaslicer_bed_texture.svg", + "hotend_model": "", + "default_materials": "Generic PLA @System;Generic PLA-CF @System;Generic PETG @System;Generic TPU @System;Generic PC @System;Generic PVA @System;Generic PA @System;Generic PA-CF @System" +} diff --git a/resources/profiles/Custom/machine/fdm_belt_common.json b/resources/profiles/Custom/machine/fdm_belt_common.json index 46603f7faf..1ca314da0f 100644 --- a/resources/profiles/Custom/machine/fdm_belt_common.json +++ b/resources/profiles/Custom/machine/fdm_belt_common.json @@ -1,100 +1,100 @@ { - "type": "machine", - "name": "fdm_belt_common", - "inherits": "fdm_klipper_common", - "from": "system", - "instantiation": "false", - "gcode_flavor": "klipper", - "single_extruder_multi_material": "0", - "default_filament_profile": [ - "Generic PLA @System" - ], - "default_print_profile": "0.20mm Standard @System", - "max_layer_height": [ - "0.32" - ], - "min_layer_height": [ - "0.08" - ], - "deretraction_speed": [ - "30" - ], - "extruder_colour": [ - "#FCE94F" - ], - "extruder_offset": [ - "0x0" - ], - "long_retractions_when_cut": [ - "0" - ], - "nozzle_diameter": [ - "0.4" - ], - "retract_before_wipe": [ - "70%" - ], - "retract_length_toolchange": [ - "2" - ], - "retract_lift_above": [ - "0" - ], - "retract_lift_below": [ - "0" - ], - "retract_lift_enforce": [ - "All Surfaces" - ], - "retract_restart_extra": [ - "0" - ], - "retract_restart_extra_toolchange": [ - "0" - ], - "retract_when_changing_layer": [ - "1" - ], - "retraction_distances_when_cut": [ - "18" - ], - "retraction_length": [ - "0.8" - ], - "retraction_minimum_travel": [ - "1" - ], - "retraction_speed": [ - "30" - ], - "travel_slope": [ - "3" - ], - "wipe": [ - "1" - ], - "wipe_distance": [ - "1" - ], - "z_hop": [ - "0.4" - ], - "z_hop_types": [ - "Normal Lift" - ], - "preslice_remap_y": "pos_z", - "preslice_remap_z": "pos_y", - "preslice_remap_global": "1", - "printer_extruder_id": [ - "1" - ], - "belt_origin_snap_y": "1", - "belt_printer": "1", - "belt_preslice_global": "1", - "belt_shear_z": "pos_cot", - "belt_shear_z_angle": "30", - "build_plate_tilt_x": "45", - "purge_in_prime_tower": "0", - "scan_first_layer": "0", - "auxiliary_fan": "0" -} \ No newline at end of file + "type": "machine", + "name": "fdm_belt_common", + "inherits": "fdm_klipper_common", + "from": "system", + "instantiation": "false", + "gcode_flavor": "klipper", + "single_extruder_multi_material": "0", + "default_filament_profile": [ + "Generic PLA @System" + ], + "default_print_profile": "0.20mm Standard @System", + "max_layer_height": [ + "0.32" + ], + "min_layer_height": [ + "0.08" + ], + "deretraction_speed": [ + "30" + ], + "extruder_colour": [ + "#FCE94F" + ], + "extruder_offset": [ + "0x0" + ], + "long_retractions_when_cut": [ + "0" + ], + "nozzle_diameter": [ + "0.4" + ], + "retract_before_wipe": [ + "70%" + ], + "retract_length_toolchange": [ + "2" + ], + "retract_lift_above": [ + "0" + ], + "retract_lift_below": [ + "0" + ], + "retract_lift_enforce": [ + "All Surfaces" + ], + "retract_restart_extra": [ + "0" + ], + "retract_restart_extra_toolchange": [ + "0" + ], + "retract_when_changing_layer": [ + "1" + ], + "retraction_distances_when_cut": [ + "18" + ], + "retraction_length": [ + "0.8" + ], + "retraction_minimum_travel": [ + "1" + ], + "retraction_speed": [ + "30" + ], + "travel_slope": [ + "3" + ], + "wipe": [ + "1" + ], + "wipe_distance": [ + "1" + ], + "z_hop": [ + "0.4" + ], + "z_hop_types": [ + "Normal Lift" + ], + "preslice_remap_y": "pos_z", + "preslice_remap_z": "pos_y", + "preslice_remap_global": "1", + "printer_extruder_id": [ + "1" + ], + "belt_origin_snap_y": "1", + "belt_printer": "1", + "belt_preslice_global": "1", + "belt_shear_z": "pos_cot", + "belt_shear_z_angle": "30", + "build_plate_tilt_x": "45", + "purge_in_prime_tower": "0", + "scan_first_layer": "0", + "auxiliary_fan": "0" +} From 6a2d690f459e265963eb81ddf3aa9a615e95bc82 Mon Sep 17 00:00:00 2001 From: Joseph Robertson <32232925+HarrierPigeon@users.noreply.github.com> Date: Mon, 18 May 2026 19:01:43 -0500 Subject: [PATCH 026/224] Decouple Slicing From Machine Frame Logic (#21) * minor logic swap * first attempt, has a race condition * fixed the offset issue * found a solution, I think things work now (at least once I quash this race condition) * still chasing down race conditions * add manual shear / scale order strategy swap * tweak manual shear, fix ui uninitialization crash * fix z height / g-code desync issue * fix shear then scale cutoff planes * getting closer * fix support termination planes * fix incorrect offsets in shear-then-scale mode * test - fix overextrusion due to model/layer scale --- .claude/scheduled_tasks.lock | 1 - src/libslic3r/BeltGCode.cpp | 52 ++++++- src/libslic3r/BeltGCodeWriter.cpp | 26 +++- src/libslic3r/BeltGCodeWriter.hpp | 9 +- src/libslic3r/BeltSliceStrategy.cpp | 17 +- src/libslic3r/BeltSliceStrategy.hpp | 9 +- src/libslic3r/BeltTransform.cpp | 73 +++++++-- src/libslic3r/BeltTransform.hpp | 4 +- src/libslic3r/CMakeLists.txt | 2 + src/libslic3r/GCode.cpp | 14 ++ src/libslic3r/GCode/MachineFrameTransform.cpp | 147 ++++++++++++++++++ src/libslic3r/GCode/MachineFrameTransform.hpp | 43 +++++ src/libslic3r/Preset.cpp | 9 ++ src/libslic3r/Print.cpp | 12 ++ src/libslic3r/PrintConfig.cpp | 65 ++++++++ src/libslic3r/PrintConfig.hpp | 30 ++++ src/libslic3r/PrintObject.cpp | 14 +- src/libslic3r/PrintObjectSlice.cpp | 53 ++++++- src/slic3r/GUI/Tab.cpp | 87 +++++++++++ 19 files changed, 621 insertions(+), 46 deletions(-) delete mode 100644 .claude/scheduled_tasks.lock create mode 100644 src/libslic3r/GCode/MachineFrameTransform.cpp create mode 100644 src/libslic3r/GCode/MachineFrameTransform.hpp diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock deleted file mode 100644 index cce3208834..0000000000 --- a/.claude/scheduled_tasks.lock +++ /dev/null @@ -1 +0,0 @@ -{"sessionId":"597ab29b-cd70-40f4-95d9-fee98b3968ad","pid":1159657,"acquiredAt":1775936463783} \ No newline at end of file diff --git a/src/libslic3r/BeltGCode.cpp b/src/libslic3r/BeltGCode.cpp index a34acf95bd..767d280579 100644 --- a/src/libslic3r/BeltGCode.cpp +++ b/src/libslic3r/BeltGCode.cpp @@ -17,6 +17,7 @@ void BeltGCode::init_belt_writer(Print &print, bool is_bbl_printers) belt_writer->set_belt_angle(print.config().belt_printer_angle.value); // Axis remap and build volume max are set by base GCode after init_belt_writer returns. belt_writer->set_belt_back_transform(print.config()); + belt_writer->set_machine_frame_transform(print.config()); m_writer = std::move(belt_writer); // Per-axis origin snap config. @@ -52,12 +53,33 @@ void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print) file.write_format("; belt_scale_y_angle = %.1f\n", print.config().belt_scale_y_angle.value); file.write_format("; belt_scale_z = %s\n", full_cfg.opt_serialize("belt_scale_z").c_str()); file.write_format("; belt_scale_z_angle = %.1f\n", print.config().belt_scale_z_angle.value); + file.write_format("; belt_mesh_transform_order = %s\n", full_cfg.opt_serialize("belt_mesh_transform_order").c_str()); // 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 configs + file.write_format("; gcode_shear_x = %s\n", full_cfg.opt_serialize("gcode_shear_x").c_str()); + file.write_format("; gcode_shear_x_angle = %.1f\n", print.config().gcode_shear_x_angle.value); + file.write_format("; gcode_shear_x_from = %s\n", full_cfg.opt_serialize("gcode_shear_x_from").c_str()); + file.write_format("; gcode_shear_y = %s\n", full_cfg.opt_serialize("gcode_shear_y").c_str()); + file.write_format("; gcode_shear_y_angle = %.1f\n", print.config().gcode_shear_y_angle.value); + file.write_format("; gcode_shear_y_from = %s\n", full_cfg.opt_serialize("gcode_shear_y_from").c_str()); + file.write_format("; gcode_shear_z = %s\n", full_cfg.opt_serialize("gcode_shear_z").c_str()); + file.write_format("; gcode_shear_z_angle = %.1f\n", print.config().gcode_shear_z_angle.value); + file.write_format("; gcode_shear_z_from = %s\n", full_cfg.opt_serialize("gcode_shear_z_from").c_str()); + file.write_format("; gcode_scale_x = %s\n", full_cfg.opt_serialize("gcode_scale_x").c_str()); + file.write_format("; gcode_scale_x_angle = %.1f\n", print.config().gcode_scale_x_angle.value); + file.write_format("; gcode_scale_y = %s\n", full_cfg.opt_serialize("gcode_scale_y").c_str()); + file.write_format("; gcode_scale_y_angle = %.1f\n", print.config().gcode_scale_y_angle.value); + file.write_format("; gcode_scale_z = %s\n", full_cfg.opt_serialize("gcode_scale_z").c_str()); + file.write_format("; gcode_scale_z_angle = %.1f\n", print.config().gcode_scale_z_angle.value); + file.write_format("; belt_gcode_transform_order = %s\n", full_cfg.opt_serialize("belt_gcode_transform_order").c_str()); + file.write_format("; post_gcode_remap_x = %s\n", full_cfg.opt_serialize("post_gcode_remap_x").c_str()); + file.write_format("; post_gcode_remap_y = %s\n", full_cfg.opt_serialize("post_gcode_remap_y").c_str()); + file.write_format("; post_gcode_remap_z = %s\n", full_cfg.opt_serialize("post_gcode_remap_z").c_str()); } void BeltGCode::on_set_origin(const PrintObject *obj, const Point &inst_shift) @@ -67,20 +89,31 @@ void BeltGCode::on_set_origin(const PrintObject *obj, const Point &inst_shift) // back_transform(T * origin) = origin (correct machine position). // This replaces the bbox-based axis snap with an exact formula. // - // Two flags trigger this path: + // Flags that trigger this path: // belt_preslice_global — full pipeline (scale * shear * remap) is global // preslice_remap_global — only the pre-slice remap is global + // belt_shear_z_global — Z-row shear treated as global (matches per-axis + // Z-offset added in PrintObjectSlice.cpp) // The XY origin adjustment uses the FULL forward transform either way, // because the back_transform applied during G-code emission is always the - // inverse of the full pipeline. When only the remap is configured, both - // flags produce identical math (T == R). + // inverse of the full pipeline. Without pre-multiplication under + // ShearThenScale order with sy != 1, machine_y of bed position cy ends up + // at cy/sy instead of cy, which also leaves the object bottom off the belt + // plane. bool use_global = m_config.belt_preslice_global.value || (m_config.preslice_remap_global.value - && BeltTransformPipeline::has_preslice_remap(m_config)); + && BeltTransformPipeline::has_preslice_remap(m_config)) + || (m_config.belt_shear_z_global.value + && m_config.belt_shear_z.value != BeltShearMode::None); if (use_global && m_config.belt_printer.value) { auto *belt_writer = dynamic_cast(m_writer.get()); if (belt_writer) { - // Clear snap — not needed with computed corrections + // The per-object lift (z_shift_val = max(0, -m_belt_min_z)) added by + // BeltSliceStrategy::apply_to_trafo is already compensated inside + // global_z_offset (via the shear_min_z term in PrintObjectSlice.cpp's + // preslice_global branch). Snap was previously used here for the same + // purpose, but with both active the lift gets subtracted twice. Clear + // any leftover snap state from a prior instance. for (int a = 0; a < 3; ++a) belt_writer->set_origin_snap(a, false, 0., 0.); } @@ -125,7 +158,12 @@ void BeltGCode::on_set_origin(const PrintObject *obj, const Point &inst_shift) unscale(inst_shift.y()), obj->belt_global_z_offset()); - // Compute this instance's machine-space bbox min + // Compute this instance's bbox min in the Cartesian frame (post back_transform + // + axis_remap, before machine_frame_transform). Using to_cartesian instead of + // to_machine_coords ensures the 8 axis-aligned bbox corners coincide with the + // geometry's extreme points — a property that breaks under shear, which would + // mis-normalize non-cubic shapes (inverted cone, benchy) by their bbox-volume + // corners rather than their actual lowest geometry point. BoundingBoxf3 bb = obj->model_object()->raw_bounding_box(); Vec3d mn = bb.min.cast(), mx = bb.max.cast(); Vec3d inst_min(std::numeric_limits::max(), @@ -135,7 +173,7 @@ void BeltGCode::on_set_origin(const PrintObject *obj, const Point &inst_shift) Vec3d c((i & 1) ? mx.x() : mn.x(), (i & 2) ? mx.y() : mn.y(), (i & 4) ? mx.z() : mn.z()); - Vec3d mc = belt_writer->to_machine_coords(full * c + shift); + Vec3d mc = belt_writer->to_cartesian(full * c + shift); for (int a = 0; a < 3; ++a) inst_min[a] = std::min(inst_min[a], mc[a]); } diff --git a/src/libslic3r/BeltGCodeWriter.cpp b/src/libslic3r/BeltGCodeWriter.cpp index ea90f2588e..1987d0127a 100644 --- a/src/libslic3r/BeltGCodeWriter.cpp +++ b/src/libslic3r/BeltGCodeWriter.cpp @@ -34,6 +34,11 @@ void BeltGCodeWriter::set_belt_back_transform(const PrintConfig &config) m_belt_back_transform.init_from_config(config); } +void BeltGCodeWriter::set_machine_frame_transform(const PrintConfig &config) +{ + m_machine_frame_transform.init_from_config(config); +} + void BeltGCodeWriter::set_origin_snap(int axis, bool enable, double offset, double bbox_min) { if (axis >= 0 && axis < 3) { @@ -43,17 +48,26 @@ void BeltGCodeWriter::set_origin_snap(int axis, bool enable, double offset, doub } } +Vec3d BeltGCodeWriter::to_cartesian(const Vec3d &pos) const +{ + // back_transform → axis_remap, no origin_snap, no machine_frame_transform. + return apply_axis_remap(m_belt_back_transform.apply(pos)); +} + Vec3d BeltGCodeWriter::to_machine_coords(const Vec3d &pos) const { - // Step 1: Undo the shear/scale applied during slicing. - Vec3d p = m_belt_back_transform.apply(pos); - // Step 2: Apply axis remap (uses inherited base class method). - Vec3d result = apply_axis_remap(p); - // Step 3: Per-axis origin snap. + // Step 1+2: To Cartesian (back_transform + axis_remap). + Vec3d result = to_cartesian(pos); + // Step 3: Per-axis origin snap (computed in the Cartesian frame). for (int i = 0; i < 3; ++i) if (m_origin_snap[i]) result[i] -= (m_origin_bbox_min[i] - m_origin_offset[i]); - return result; + // Step 4: Machine-frame transform (gcode_shear / gcode_scale / post_gcode_remap) + // applied LAST so it acts as a global linear transform on the placed coords. + // Order matters: putting it before origin_snap would feed sheared bbox corners + // into the snap's per-object min calculation, mis-normalizing non-cubic geometries + // (the corners of the original bbox aren't extreme points of the sheared shape). + return m_machine_frame_transform.apply(result); } // ---- Overridden movement methods ------------------------------------------ diff --git a/src/libslic3r/BeltGCodeWriter.hpp b/src/libslic3r/BeltGCodeWriter.hpp index 828dd43bb3..60fd93f3e0 100644 --- a/src/libslic3r/BeltGCodeWriter.hpp +++ b/src/libslic3r/BeltGCodeWriter.hpp @@ -2,6 +2,7 @@ #include "GCodeWriter.hpp" #include "GCode/BeltBackTransform.hpp" +#include "GCode/MachineFrameTransform.hpp" namespace Slic3r { @@ -21,8 +22,13 @@ public: void set_belt_angle(double angle_deg); bool is_belt_printer() const { return m_belt_angle_rad != 0.; } void set_belt_back_transform(const PrintConfig &config); + void set_machine_frame_transform(const PrintConfig &config); void set_origin_snap(int axis, bool enable, double offset, double bbox_min); Vec3d to_machine_coords(const Vec3d &pos) const; + // back_transform + axis_remap only (no origin_snap, no machine_frame_transform). + // Used by on_set_origin for bbox computation in the Cartesian frame, where + // axis-aligned bbox corners coincide with the geometry's extreme points. + Vec3d to_cartesian(const Vec3d &pos) const; // First-layer plane: when set to a non-null active evaluator, travel // speed selection consults the plane per-move and uses @@ -47,7 +53,8 @@ protected: private: double m_belt_angle_rad = 0.; - BeltBackTransform m_belt_back_transform; + BeltBackTransform m_belt_back_transform; + MachineFrameTransform m_machine_frame_transform; bool m_origin_snap[3] = {false, false, false}; double m_origin_offset[3] = {0., 0., 0.}; double m_origin_bbox_min[3] = {0., 0., 0.}; diff --git a/src/libslic3r/BeltSliceStrategy.cpp b/src/libslic3r/BeltSliceStrategy.cpp index c1a300869e..850a4ad221 100644 --- a/src/libslic3r/BeltSliceStrategy.cpp +++ b/src/libslic3r/BeltSliceStrategy.cpp @@ -1,6 +1,7 @@ #include "BeltSliceStrategy.hpp" #include +#include namespace Slic3r { @@ -15,6 +16,7 @@ BeltSliceStrategy::BeltSliceStrategy(const PrintConfig &config) { m_shear = BeltTransformPipeline::build_shear_matrix(config, &m_has_shear); m_scale = BeltTransformPipeline::build_scale_matrix(config, &m_has_scale); + m_order = config.belt_mesh_transform_order.value; } void BeltSliceStrategy::apply_to_trafo(Transform3d &trafo, @@ -22,10 +24,13 @@ void BeltSliceStrategy::apply_to_trafo(Transform3d &trafo, bool has_remap, double *out_belt_min_z) const { - // Shear + scale (belt-only; pre-slice remap is handled separately). + // ScaleThenShear: applied to a point, scale runs first then shear (m_shear * m_scale). + // ShearThenScale: applied to a point, shear runs first then scale (m_scale * m_shear). if (m_has_shear || m_has_scale) { Transform3d belt_xform = Transform3d::Identity(); - belt_xform.linear() = m_scale * m_shear; + belt_xform.linear() = (m_order == BeltTransformOrder::ScaleThenShear) + ? Matrix3d(m_shear * m_scale) + : Matrix3d(m_scale * m_shear); trafo = belt_xform * trafo; } @@ -47,8 +52,12 @@ void BeltSliceStrategy::apply_to_trafo(Transform3d &trafo, z_shift.matrix()(2, 3) = belt_z_shift_val; trafo = z_shift * trafo; } - if (out_belt_min_z) - *out_belt_min_z = (min_z != std::numeric_limits::max()) ? min_z : 0.; + if (out_belt_min_z) { + double new_val = (min_z != std::numeric_limits::max()) ? min_z : 0.; + BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] write m_belt_min_z tid=" << std::this_thread::get_id() + << " target=" << out_belt_min_z << " old=" << *out_belt_min_z << " new=" << new_val; + *out_belt_min_z = new_val; + } } } diff --git a/src/libslic3r/BeltSliceStrategy.hpp b/src/libslic3r/BeltSliceStrategy.hpp index ef6e038ee7..c2362e3552 100644 --- a/src/libslic3r/BeltSliceStrategy.hpp +++ b/src/libslic3r/BeltSliceStrategy.hpp @@ -34,10 +34,11 @@ public: private: explicit BeltSliceStrategy(const PrintConfig &config); - bool m_has_shear = false; - bool m_has_scale = false; - Matrix3d m_shear = Matrix3d::Identity(); - Matrix3d m_scale = Matrix3d::Identity(); + bool m_has_shear = false; + bool m_has_scale = false; + Matrix3d m_shear = Matrix3d::Identity(); + Matrix3d m_scale = Matrix3d::Identity(); + BeltTransformOrder m_order = BeltTransformOrder::ScaleThenShear; }; } // namespace Slic3r diff --git a/src/libslic3r/BeltTransform.cpp b/src/libslic3r/BeltTransform.cpp index 9bb66fcfb6..6c0838b2eb 100644 --- a/src/libslic3r/BeltTransform.cpp +++ b/src/libslic3r/BeltTransform.cpp @@ -103,9 +103,15 @@ Transform3d BeltTransformPipeline::build_forward_transform(const PrintConfig &co bool scale_active = false; Matrix3d scale = build_scale_matrix(config, &scale_active); - // Pipeline: scale * shear * pre_remap + // Match the mesh-side ordering selected by belt_mesh_transform_order so + // BeltBackTransform inverts the same composition that BeltSliceStrategy + // applied to the mesh. + // ScaleThenShear: applied to p, scale runs first then shear (shear * scale). + // ShearThenScale: applied to p, shear runs first then scale (scale * shear). Transform3d combined = Transform3d::Identity(); - combined.linear() = scale * shear; + combined.linear() = (config.belt_mesh_transform_order.value == BeltTransformOrder::ScaleThenShear) + ? Matrix3d(shear * scale) + : Matrix3d(scale * shear); combined = combined * pre_remap; return combined; } @@ -160,19 +166,30 @@ BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl( BeltTransformPipeline::BeltHeightResult result; result.object_height = original_height; - // Extract Z-axis shear/scale config. + // Extract Z-axis shear/scale + per-axis scale + transform order from config. BeltShearMode z_shear_mode; double z_shear_angle; BeltScaleMode z_scale_mode; double z_scale_angle; int z_shear_from; + BeltScaleMode from_scale_mode; // scale on the shear's source axis + double from_scale_angle; + BeltTransformOrder order; if constexpr (std::is_same_v) { - z_shear_mode = config.belt_shear_z.value; - z_shear_angle = config.belt_shear_z_angle.value; - z_scale_mode = config.belt_scale_z.value; - z_scale_angle = config.belt_scale_z_angle.value; - z_shear_from = int(config.belt_shear_z_from.value); + z_shear_mode = config.belt_shear_z.value; + z_shear_angle = config.belt_shear_z_angle.value; + z_scale_mode = config.belt_scale_z.value; + z_scale_angle = config.belt_scale_z_angle.value; + z_shear_from = int(config.belt_shear_z_from.value); + order = config.belt_mesh_transform_order.value; + if (z_shear_from == 0) { + from_scale_mode = config.belt_scale_x.value; + from_scale_angle = config.belt_scale_x_angle.value; + } else { + from_scale_mode = config.belt_scale_y.value; + from_scale_angle = config.belt_scale_y_angle.value; + } } else { // DynamicPrintConfig path auto get_shear = [&](const char *key) { @@ -191,11 +208,23 @@ BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl( auto *opt = config.template option>(key); return opt ? int(opt->value) : 1; }; + auto get_order = [&](const char *key) { + auto *opt = config.template option>(key); + return opt ? opt->value : BeltTransformOrder::ScaleThenShear; + }; z_shear_mode = get_shear("belt_shear_z"); z_shear_angle = get_float("belt_shear_z_angle"); z_scale_mode = get_scale("belt_scale_z"); z_scale_angle = get_float("belt_scale_z_angle"); z_shear_from = get_axis("belt_shear_z_from"); + order = get_order("belt_mesh_transform_order"); + if (z_shear_from == 0) { + from_scale_mode = get_scale("belt_scale_x"); + from_scale_angle = get_float("belt_scale_x_angle"); + } else { + from_scale_mode = get_scale("belt_scale_y"); + from_scale_angle = get_float("belt_scale_y_angle"); + } } bool has_z_shear = z_shear_mode != BeltShearMode::None; @@ -206,7 +235,8 @@ BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl( double shear_factor = has_z_shear ? BeltTransformPipeline::compute_shear_factor(z_shear_mode, z_shear_angle) : 0.; - double scale_z = BeltTransformPipeline::compute_scale_factor(z_scale_mode, z_scale_angle); + double scale_z = BeltTransformPipeline::compute_scale_factor(z_scale_mode, z_scale_angle); + double scale_from = BeltTransformPipeline::compute_scale_factor(from_scale_mode, from_scale_angle); if (has_z_shear && std::abs(shear_factor) > EPSILON) { int from = z_shear_from; @@ -214,14 +244,29 @@ BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl( double max_rz = std::numeric_limits::lowest(); for (double vz : {bb.min.z(), bb.max.z()}) for (double vs : {bb.min(from), bb.max(from)}) { - double new_z = scale_z * (vz + shear_factor * vs); + // Mesh-frame new_z computed per ordering. + // scale-then-shear: Z_s = sz*Z_m + s_from*tan(α)*from_m + // shear-then-scale: Z_s = sz*(Z_m + tan(α)*from_m) + double new_z = (order == BeltTransformOrder::ScaleThenShear) + ? scale_z * vz + scale_from * shear_factor * vs + : scale_z * (vz + shear_factor * vs); min_rz = std::min(min_rz, new_z); max_rz = std::max(max_rz, new_z); } - result.object_height = max_rz - min_rz; - result.floor_params.shear_factor = shear_factor; - result.floor_params.from_axis = from; - result.floor_params.z_shift = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.); + result.object_height = max_rz - min_rz; + // Effective slicer-frame slope of the belt surface (Z_m=0 line): + // scale-then-shear: Z_s = tan(α) * Y_s → slope = tan(α) + // shear-then-scale: Z_s = sz/s_from * tan(α) * Y_s → slope = sz*tan(α)/s_from + // The downstream cutoff formula `Y_s = (print_z - z_shift) / slope` + // and floor_print_z(Y_s) = slope * Y_s + z_shift use this slope. + double effective_shear = (order == BeltTransformOrder::ScaleThenShear) + ? shear_factor + : (std::abs(scale_from) > EPSILON + ? scale_z * shear_factor / scale_from + : shear_factor); + result.floor_params.shear_factor = effective_shear; + result.floor_params.from_axis = from; + result.floor_params.z_shift = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.); } else { result.object_height = original_height * scale_z; } diff --git a/src/libslic3r/BeltTransform.hpp b/src/libslic3r/BeltTransform.hpp index 3c0665c252..f15a9995c3 100644 --- a/src/libslic3r/BeltTransform.hpp +++ b/src/libslic3r/BeltTransform.hpp @@ -105,7 +105,9 @@ public: // Also sets has_scale_out if non-null. static Matrix3d build_scale_matrix(const PrintConfig &config, bool *has_scale_out = nullptr); - // Combined forward transform: scale * shear * pre_remap. + // Combined forward transform. Shear/scale order is selected by + // belt_mesh_transform_order so the result matches what BeltSliceStrategy + // applied to the mesh (BeltBackTransform inverts this). // Does NOT include the per-object Z-shift. static Transform3d build_forward_transform(const PrintConfig &config); diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index a887336361..f9910dc894 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -221,6 +221,8 @@ set(lisbslic3r_sources GCode/AvoidCrossingPerimeters.hpp GCode/BeltBackTransform.cpp GCode/BeltBackTransform.hpp + GCode/MachineFrameTransform.cpp + GCode/MachineFrameTransform.hpp GCode/ConflictChecker.cpp GCode/ConflictChecker.hpp GCode/CoolingBuffer.cpp diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index c3c3532364..b2aba1e35c 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -6462,6 +6462,20 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, auto _mm3_per_mm = path.mm3_per_mm * this->config().print_flow_ratio; _mm3_per_mm *= filament_flow_ratio; + // Belt printer: compensate for the volume change introduced by the mesh + // forward transform. path.mm3_per_mm is derived from slicer-frame layer + // height × line width, but a slicer-frame slab of volume V maps under the + // back-transform to a machine-frame region of volume V / |det(T)|. When + // det(T) > 1 — e.g. belt_scale_y = 1/cos(α) ≈ 1.155 at α = 30° — the + // machine slab holds less plastic than the slicer thinks it is filling + // and we over-extrude by det(T). Pure shear has det 1, so this is a + // no-op until scale factors deviate from 1. + if (m_config.belt_printer.value) { + double det = std::abs(BeltTransformPipeline::build_forward_transform(m_config).linear().determinant()); + if (det > EPSILON) + _mm3_per_mm /= det; + } + if (path.role() == erTopSolidInfill) { _mm3_per_mm *= m_config.top_solid_infill_flow_ratio; } else if (path.role() == erBottomSurface) { diff --git a/src/libslic3r/GCode/MachineFrameTransform.cpp b/src/libslic3r/GCode/MachineFrameTransform.cpp new file mode 100644 index 0000000000..10a658c7f7 --- /dev/null +++ b/src/libslic3r/GCode/MachineFrameTransform.cpp @@ -0,0 +1,147 @@ +#include "MachineFrameTransform.hpp" +#include "../BeltTransform.hpp" +#include "../BoundingBox.hpp" + +namespace Slic3r { + +namespace { + +// Build the post-gcode axis-remap transform (mirrors BeltTransformPipeline::build_preslice_remap +// but reads post_gcode_remap_* keys). Includes Rev-mode translation derived from build volume. +Transform3d build_post_gcode_remap(const PrintConfig &config) +{ + Transform3d remap = Transform3d::Identity(); + + int rx = int(config.post_gcode_remap_x.value); + int ry = int(config.post_gcode_remap_y.value); + int rz = int(config.post_gcode_remap_z.value); + + if (rx == int(RemapAxis::PosX) && ry == int(RemapAxis::PosY) && rz == int(RemapAxis::PosZ)) + return remap; + + auto remap_column = [](int r) -> Vec3d { + int axis = r % 3; + Vec3d col = Vec3d::Zero(); + if (r < 3) col[axis] = 1.0; + else if (r < 6) col[axis] = -1.0; + else col[axis] = -1.0; // Rev: max - pos + return col; + }; + + Matrix3d lin; + lin.col(0) = remap_column(rx); + lin.col(1) = remap_column(ry); + lin.col(2) = remap_column(rz); + remap.linear() = lin; + + if (rx >= 6 || ry >= 6 || 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 trans = Vec3d::Zero(); + auto add_rev = [&](int r, int out) { + if (r >= 6) trans[out] = vol_max[r % 3]; + }; + add_rev(rx, 0); + add_rev(ry, 1); + add_rev(rz, 2); + remap.translation() = trans; + } + + return remap; +} + +// Build the 3x3 shear matrix from gcode_shear_* keys. +Matrix3d build_gcode_shear_matrix(const PrintConfig &config, bool &active) +{ + struct AxisShear { BeltShearMode mode; double angle; int from; }; + AxisShear axes[3] = { + { config.gcode_shear_x.value, config.gcode_shear_x_angle.value, int(config.gcode_shear_x_from.value) }, + { config.gcode_shear_y.value, config.gcode_shear_y_angle.value, int(config.gcode_shear_y_from.value) }, + { config.gcode_shear_z.value, config.gcode_shear_z_angle.value, int(config.gcode_shear_z_from.value) }, + }; + + Matrix3d shear = Matrix3d::Identity(); + active = false; + for (int row = 0; row < 3; ++row) { + if (axes[row].mode != BeltShearMode::None) { + double factor = BeltTransformPipeline::compute_shear_factor(axes[row].mode, axes[row].angle); + if (std::abs(factor) > EPSILON) { + shear(row, axes[row].from) += factor; + active = true; + } + } + } + return shear; +} + +// Build the 3x3 diagonal scale matrix from gcode_scale_* keys. +Matrix3d build_gcode_scale_matrix(const PrintConfig &config, bool &active) +{ + double sx = BeltTransformPipeline::compute_scale_factor(config.gcode_scale_x.value, config.gcode_scale_x_angle.value); + double sy = BeltTransformPipeline::compute_scale_factor(config.gcode_scale_y.value, config.gcode_scale_y_angle.value); + double sz = BeltTransformPipeline::compute_scale_factor(config.gcode_scale_z.value, config.gcode_scale_z_angle.value); + + active = (std::abs(sx - 1.) > EPSILON || + std::abs(sy - 1.) > EPSILON || + std::abs(sz - 1.) > EPSILON); + + Matrix3d scale = Matrix3d::Identity(); + if (active) { + scale(0, 0) = sx; + scale(1, 1) = sy; + scale(2, 2) = sz; + } + return scale; +} + +bool has_post_gcode_remap(const PrintConfig &config) +{ + return int(config.post_gcode_remap_x.value) != int(RemapAxis::PosX) || + int(config.post_gcode_remap_y.value) != int(RemapAxis::PosY) || + int(config.post_gcode_remap_z.value) != int(RemapAxis::PosZ); +} + +} // namespace + +bool MachineFrameTransform::init_from_config(const PrintConfig &config) +{ + m_active = false; + m_transform = Transform3d::Identity(); + + if (!config.belt_printer.value) + return false; + + Transform3d post_remap = build_post_gcode_remap(config); + bool shear_active = false; + Matrix3d shear = build_gcode_shear_matrix(config, shear_active); + bool scale_active = false; + Matrix3d scale = build_gcode_scale_matrix(config, scale_active); + + if (!shear_active && !scale_active && !has_post_gcode_remap(config)) + return false; + + // Compose per belt_gcode_transform_order: + // ScaleThenShear: applied to p, scale runs first then shear (shear * scale). + // ShearThenScale: applied to p, shear runs first then scale (scale * shear). + Transform3d combined = Transform3d::Identity(); + combined.linear() = (config.belt_gcode_transform_order.value == BeltTransformOrder::ScaleThenShear) + ? Matrix3d(shear * scale) + : Matrix3d(scale * shear); + combined = combined * post_remap; + + if (combined.isApprox(Transform3d::Identity())) + return false; + + m_transform = combined; + m_active = true; + return true; +} + +Vec3d MachineFrameTransform::apply(const Vec3d &pos) const +{ + if (!m_active) + return pos; + return m_transform * pos; +} + +} // namespace Slic3r diff --git a/src/libslic3r/GCode/MachineFrameTransform.hpp b/src/libslic3r/GCode/MachineFrameTransform.hpp new file mode 100644 index 0000000000..4a2f189802 --- /dev/null +++ b/src/libslic3r/GCode/MachineFrameTransform.hpp @@ -0,0 +1,43 @@ +#ifndef slic3r_MachineFrameTransform_hpp_ +#define slic3r_MachineFrameTransform_hpp_ + +#include "../libslic3r.h" +#include "../Point.hpp" +#include "../PrintConfig.hpp" + +namespace Slic3r { + +// Post-stage machine-frame transform for belt printers. +// +// Applied in BeltGCodeWriter::to_machine_coords AFTER the back-transform +// and the existing gcode_remap_* axis remap, and BEFORE per-axis origin snap. +// Maps Cartesian (axis-permuted) G-code coordinates into the printer's +// physical machine frame using a parallel set of options: +// gcode_shear_x/y/z + _angle + _from +// gcode_scale_x/y/z + _angle +// post_gcode_remap_x/y/z +// +// Composition matches the mesh-side pipeline: shear * scale * post_remap. +class MachineFrameTransform +{ +public: + MachineFrameTransform() = default; + + // Initialize from belt printer config. Returns true if a non-identity + // transform was computed. Inactive when belt_printer is disabled or + // all three sub-stages are identity. + bool init_from_config(const PrintConfig &config); + + // Apply the transform to a point. Returns pos unchanged if not active. + Vec3d apply(const Vec3d &pos) const; + + bool is_active() const { return m_active; } + +private: + bool m_active = false; + Transform3d m_transform = Transform3d::Identity(); +}; + +} // namespace Slic3r + +#endif // slic3r_MachineFrameTransform_hpp_ diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 6c6382f91d..a62628b3cd 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1326,8 +1326,17 @@ static std::vector s_Preset_printer_options { "belt_shear_y", "belt_shear_y_angle", "belt_shear_y_from", "belt_shear_y_global", "belt_shear_z", "belt_shear_z_angle", "belt_shear_z_from", "belt_shear_z_global", "belt_scale_x", "belt_scale_x_angle", "belt_scale_y", "belt_scale_y_angle", "belt_scale_z", "belt_scale_z_angle", + "belt_mesh_transform_order", "preslice_remap_x", "preslice_remap_y", "preslice_remap_z", "preslice_remap_global", "gcode_remap_x", "gcode_remap_y", "gcode_remap_z", "gcode_back_transform", + "gcode_shear_x", "gcode_shear_x_angle", "gcode_shear_x_from", + "gcode_shear_y", "gcode_shear_y_angle", "gcode_shear_y_from", + "gcode_shear_z", "gcode_shear_z_angle", "gcode_shear_z_from", + "gcode_scale_x", "gcode_scale_x_angle", + "gcode_scale_y", "gcode_scale_y_angle", + "gcode_scale_z", "gcode_scale_z_angle", + "belt_gcode_transform_order", + "post_gcode_remap_x", "post_gcode_remap_y", "post_gcode_remap_z", "belt_preslice_global", "first_layer_plane", "first_layer_plane_offset", "first_layer_plane_thickness", "belt_origin_snap_x", "belt_origin_offset_x", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 09a8a50923..de5621a67a 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -104,6 +104,15 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n "gcode_remap_x", "gcode_remap_y", "gcode_remap_z", + // Machine-frame transforms (only affect G-code output, not slicing). + "gcode_shear_x", "gcode_shear_x_angle", "gcode_shear_x_from", + "gcode_shear_y", "gcode_shear_y_angle", "gcode_shear_y_from", + "gcode_shear_z", "gcode_shear_z_angle", "gcode_shear_z_from", + "gcode_scale_x", "gcode_scale_x_angle", + "gcode_scale_y", "gcode_scale_y_angle", + "gcode_scale_z", "gcode_scale_z_angle", + "belt_gcode_transform_order", + "post_gcode_remap_x", "post_gcode_remap_y", "post_gcode_remap_z", "belt_origin_snap_x", "belt_origin_offset_x", "belt_origin_snap_y", "belt_origin_offset_y", "belt_origin_snap_z", "belt_origin_offset_z", @@ -310,6 +319,9 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "belt_scale_y_angle" || opt_key == "belt_scale_z" || opt_key == "belt_scale_z_angle" + || opt_key == "belt_mesh_transform_order" + || 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") { diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 5a9b63fc0f..ed0892ea04 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -307,6 +307,12 @@ static t_config_enum_values s_keys_map_BeltAxis { }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltAxis) +static t_config_enum_values s_keys_map_BeltTransformOrder { + { "scale_then_shear", int(BeltTransformOrder::ScaleThenShear) }, + { "shear_then_scale", int(BeltTransformOrder::ShearThenScale) }, +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltTransformOrder) + static t_config_enum_values s_keys_map_BeltScaleMode { { "none", int(BeltScaleMode::None) }, { "inv_sin", int(BeltScaleMode::InvSin) }, @@ -6320,6 +6326,24 @@ void PrintConfigDef::init_fff_params() add_belt_scale_mode("belt_scale_z", "Function", BeltScaleMode::None); add_belt_scale_angle("belt_scale_z_angle", "Angle"); + auto add_belt_transform_order = [this](const char *key, const char *label, const char *tooltip) { + auto def = this->add(key, coEnum); + def->label = L(label); + def->category = L("Printable space"); + def->tooltip = L(tooltip); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values = {"scale_then_shear", "shear_then_scale"}; + def->enum_labels = {L("Scale, then shear"), L("Shear, then scale")}; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(BeltTransformOrder::ScaleThenShear)); + }; + + add_belt_transform_order("belt_mesh_transform_order", "Mesh transform order", + "Order in which the mesh shear and scale matrices are composed before slicing. " + "'Scale, then shear' applies scale first and then shear (current default). " + "'Shear, then scale' applies shear first and then scale. The g-code back-transform " + "follows the same order so that it correctly inverts the mesh transform."); + // G-code axis remap with sign auto add_belt_remap = [this](const char *key, const char *label, const char *tooltip, RemapAxis default_axis) { auto def = this->add(key, coEnum); @@ -6367,6 +6391,47 @@ void PrintConfigDef::init_fff_params() add_belt_remap("gcode_remap_y", "Y", "Which slicing axis maps to machine Y in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosY); add_belt_remap("gcode_remap_z", "Z", "Which slicing axis maps to machine Z in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosZ); + // Machine-frame G-code transforms: applied AFTER back-transform and gcode_remap, + // before per-axis origin snap. Maps Cartesian G-code to the printer's physical machine frame. + add_belt_shear_mode ("gcode_shear_x", "Function", BeltShearMode::None); + add_belt_shear_angle("gcode_shear_x_angle", "Angle"); + add_belt_axis_enum ("gcode_shear_x_from", "From", "Source axis for X shear in the machine-frame stage.", BeltAxis::Z); + + add_belt_shear_mode ("gcode_shear_y", "Function", BeltShearMode::None); + add_belt_shear_angle("gcode_shear_y_angle", "Angle"); + add_belt_axis_enum ("gcode_shear_y_from", "From", "Source axis for Y shear in the machine-frame stage.", BeltAxis::Z); + + add_belt_shear_mode ("gcode_shear_z", "Function", BeltShearMode::None); + add_belt_shear_angle("gcode_shear_z_angle", "Angle"); + add_belt_axis_enum ("gcode_shear_z_from", "From", "Source axis for Z shear in the machine-frame stage.", BeltAxis::Y); + + add_belt_scale_mode ("gcode_scale_x", "Function", BeltScaleMode::None); + add_belt_scale_angle("gcode_scale_x_angle", "Angle"); + + add_belt_scale_mode ("gcode_scale_y", "Function", BeltScaleMode::None); + add_belt_scale_angle("gcode_scale_y_angle", "Angle"); + + add_belt_scale_mode ("gcode_scale_z", "Function", BeltScaleMode::None); + add_belt_scale_angle("gcode_scale_z_angle", "Angle"); + + add_belt_transform_order("belt_gcode_transform_order", "G-code transform order", + "Order in which the machine-frame shear and scale matrices are composed when " + "applied to G-code coordinates. 'Scale, then shear' applies scale first and then " + "shear (current default). 'Shear, then scale' applies shear first and then scale."); + + add_belt_remap("post_gcode_remap_x", "X", + "Axis remap in the machine-frame stage. Applied AFTER gcode_remap, " + "to put coordinates into the printer's physical axis labelling. Default +X: no change.", + RemapAxis::PosX); + add_belt_remap("post_gcode_remap_y", "Y", + "Axis remap in the machine-frame stage. Applied AFTER gcode_remap, " + "to put coordinates into the printer's physical axis labelling. Default +Y: no change.", + RemapAxis::PosY); + add_belt_remap("post_gcode_remap_z", "Z", + "Axis remap in the machine-frame stage. Applied AFTER gcode_remap, " + "to put coordinates into the printer's physical axis labelling. Default +Z: no change.", + RemapAxis::PosZ); + def = this->add("gcode_back_transform", coBool); def->label = L("G-code back-transform"); def->category = L("Printable space"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 601cfcbb7e..904ecd742d 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -183,6 +183,15 @@ enum class BeltAxis Z = 2, }; +// Order in which the belt shear and scale matrices are composed. +// ScaleThenShear: applied to a point p, the result is shear(scale(p)). +// ShearThenScale: applied to a point p, the result is scale(shear(p)). +enum class BeltTransformOrder +{ + ScaleThenShear = 0, + ShearThenScale = 1, +}; + enum class RemapAxis { PosX = 0, PosY = 1, PosZ = 2, @@ -571,6 +580,7 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SlicingMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltShearMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltScaleMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltAxis) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltTransformOrder) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(RemapAxis) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltSupportFloorMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltSupportZOffsetMode) @@ -1535,6 +1545,26 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionEnum, gcode_remap_x)) ((ConfigOptionEnum, gcode_remap_y)) ((ConfigOptionEnum, gcode_remap_z)) + ((ConfigOptionEnum, gcode_shear_x)) + ((ConfigOptionFloat, gcode_shear_x_angle)) + ((ConfigOptionEnum, gcode_shear_x_from)) + ((ConfigOptionEnum, gcode_shear_y)) + ((ConfigOptionFloat, gcode_shear_y_angle)) + ((ConfigOptionEnum, gcode_shear_y_from)) + ((ConfigOptionEnum, gcode_shear_z)) + ((ConfigOptionFloat, gcode_shear_z_angle)) + ((ConfigOptionEnum, gcode_shear_z_from)) + ((ConfigOptionEnum, gcode_scale_x)) + ((ConfigOptionFloat, gcode_scale_x_angle)) + ((ConfigOptionEnum, gcode_scale_y)) + ((ConfigOptionFloat, gcode_scale_y_angle)) + ((ConfigOptionEnum, gcode_scale_z)) + ((ConfigOptionFloat, gcode_scale_z_angle)) + ((ConfigOptionEnum, belt_mesh_transform_order)) + ((ConfigOptionEnum, belt_gcode_transform_order)) + ((ConfigOptionEnum, post_gcode_remap_x)) + ((ConfigOptionEnum, post_gcode_remap_y)) + ((ConfigOptionEnum, post_gcode_remap_z)) ((ConfigOptionBool, gcode_back_transform)) ((ConfigOptionBool, belt_preslice_global)) ((ConfigOptionEnum, first_layer_plane)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 78b75a8bbb..596053f685 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -3,6 +3,8 @@ #include "Point.hpp" #include "Print.hpp" #include "BeltTransform.hpp" + +#include #include "BoundingBox.hpp" #include "ClipperUtils.hpp" #include "ElephantFootCompensation.hpp" @@ -425,11 +427,15 @@ std::vector> PrintObject::detect_extruder_geometric_unprintables() // 3) Generates perimeters, gap fills and fill regions (fill regions of type stInternal). void PrintObject::make_perimeters() { + BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] make_perimeters request tid=" << std::this_thread::get_id() << " obj=" << this; // prerequisites this->slice(); - if (! this->set_started(posPerimeters)) + if (! this->set_started(posPerimeters)) { + BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] make_perimeters SKIP tid=" << std::this_thread::get_id() << " obj=" << this << " (already started/done)"; return; + } + BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] make_perimeters ENTER tid=" << std::this_thread::get_id() << " obj=" << this; m_print->set_status(15, L("Generating walls")); BOOST_LOG_TRIVIAL(info) << "Generating walls..." << log_memory_info(); @@ -527,6 +533,7 @@ void PrintObject::make_perimeters() m_print->throw_if_canceled(); BOOST_LOG_TRIVIAL(debug) << "Generating perimeters in parallel - end"; + BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] make_perimeters EXIT tid=" << std::this_thread::get_id() << " obj=" << this; this->set_done(posPerimeters); } @@ -822,7 +829,9 @@ void PrintObject::detect_overhangs_for_lift() void PrintObject::generate_support_material() { + BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] generate_support_material request tid=" << std::this_thread::get_id() << " obj=" << this; if (this->set_started(posSupportMaterial)) { + BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] generate_support_material ENTER tid=" << std::this_thread::get_id() << " obj=" << this; this->clear_support_layers(); if(!has_support() && !m_print->get_no_check_flag()) { @@ -863,7 +872,10 @@ void PrintObject::generate_support_material() this->_generate_support_material(); m_print->throw_if_canceled(); } + BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] generate_support_material EXIT tid=" << std::this_thread::get_id() << " obj=" << this; this->set_done(posSupportMaterial); + } else { + BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] generate_support_material SKIP tid=" << std::this_thread::get_id() << " obj=" << this << " (already started/done)"; } } diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 23e3583a2f..4454ce87ea 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -1,5 +1,6 @@ #include #include +#include #include @@ -837,8 +838,12 @@ void groupingVolumesForBrim(PrintObject* object, LayerPtrs& layers, int firstLay // Resulting expolygons of layer regions are marked as Internal. void PrintObject::slice() { - if (! this->set_started(posSlice)) + BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] slice request tid=" << std::this_thread::get_id() << " obj=" << this; + if (! this->set_started(posSlice)) { + BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] slice SKIP tid=" << std::this_thread::get_id() << " obj=" << this << " (already started/done)"; return; + } + BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] slice ENTER tid=" << std::this_thread::get_id() << " obj=" << this; //BBS: add flag to reload scene for shell rendering m_print->set_status(5, L("Slicing mesh"), PrintBase::SlicingStatus::RELOAD_SCENE); std::vector layer_height_profile; @@ -942,11 +947,25 @@ void PrintObject::slice() Vec3d d(unscale(inst_shift.x()), unscale(inst_shift.y()), 0.); Vec3d c = T.linear() * d - d; - global_z_offset = c.z(); + // Per-object shape contribution: BeltSliceStrategy::apply_to_trafo + // lifts the mesh by max(0, -m_belt_min_z) to keep slicer-frame Z + // above 0. Two objects at the same bed position but different + // m_belt_min_z (e.g. cube vs inverted-cone tip) otherwise end up + // at the same print_z, which causes the inverted-cone tip to + // start on the same layer as the cube's lowest sheared corner. + double belt_surface_z = BeltTransformPipeline::has_preslice_remap(pcfg) + ? BeltTransformPipeline::remap_bbox(*this->model_object(), pcfg).min.z() : 0.; + double shear_min_z = m_belt_min_z - belt_surface_z; + + global_z_offset = c.z() + shear_min_z; + BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] write m_belt_global_xy_correction tid=" << std::this_thread::get_id() + << " obj=" << this << " old=(" << m_belt_global_xy_correction.x() << "," << m_belt_global_xy_correction.y() + << ") new=(" << c.x() << "," << c.y() << ")"; m_belt_global_xy_correction = Vec2d(c.x(), c.y()); BOOST_LOG_TRIVIAL(warning) << "Belt preslice_global: correction=(" - << c.x() << ", " << c.y() << ", " << c.z() << ")"; + << c.x() << ", " << c.y() << ", " << c.z() << ")" + << " shear_min_z=" << shear_min_z << " (m_belt_min_z=" << m_belt_min_z << ")"; } else { struct GAxis { BeltShearMode mode; double angle; int from; bool global; }; GAxis gaxes[3] = { @@ -959,13 +978,27 @@ void PrintObject::slice() // (X/Y row shears with global would offset X/Y, not Z — not useful here.) const auto &za = gaxes[2]; // Z row if (za.global && za.mode != BeltShearMode::None && za.from < 2) { - double factor = BeltTransformPipeline::compute_shear_factor(za.mode, za.angle); + // Use the full forward-transform correction (same formula as + // preslice_global) so the per-bed-position offset matches what + // BeltGCode::on_set_origin's T.linear() pre-multiplication + // expects after back-transform. The simple `cy*tan(α)` form + // is exact only for ScaleThenShear; under ShearThenScale with + // sy != 1 it leaves the object bottom off the belt plane by + // cy*tan(α)*(sy-1)/sy. + Transform3d T = BeltTransformPipeline::build_forward_transform(pcfg); + Vec3d d(unscale(inst_shift.x()), unscale(inst_shift.y()), 0.); + Vec3d c = T.linear() * d - d; double belt_surface_z = BeltTransformPipeline::has_preslice_remap(pcfg) ? BeltTransformPipeline::remap_bbox(*this->model_object(), pcfg).min.z() : 0.; double shear_min_z = m_belt_min_z - belt_surface_z; - Point phys = inst_shift; - double center_on_axis = (za.from == 0) ? unscale(phys.x()) : unscale(phys.y()); - global_z_offset += center_on_axis * factor + shear_min_z; + global_z_offset += c.z() + shear_min_z; + BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] write m_belt_global_xy_correction tid=" << std::this_thread::get_id() + << " obj=" << this << " old=(" << m_belt_global_xy_correction.x() << "," << m_belt_global_xy_correction.y() + << ") new=(" << c.x() << "," << c.y() << ")"; + m_belt_global_xy_correction = Vec2d(c.x(), c.y()); + BOOST_LOG_TRIVIAL(warning) << "Belt per-axis Z-shear-global: correction=(" + << c.x() << ", " << c.y() << ", " << c.z() << ")" + << " shear_min_z=" << shear_min_z << " (m_belt_min_z=" << m_belt_min_z << ")"; } // Pre-slice remap global mode: when on, the remap accounts for the @@ -983,6 +1016,8 @@ void PrintObject::slice() BOOST_LOG_TRIVIAL(warning) << "Belt global: z_offset=" << global_z_offset << " (relative to min across " << this->print()->objects().size() << " objects)"; + BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] write m_belt_global_z_offset tid=" << std::this_thread::get_id() + << " obj=" << this << " old=" << m_belt_global_z_offset << " new=" << global_z_offset; m_belt_global_z_offset = global_z_offset; if (std::abs(global_z_offset) > EPSILON) { for (Layer *layer : m_layers) @@ -1003,6 +1038,10 @@ void PrintObject::slice() } // BBS + BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] slice EXIT tid=" << std::this_thread::get_id() << " obj=" << this + << " layers=" << m_layers.size() << " belt_min_z=" << m_belt_min_z + << " belt_global_z_offset=" << m_belt_global_z_offset + << " belt_xy=(" << m_belt_global_xy_correction.x() << "," << m_belt_global_xy_correction.y() << ")"; this->set_done(posSlice); } diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index e707386576..92302c583e 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4491,6 +4491,7 @@ void TabPrinter::build_fff() line.append_option(optgroup->get_option("belt_scale_z_angle")); optgroup->append_line(line); } + optgroup->append_single_option_line("belt_mesh_transform_order"); { Line line = { L("Pre-slice axis remap"), L("Remap model axes before slicing so the slicer's coordinate system matches " @@ -4548,6 +4549,63 @@ void TabPrinter::build_fff() line.append_option(optgroup->get_option("belt_support_z_offset_mode")); optgroup->append_line(line); } + + // Machine-frame transforms: applied to G-code after back-transform and + // gcode_remap, before per-axis origin snap. Maps Cartesian G-code into + // the printer's physical machine frame. + { + auto mf = page->new_optgroup(L("Machine frame transforms"), L"param_advanced"); + { + Line line = { L("G-code shear X"), L("Shear applied to the X axis in the machine-frame stage (after back-transform and gcode_remap).") }; + line.append_option(mf->get_option("gcode_shear_x")); + line.append_option(mf->get_option("gcode_shear_x_angle")); + line.append_option(mf->get_option("gcode_shear_x_from")); + mf->append_line(line); + } + { + Line line = { L("G-code shear Y"), L("Shear applied to the Y axis in the machine-frame stage (after back-transform and gcode_remap).") }; + line.append_option(mf->get_option("gcode_shear_y")); + line.append_option(mf->get_option("gcode_shear_y_angle")); + line.append_option(mf->get_option("gcode_shear_y_from")); + mf->append_line(line); + } + { + Line line = { L("G-code shear Z"), L("Shear applied to the Z axis in the machine-frame stage (after back-transform and gcode_remap).") }; + line.append_option(mf->get_option("gcode_shear_z")); + line.append_option(mf->get_option("gcode_shear_z_angle")); + line.append_option(mf->get_option("gcode_shear_z_from")); + mf->append_line(line); + } + { + Line line = { L("G-code scale X"), L("Scale applied to the X axis in the machine-frame stage.") }; + line.append_option(mf->get_option("gcode_scale_x")); + line.append_option(mf->get_option("gcode_scale_x_angle")); + mf->append_line(line); + } + { + Line line = { L("G-code scale Y"), L("Scale applied to the Y axis in the machine-frame stage.") }; + line.append_option(mf->get_option("gcode_scale_y")); + line.append_option(mf->get_option("gcode_scale_y_angle")); + mf->append_line(line); + } + { + Line line = { L("G-code scale Z"), L("Scale applied to the Z axis in the machine-frame stage.") }; + line.append_option(mf->get_option("gcode_scale_z")); + line.append_option(mf->get_option("gcode_scale_z_angle")); + mf->append_line(line); + } + mf->append_single_option_line("belt_gcode_transform_order"); + { + Line line = { L("Post-gcode axis remap"), + L("Axis remap in the machine-frame stage. Applied AFTER gcode_remap, " + "to put coordinates into the printer's physical axis labelling.") }; + line.append_option(mf->get_option("post_gcode_remap_x")); + line.append_option(mf->get_option("post_gcode_remap_y")); + line.append_option(mf->get_option("post_gcode_remap_z")); + mf->append_line(line); + } + } + option = optgroup->get_option("thumbnails"); option.opt.full_width = true; optgroup->append_single_option_line(option, "printer_basic_information_advanced#g-code-thumbnails"); @@ -5395,6 +5453,7 @@ void TabPrinter::toggle_options() toggle_line("belt_printer_infinite_y", is_belt); for (auto el : {"belt_shear_x", "belt_shear_y", "belt_shear_z", "belt_scale_x", "belt_scale_y", "belt_scale_z", + "belt_mesh_transform_order", "belt_origin_snap_x", "belt_origin_snap_y", "belt_origin_snap_z"}) toggle_line(el, is_belt); @@ -5435,6 +5494,34 @@ void TabPrinter::toggle_options() auto scz = m_config->option>("belt_scale_z")->value; toggle_option("belt_scale_z_angle", is_belt && scz != BeltScaleMode::None); + // Machine-frame transforms: shown only in belt mode. + for (auto el : {"gcode_shear_x", "gcode_shear_y", "gcode_shear_z", + "gcode_scale_x", "gcode_scale_y", "gcode_scale_z", + "belt_gcode_transform_order", + "post_gcode_remap_x"}) + toggle_line(el, is_belt); + + auto gsx = m_config->option>("gcode_shear_x")->value; + toggle_option("gcode_shear_x_angle", is_belt && gsx != BeltShearMode::None); + toggle_option("gcode_shear_x_from", is_belt && gsx != BeltShearMode::None); + + auto gsy = m_config->option>("gcode_shear_y")->value; + toggle_option("gcode_shear_y_angle", is_belt && gsy != BeltShearMode::None); + toggle_option("gcode_shear_y_from", is_belt && gsy != BeltShearMode::None); + + auto gsz = m_config->option>("gcode_shear_z")->value; + toggle_option("gcode_shear_z_angle", is_belt && gsz != BeltShearMode::None); + toggle_option("gcode_shear_z_from", is_belt && gsz != BeltShearMode::None); + + auto gscx = m_config->option>("gcode_scale_x")->value; + toggle_option("gcode_scale_x_angle", is_belt && gscx != BeltScaleMode::None); + + auto gscy = m_config->option>("gcode_scale_y")->value; + toggle_option("gcode_scale_y_angle", is_belt && gscy != BeltScaleMode::None); + + auto gscz = m_config->option>("gcode_scale_z")->value; + toggle_option("gcode_scale_z_angle", is_belt && gscz != BeltScaleMode::None); + // Origin snap is superseded by belt_preslice_global toggle_option("belt_origin_offset_x", is_belt && m_config->opt_bool("belt_origin_snap_x") && !belt_global); toggle_option("belt_origin_offset_y", is_belt && m_config->opt_bool("belt_origin_snap_y") && !belt_global); From c79970bedbcc542f448aeb8f29bf78d314f09cf3 Mon Sep 17 00:00:00 2001 From: Joseph Robertson <32232925+HarrierPigeon@users.noreply.github.com> Date: Tue, 19 May 2026 00:56:08 -0500 Subject: [PATCH 027/224] Clean Up Settings Interface, Update Generic Profile (#22) * clean up UI elements * further cleaning * final cleanup for first round of settings UI streamlining * update generic belt printer settings * fix generic again --- resources/profiles/Custom.json | 2 +- .../Custom/machine/fdm_belt_common.json | 10 +- src/libslic3r/PrintConfig.cpp | 126 +++++------ src/slic3r/GUI/Tab.cpp | 198 ++++++++++-------- 4 files changed, 181 insertions(+), 155 deletions(-) diff --git a/resources/profiles/Custom.json b/resources/profiles/Custom.json index 2119514211..fa683db8d6 100644 --- a/resources/profiles/Custom.json +++ b/resources/profiles/Custom.json @@ -1,6 +1,6 @@ { "name": "Custom Printer", - "version": "02.03.02.62", + "version": "02.03.02.65", "force_update": "0", "description": "My configurations", "machine_model_list": [ diff --git a/resources/profiles/Custom/machine/fdm_belt_common.json b/resources/profiles/Custom/machine/fdm_belt_common.json index 1ca314da0f..039c4745e0 100644 --- a/resources/profiles/Custom/machine/fdm_belt_common.json +++ b/resources/profiles/Custom/machine/fdm_belt_common.json @@ -84,15 +84,15 @@ ], "preslice_remap_y": "pos_z", "preslice_remap_z": "pos_y", - "preslice_remap_global": "1", "printer_extruder_id": [ "1" ], - "belt_origin_snap_y": "1", "belt_printer": "1", - "belt_preslice_global": "1", - "belt_shear_z": "pos_cot", - "belt_shear_z_angle": "30", + "belt_shear_z": "pos_tan", + "belt_shear_z_angle": "45", + "belt_scale_y": "inv_cos", + "gcode_shear_z": "pos_tan", + "gcode_scale_y": "inv_cos", "build_plate_tilt_x": "45", "purge_in_prime_tower": "0", "scan_first_layer": "0", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 665b11dcd7..e83edf5a8d 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -6305,7 +6305,7 @@ void PrintConfigDef::init_fff_params() def->sidetext = u8"\u00B0"; def->min = -90; def->max = 90; - def->mode = comAdvanced; + def->mode = comExpert; def->set_default_value(new ConfigOptionFloat(0.)); def = this->add("build_plate_tilt_y", coFloat); @@ -6317,11 +6317,11 @@ void PrintConfigDef::init_fff_params() def->sidetext = u8"\u00B0"; def->min = -90; def->max = 90; - def->mode = comAdvanced; + def->mode = comExpert; def->set_default_value(new ConfigOptionFloat(0.)); def = this->add("belt_printer", coBool); - def->label = L("Belt printer"); + def->label = L("Enable belt printing"); def->category = L("Printable space"); def->tooltip = L("Enable belt printer mode. Belt printers use a conveyor belt as the build surface, " "tilted at an angle (typically 45 degrees). The slicer will rotate the slicing plane " @@ -6351,7 +6351,8 @@ void PrintConfigDef::init_fff_params() def->set_default_value(new ConfigOptionBool(true)); // Per-axis shear controls for belt printer - auto add_belt_shear_mode = [this](const char *key, const char *label, BeltShearMode default_mode) { + auto add_belt_shear_mode = [this](const char *key, const char *label, BeltShearMode default_mode, + ConfigOptionMode mode = comAdvanced) { auto def = this->add(key, coEnum); def->label = L(label); def->category = L("Printable space"); @@ -6359,10 +6360,11 @@ void PrintConfigDef::init_fff_params() def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); def->enum_values = {"none", "pos_cot", "neg_cot", "pos_tan", "neg_tan"}; def->enum_labels = {L("None"), L("+cot(α)"), L("-cot(α)"), L("+tan(α)"), L("-tan(α)")}; - def->mode = comAdvanced; + def->mode = mode; def->set_default_value(new ConfigOptionEnum(default_mode)); }; - auto add_belt_shear_angle = [this](const char *key, const char *label) { + auto add_belt_shear_angle = [this](const char *key, const char *label, + ConfigOptionMode mode = comAdvanced) { auto def = this->add(key, coFloat); def->label = L(label); def->category = L("Printable space"); @@ -6370,10 +6372,11 @@ void PrintConfigDef::init_fff_params() def->sidetext = L("°"); def->min = 0.1; def->max = 89.9; - def->mode = comAdvanced; + def->mode = mode; def->set_default_value(new ConfigOptionFloat(45)); }; - auto add_belt_axis_enum = [this](const char *key, const char *label, const char *tooltip, BeltAxis default_axis) { + auto add_belt_axis_enum = [this](const char *key, const char *label, const char *tooltip, + BeltAxis default_axis, ConfigOptionMode mode = comAdvanced) { auto def = this->add(key, coEnum); def->label = L(label); def->category = L("Printable space"); @@ -6381,7 +6384,7 @@ void PrintConfigDef::init_fff_params() def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); def->enum_values = {"x", "y", "z"}; def->enum_labels = {L("X"), L("Y"), L("Z")}; - def->mode = comAdvanced; + def->mode = mode; def->set_default_value(new ConfigOptionEnum(default_axis)); }; @@ -6390,27 +6393,28 @@ void PrintConfigDef::init_fff_params() def->label = L(label); def->category = L("Printable space"); def->tooltip = L("Apply shear in global coordinates (position-aware) rather than object-local coordinates."); - def->mode = comAdvanced; + def->mode = comExpert; def->set_default_value(new ConfigOptionBool(default_val)); }; - add_belt_shear_mode("belt_shear_x", "Function", BeltShearMode::None); - add_belt_shear_angle("belt_shear_x_angle", "Angle"); - add_belt_axis_enum("belt_shear_x_from", "From", "Source axis for X shear.", BeltAxis::Z); + add_belt_shear_mode ("belt_shear_x", "Function", BeltShearMode::None, comExpert); + add_belt_shear_angle("belt_shear_x_angle", "Angle", comExpert); + add_belt_axis_enum ("belt_shear_x_from", "From", "Source axis for X shear.", BeltAxis::Z, comExpert); add_belt_shear_global("belt_shear_x_global", "Global"); - add_belt_shear_mode("belt_shear_y", "Function", BeltShearMode::None); - add_belt_shear_angle("belt_shear_y_angle", "Angle"); - add_belt_axis_enum("belt_shear_y_from", "From", "Source axis for Y shear.", BeltAxis::Z); + add_belt_shear_mode ("belt_shear_y", "Function", BeltShearMode::None, comExpert); + add_belt_shear_angle("belt_shear_y_angle", "Angle", comExpert); + add_belt_axis_enum ("belt_shear_y_from", "From", "Source axis for Y shear.", BeltAxis::Z, comExpert); add_belt_shear_global("belt_shear_y_global", "Global"); - add_belt_shear_mode("belt_shear_z", "Function", BeltShearMode::PosTan); + add_belt_shear_mode ("belt_shear_z", "Function", BeltShearMode::PosTan); add_belt_shear_angle("belt_shear_z_angle", "Angle"); - add_belt_axis_enum("belt_shear_z_from", "From", "Source axis for Z shear.", BeltAxis::Y); + add_belt_axis_enum ("belt_shear_z_from", "From", "Source axis for Z shear.", BeltAxis::Y); add_belt_shear_global("belt_shear_z_global", "Global", true); // Per-axis scale controls for belt printer - auto add_belt_scale_mode = [this](const char *key, const char *label, BeltScaleMode default_mode) { + auto add_belt_scale_mode = [this](const char *key, const char *label, BeltScaleMode default_mode, + ConfigOptionMode mode = comAdvanced) { auto def = this->add(key, coEnum); def->label = L(label); def->category = L("Printable space"); @@ -6418,10 +6422,11 @@ void PrintConfigDef::init_fff_params() def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); def->enum_values = {"none", "inv_sin", "inv_cos", "sin", "cos"}; def->enum_labels = {L("None"), L("1/sin(α)"), L("1/cos(α)"), L("sin(α)"), L("cos(α)")}; - def->mode = comAdvanced; + def->mode = mode; def->set_default_value(new ConfigOptionEnum(default_mode)); }; - auto add_belt_scale_angle = [this](const char *key, const char *label) { + auto add_belt_scale_angle = [this](const char *key, const char *label, + ConfigOptionMode mode = comAdvanced) { auto def = this->add(key, coFloat); def->label = L(label); def->category = L("Printable space"); @@ -6429,18 +6434,18 @@ void PrintConfigDef::init_fff_params() def->sidetext = L("°"); def->min = 0.1; def->max = 89.9; - def->mode = comAdvanced; + def->mode = mode; def->set_default_value(new ConfigOptionFloat(45)); }; - add_belt_scale_mode("belt_scale_x", "Function", BeltScaleMode::None); - add_belt_scale_angle("belt_scale_x_angle", "Angle"); + add_belt_scale_mode ("belt_scale_x", "Function", BeltScaleMode::None, comExpert); + add_belt_scale_angle("belt_scale_x_angle", "Angle", comExpert); - add_belt_scale_mode("belt_scale_y", "Function", BeltScaleMode::None); + add_belt_scale_mode ("belt_scale_y", "Function", BeltScaleMode::None); add_belt_scale_angle("belt_scale_y_angle", "Angle"); - add_belt_scale_mode("belt_scale_z", "Function", BeltScaleMode::None); - add_belt_scale_angle("belt_scale_z_angle", "Angle"); + add_belt_scale_mode ("belt_scale_z", "Function", BeltScaleMode::None, comExpert); + add_belt_scale_angle("belt_scale_z_angle", "Angle", comExpert); auto add_belt_transform_order = [this](const char *key, const char *label, const char *tooltip) { auto def = this->add(key, coEnum); @@ -6450,8 +6455,8 @@ void PrintConfigDef::init_fff_params() def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); def->enum_values = {"scale_then_shear", "shear_then_scale"}; def->enum_labels = {L("Scale, then shear"), L("Shear, then scale")}; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionEnum(BeltTransformOrder::ScaleThenShear)); + def->mode = comExpert; + def->set_default_value(new ConfigOptionEnum(BeltTransformOrder::ShearThenScale)); }; add_belt_transform_order("belt_mesh_transform_order", "Mesh transform order", @@ -6461,7 +6466,8 @@ void PrintConfigDef::init_fff_params() "follows the same order so that it correctly inverts the mesh transform."); // G-code axis remap with sign - auto add_belt_remap = [this](const char *key, const char *label, const char *tooltip, RemapAxis default_axis) { + 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); def->label = L(label); def->category = L("Printable space"); @@ -6469,7 +6475,7 @@ void PrintConfigDef::init_fff_params() def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); def->enum_values = {"pos_x", "pos_y", "pos_z", "neg_x", "neg_y", "neg_z", "rev_x", "rev_y", "rev_z"}; def->enum_labels = {L("+X"), L("+Y"), L("+Z"), L("-X"), L("-Y"), L("-Z"), L("Rev X"), L("Rev Y"), L("Rev Z")}; - def->mode = comSimple; // Visibility controlled by toggle_line in Tab.cpp + def->mode = mode; // Visibility may also be gated by toggle_line in Tab.cpp def->set_default_value(new ConfigOptionEnum(default_axis)); }; @@ -6479,19 +6485,19 @@ void PrintConfigDef::init_fff_params() "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); + RemapAxis::PosX, comExpert); 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); + RemapAxis::PosY, comExpert); 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); + RemapAxis::PosZ, comExpert); def = this->add("preslice_remap_global", coBool); def->label = L("Global"); @@ -6500,35 +6506,35 @@ void PrintConfigDef::init_fff_params() "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 per-axis 'Global' option on belt mesh shears, but for the remap."); - def->mode = comAdvanced; + def->mode = comExpert; 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); - add_belt_remap("gcode_remap_y", "Y", "Which slicing axis maps to machine Y in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosY); - add_belt_remap("gcode_remap_z", "Z", "Which slicing axis maps to machine Z in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosZ); + 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, comExpert); + 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, comExpert); + 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, comExpert); // Machine-frame G-code transforms: applied AFTER back-transform and gcode_remap, // before per-axis origin snap. Maps Cartesian G-code to the printer's physical machine frame. - add_belt_shear_mode ("gcode_shear_x", "Function", BeltShearMode::None); - add_belt_shear_angle("gcode_shear_x_angle", "Angle"); - add_belt_axis_enum ("gcode_shear_x_from", "From", "Source axis for X shear in the machine-frame stage.", BeltAxis::Z); + add_belt_shear_mode ("gcode_shear_x", "Function", BeltShearMode::None, comExpert); + add_belt_shear_angle("gcode_shear_x_angle", "Angle", comExpert); + add_belt_axis_enum ("gcode_shear_x_from", "From", "Source axis for X shear in the machine-frame stage.", BeltAxis::Z, comExpert); - add_belt_shear_mode ("gcode_shear_y", "Function", BeltShearMode::None); - add_belt_shear_angle("gcode_shear_y_angle", "Angle"); - add_belt_axis_enum ("gcode_shear_y_from", "From", "Source axis for Y shear in the machine-frame stage.", BeltAxis::Z); + add_belt_shear_mode ("gcode_shear_y", "Function", BeltShearMode::None, comExpert); + add_belt_shear_angle("gcode_shear_y_angle", "Angle", comExpert); + add_belt_axis_enum ("gcode_shear_y_from", "From", "Source axis for Y shear in the machine-frame stage.", BeltAxis::Z, comExpert); add_belt_shear_mode ("gcode_shear_z", "Function", BeltShearMode::None); add_belt_shear_angle("gcode_shear_z_angle", "Angle"); add_belt_axis_enum ("gcode_shear_z_from", "From", "Source axis for Z shear in the machine-frame stage.", BeltAxis::Y); - add_belt_scale_mode ("gcode_scale_x", "Function", BeltScaleMode::None); - add_belt_scale_angle("gcode_scale_x_angle", "Angle"); + add_belt_scale_mode ("gcode_scale_x", "Function", BeltScaleMode::None, comExpert); + add_belt_scale_angle("gcode_scale_x_angle", "Angle", comExpert); add_belt_scale_mode ("gcode_scale_y", "Function", BeltScaleMode::None); add_belt_scale_angle("gcode_scale_y_angle", "Angle"); - add_belt_scale_mode ("gcode_scale_z", "Function", BeltScaleMode::None); - add_belt_scale_angle("gcode_scale_z_angle", "Angle"); + add_belt_scale_mode ("gcode_scale_z", "Function", BeltScaleMode::None, comExpert); + add_belt_scale_angle("gcode_scale_z_angle", "Angle", comExpert); add_belt_transform_order("belt_gcode_transform_order", "G-code transform order", "Order in which the machine-frame shear and scale matrices are composed when " @@ -6554,8 +6560,8 @@ void PrintConfigDef::init_fff_params() def->tooltip = L("Reverse the shear/scale transform applied during slicing so G-code " "coordinates are in the machine's physical coordinate space. " "Requires at least one shear axis with global mode enabled."); - def->mode = comSimple; // Visibility controlled by toggle_line in Tab.cpp - def->set_default_value(new ConfigOptionBool(false)); + def->mode = comExpert; + def->set_default_value(new ConfigOptionBool(true)); def = this->add("belt_preslice_global", coBool); def->label = L("Global mesh transforms"); @@ -6563,8 +6569,8 @@ void PrintConfigDef::init_fff_params() 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 = comAdvanced; - def->set_default_value(new ConfigOptionBool(false)); + 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 @@ -6583,11 +6589,11 @@ void PrintConfigDef::init_fff_params() def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); def->enum_values = {"auto", "xy", "yz", "xz", "belt_shear"}; def->enum_labels = {L("Auto"), L("XY (machine bed)"), L("YZ"), L("XZ"), L("Belt shear plane")}; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionEnum(FirstLayerPlaneMode::Auto)); + def->mode = comExpert; + def->set_default_value(new ConfigOptionEnum(FirstLayerPlaneMode::BeltShear)); def = this->add("first_layer_plane_offset", coFloat); - def->label = L("Plane offset"); + 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 " @@ -6619,7 +6625,7 @@ void PrintConfigDef::init_fff_params() std::string tip = std::string("Shift G-code output so the object's bounding box minimum on machine ") + axis_label + " equals the offset value."; def->tooltip = L(tip); - def->mode = comAdvanced; + def->mode = comExpert; def->set_default_value(new ConfigOptionBool(false)); def = this->add(key_offset, coFloat); @@ -6629,7 +6635,7 @@ void PrintConfigDef::init_fff_params() def->sidetext = L("mm"); def->min = -10000; def->max = 10000; - def->mode = comAdvanced; + def->mode = comExpert; def->set_default_value(new ConfigOptionFloat(0)); }; add_belt_origin_snap("belt_origin_snap_x", "belt_origin_offset_x", "X"); @@ -6638,7 +6644,7 @@ void PrintConfigDef::init_fff_params() // Belt support floor debug controls def = this->add("belt_support_floor_offset", coFloat); - def->label = L("Floor Z offset"); + def->label = L("Support Floor Z offset"); def->category = L("Printable space"); def->tooltip = L("Shifts the computed belt floor up or down (mm). Negative values lower the floor, allowing more supports to survive. Use this to diagnose belt floor formula issues."); def->sidetext = L("mm"); @@ -6656,7 +6662,7 @@ void PrintConfigDef::init_fff_params() def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); def->enum_values = {"none", "generator_only"}; def->enum_labels = {L("None"), L("Generator only")}; - def->mode = comAdvanced; + def->mode = comDevelop; def->set_default_value(new ConfigOptionEnum(BeltSupportFloorMode::GeneratorOnly)); } @@ -6669,7 +6675,7 @@ void PrintConfigDef::init_fff_params() def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); def->enum_values = {"none", "unconditional", "raft_only"}; def->enum_labels = {L("None"), L("Unconditional"), L("Raft only")}; - def->mode = comAdvanced; + def->mode = comExpert; def->set_default_value(new ConfigOptionEnum(BeltSupportZOffsetMode::Unconditional)); } diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index e9356144ad..b0ee2c9435 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1268,6 +1268,13 @@ void Tab::update_mode() { m_mode = wxGetApp().get_mode(); + // toggle_options reads m_mode to gate Lines whose contents are mode-mixed + // (e.g., a multi-option row where some options are Advanced and some Expert): + // when all of a Line's options would be hidden, we hide the Line itself. + // Without refreshing here, the toggle_visible state stays stale across mode + // switches and Lines stay hidden. + toggle_options(); + update_visibility(); update_changed_tree_ui(); @@ -4448,75 +4455,77 @@ void TabPrinter::build_fff() //option.opt.full_width = true; //optgroup->append_single_option_line(option); optgroup->append_single_option_line("disable_m73", "printer_basic_information_advanced#disable-set-remaining-print-time"); - optgroup->append_single_option_line("build_plate_tilt_x"); - optgroup->append_single_option_line("build_plate_tilt_y"); - optgroup->append_single_option_line("belt_printer"); - optgroup->append_single_option_line("belt_printer_angle"); - optgroup->append_single_option_line("belt_printer_infinite_y"); + + // Belt printer: dedicated section. Everything except the "Enable belt printing" + // checkbox is hidden when belt_printer is off (see TabPrinter::toggle_options). + auto belt_og = page->new_optgroup(L("Belt printer"), L"param_advanced"); + belt_og->append_single_option_line("belt_printer"); + belt_og->append_single_option_line("belt_printer_angle"); + belt_og->append_single_option_line("belt_printer_infinite_y"); // Per-axis shear: group mode + angle + source on one row per axis { Line line = { L("Mesh shear X"), L("Shear applied to the X axis before slicing") }; - line.append_option(optgroup->get_option("belt_shear_x")); - line.append_option(optgroup->get_option("belt_shear_x_angle")); - line.append_option(optgroup->get_option("belt_shear_x_from")); - line.append_option(optgroup->get_option("belt_shear_x_global")); - optgroup->append_line(line); + line.append_option(belt_og->get_option("belt_shear_x")); + line.append_option(belt_og->get_option("belt_shear_x_angle")); + line.append_option(belt_og->get_option("belt_shear_x_from")); + line.append_option(belt_og->get_option("belt_shear_x_global")); + belt_og->append_line(line); } { Line line = { L("Mesh shear Y"), L("Shear applied to the Y axis before slicing") }; - line.append_option(optgroup->get_option("belt_shear_y")); - line.append_option(optgroup->get_option("belt_shear_y_angle")); - line.append_option(optgroup->get_option("belt_shear_y_from")); - line.append_option(optgroup->get_option("belt_shear_y_global")); - optgroup->append_line(line); + line.append_option(belt_og->get_option("belt_shear_y")); + line.append_option(belt_og->get_option("belt_shear_y_angle")); + line.append_option(belt_og->get_option("belt_shear_y_from")); + line.append_option(belt_og->get_option("belt_shear_y_global")); + belt_og->append_line(line); } { Line line = { L("Mesh shear Z"), L("Shear applied to the Z axis before slicing") }; - line.append_option(optgroup->get_option("belt_shear_z")); - line.append_option(optgroup->get_option("belt_shear_z_angle")); - line.append_option(optgroup->get_option("belt_shear_z_from")); - line.append_option(optgroup->get_option("belt_shear_z_global")); - optgroup->append_line(line); + line.append_option(belt_og->get_option("belt_shear_z")); + line.append_option(belt_og->get_option("belt_shear_z_angle")); + line.append_option(belt_og->get_option("belt_shear_z_from")); + line.append_option(belt_og->get_option("belt_shear_z_global")); + belt_og->append_line(line); } { Line line = { L("Mesh scale X"), L("Scale applied to the X axis before slicing") }; - line.append_option(optgroup->get_option("belt_scale_x")); - line.append_option(optgroup->get_option("belt_scale_x_angle")); - optgroup->append_line(line); + line.append_option(belt_og->get_option("belt_scale_x")); + line.append_option(belt_og->get_option("belt_scale_x_angle")); + belt_og->append_line(line); } { Line line = { L("Mesh scale Y"), L("Scale applied to the Y axis before slicing") }; - line.append_option(optgroup->get_option("belt_scale_y")); - line.append_option(optgroup->get_option("belt_scale_y_angle")); - optgroup->append_line(line); + line.append_option(belt_og->get_option("belt_scale_y")); + line.append_option(belt_og->get_option("belt_scale_y_angle")); + belt_og->append_line(line); } { Line line = { L("Mesh scale Z"), L("Scale applied to the Z axis before slicing") }; - line.append_option(optgroup->get_option("belt_scale_z")); - line.append_option(optgroup->get_option("belt_scale_z_angle")); - optgroup->append_line(line); + line.append_option(belt_og->get_option("belt_scale_z")); + line.append_option(belt_og->get_option("belt_scale_z_angle")); + belt_og->append_line(line); } - optgroup->append_single_option_line("belt_mesh_transform_order"); + belt_og->append_single_option_line("belt_mesh_transform_order"); { 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.append_option(optgroup->get_option("preslice_remap_x")); - line.append_option(optgroup->get_option("preslice_remap_y")); - line.append_option(optgroup->get_option("preslice_remap_z")); - line.append_option(optgroup->get_option("preslice_remap_global")); - optgroup->append_line(line); + 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); } { Line line = { L("G-code axis remap (post-slice)"), L("Remap slicing-frame axes to machine axes in G-code output. Applied AFTER slicing, during G-code generation.") }; - line.append_option(optgroup->get_option("gcode_remap_x")); - line.append_option(optgroup->get_option("gcode_remap_y")); - line.append_option(optgroup->get_option("gcode_remap_z")); - optgroup->append_line(line); + line.append_option(belt_og->get_option("gcode_remap_x")); + line.append_option(belt_og->get_option("gcode_remap_y")); + line.append_option(belt_og->get_option("gcode_remap_z")); + belt_og->append_line(line); } - optgroup->append_single_option_line("belt_preslice_global"); - optgroup->append_single_option_line("gcode_back_transform"); + belt_og->append_single_option_line("belt_preslice_global"); + belt_og->append_single_option_line("gcode_back_transform"); { Line line = { L("First layer plane"), L("Reference plane used to decide which extrusions get first-layer " @@ -4524,42 +4533,49 @@ void TabPrinter::build_fff() "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.append_option(optgroup->get_option("first_layer_plane")); - line.append_option(optgroup->get_option("first_layer_plane_offset")); - line.append_option(optgroup->get_option("first_layer_plane_thickness")); - optgroup->append_line(line); + 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); } { Line line = { L("Origin snap X"), L("Snap object bbox min X to offset in G-code output") }; - line.append_option(optgroup->get_option("belt_origin_snap_x")); - line.append_option(optgroup->get_option("belt_origin_offset_x")); - optgroup->append_line(line); + line.append_option(belt_og->get_option("belt_origin_snap_x")); + line.append_option(belt_og->get_option("belt_origin_offset_x")); + belt_og->append_line(line); } { Line line = { L("Origin snap Y"), L("Snap object bbox min Y to offset in G-code output") }; - line.append_option(optgroup->get_option("belt_origin_snap_y")); - line.append_option(optgroup->get_option("belt_origin_offset_y")); - optgroup->append_line(line); + line.append_option(belt_og->get_option("belt_origin_snap_y")); + line.append_option(belt_og->get_option("belt_origin_offset_y")); + belt_og->append_line(line); } { Line line = { L("Origin snap Z"), L("Snap object bbox min Z to offset in G-code output") }; - line.append_option(optgroup->get_option("belt_origin_snap_z")); - line.append_option(optgroup->get_option("belt_origin_offset_z")); - optgroup->append_line(line); - } - { - Line line = { L("Support floor"), L("Belt floor awareness for support generation and clipping") }; - line.append_option(optgroup->get_option("belt_support_floor_mode")); - line.append_option(optgroup->get_option("belt_support_floor_offset")); - line.append_option(optgroup->get_option("belt_support_z_offset_mode")); - optgroup->append_line(line); + line.append_option(belt_og->get_option("belt_origin_snap_z")); + line.append_option(belt_og->get_option("belt_origin_offset_z")); + 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). + belt_og->append_single_option_line("belt_support_floor_offset"); + belt_og->append_single_option_line("belt_support_z_offset_mode"); + belt_og->append_single_option_line("belt_support_floor_mode"); // Machine-frame transforms: applied to G-code after back-transform and // gcode_remap, before per-axis origin snap. Maps Cartesian G-code into // the printer's physical machine frame. { auto mf = page->new_optgroup(L("Machine frame transforms"), L"param_advanced"); + { + Line line = { L("Post-gcode axis remap"), + L("Axis remap in the machine-frame stage. Applied AFTER gcode_remap, " + "to put coordinates into the printer's physical axis labelling.") }; + line.append_option(mf->get_option("post_gcode_remap_x")); + line.append_option(mf->get_option("post_gcode_remap_y")); + line.append_option(mf->get_option("post_gcode_remap_z")); + mf->append_line(line); + } { Line line = { L("G-code shear X"), L("Shear applied to the X axis in the machine-frame stage (after back-transform and gcode_remap).") }; line.append_option(mf->get_option("gcode_shear_x")); @@ -4600,15 +4616,6 @@ void TabPrinter::build_fff() mf->append_line(line); } mf->append_single_option_line("belt_gcode_transform_order"); - { - Line line = { L("Post-gcode axis remap"), - L("Axis remap in the machine-frame stage. Applied AFTER gcode_remap, " - "to put coordinates into the printer's physical axis labelling.") }; - line.append_option(mf->get_option("post_gcode_remap_x")); - line.append_option(mf->get_option("post_gcode_remap_y")); - line.append_option(mf->get_option("post_gcode_remap_z")); - mf->append_line(line); - } } option = optgroup->get_option("thumbnails"); @@ -4655,6 +4662,8 @@ void TabPrinter::build_fff() optgroup->append_single_option_line("use_firmware_retraction", "printer_basic_information_advanced#use-firmware-retraction"); // optgroup->append_single_option_line("spaghetti_detector"); optgroup->append_single_option_line("time_cost", "printer_basic_information_advanced#time-cost"); + optgroup->append_single_option_line("build_plate_tilt_x"); + optgroup->append_single_option_line("build_plate_tilt_y"); optgroup = page->new_optgroup(L("Cooling Fan"), "param_cooling_fan"); Line line = Line{ L("Fan speed-up time"), optgroup->get_option("fan_speedup_time").opt.tooltip }; @@ -5580,24 +5589,33 @@ void TabPrinter::toggle_options() // Belt printer: show belt-specific settings only when belt_printer is enabled. bool is_belt = m_config->opt_bool("belt_printer"); + bool expert_or_above = (m_mode >= comExpert); toggle_line("belt_printer_angle", is_belt); toggle_line("belt_printer_infinite_y", is_belt); - for (auto el : {"belt_shear_x", "belt_shear_y", "belt_shear_z", - "belt_scale_x", "belt_scale_y", "belt_scale_z", - "belt_mesh_transform_order", + // Mesh shear/scale: only shear Z and scale Y are shown in Advanced; the others + // are Expert-only. Each Line packs all its options on one row, so toggle the + // Line here in addition to the per-option mode gating. + toggle_line("belt_shear_x", is_belt && expert_or_above); + toggle_line("belt_shear_y", is_belt && expert_or_above); + toggle_line("belt_shear_z", is_belt); + toggle_line("belt_scale_x", is_belt && expert_or_above); + toggle_line("belt_scale_y", is_belt); + toggle_line("belt_scale_z", is_belt && expert_or_above); + for (auto el : {"belt_mesh_transform_order", "belt_origin_snap_x", "belt_origin_snap_y", "belt_origin_snap_z"}) toggle_line(el, is_belt); - // Remap options: visible when belt mode is on OR when UI is in developer mode. - bool show_remap = is_belt || (m_mode >= comDevelop); + // Remap, back-transform, and global mesh-transforms toggles are gated by belt + // mode here; finer mode-based visibility (Advanced vs Expert) is handled by + // each option's ConfigOptionMode in PrintConfig.cpp. for (auto el : {"preslice_remap_x", "gcode_remap_x", "gcode_back_transform"}) - toggle_line(el, show_remap); - toggle_line("belt_preslice_global", show_remap); + 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", show_remap && !belt_global); + toggle_option("preslice_remap_global", is_belt && !belt_global); // Gray out angle/from sub-options when their parent shear/scale mode is None. // Per-axis globals are superseded when belt_preslice_global is on. @@ -5626,10 +5644,14 @@ void TabPrinter::toggle_options() toggle_option("belt_scale_z_angle", is_belt && scz != BeltScaleMode::None); // Machine-frame transforms: shown only in belt mode. - for (auto el : {"gcode_shear_x", "gcode_shear_y", "gcode_shear_z", - "gcode_scale_x", "gcode_scale_y", "gcode_scale_z", - "belt_gcode_transform_order", - "post_gcode_remap_x"}) + // Mirror the Advanced/Expert split used for mesh shear/scale. + toggle_line("gcode_shear_x", is_belt && expert_or_above); + toggle_line("gcode_shear_y", is_belt && expert_or_above); + toggle_line("gcode_shear_z", is_belt); + toggle_line("gcode_scale_x", is_belt && expert_or_above); + toggle_line("gcode_scale_y", is_belt); + toggle_line("gcode_scale_z", is_belt && expert_or_above); + for (auto el : {"belt_gcode_transform_order", "post_gcode_remap_x"}) toggle_line(el, is_belt); auto gsx = m_config->option>("gcode_shear_x")->value; @@ -5659,14 +5681,12 @@ void TabPrinter::toggle_options() toggle_option("belt_origin_offset_z", is_belt && m_config->opt_bool("belt_origin_snap_z") && !belt_global); // First-layer plane: visible alongside the rest of belt-printer settings. - // The Auto default keeps legacy XY behavior on non-belt printers, so it's - // safe to also show it in the developer mode for non-belt printers. - bool show_first_layer_plane = is_belt || (m_mode >= comDevelop); - toggle_line("first_layer_plane", show_first_layer_plane); - toggle_option("first_layer_plane_offset", show_first_layer_plane); - toggle_option("first_layer_plane_thickness", show_first_layer_plane); + toggle_line("first_layer_plane", is_belt); + toggle_option("first_layer_plane_offset", is_belt); + toggle_option("first_layer_plane_thickness", is_belt); - toggle_line("belt_support_floor_mode", is_belt); + for (auto el : {"belt_support_floor_mode", "belt_support_floor_offset", "belt_support_z_offset_mode"}) + toggle_line(el, is_belt); } From da8b11b8ab9351698022e48fbf6a26bcc39b3386 Mon Sep 17 00:00:00 2001 From: Joseph Robertson <32232925+HarrierPigeon@users.noreply.github.com> Date: Tue, 19 May 2026 01:06:39 -0500 Subject: [PATCH 028/224] HOTFIX: update generic belt printer profile (#23) oops --- resources/profiles/Custom.json | 2 +- resources/profiles/Custom/machine/fdm_belt_common.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/profiles/Custom.json b/resources/profiles/Custom.json index fa683db8d6..85e04e525c 100644 --- a/resources/profiles/Custom.json +++ b/resources/profiles/Custom.json @@ -1,6 +1,6 @@ { "name": "Custom Printer", - "version": "02.03.02.65", + "version": "02.03.02.66", "force_update": "0", "description": "My configurations", "machine_model_list": [ diff --git a/resources/profiles/Custom/machine/fdm_belt_common.json b/resources/profiles/Custom/machine/fdm_belt_common.json index 039c4745e0..1562507631 100644 --- a/resources/profiles/Custom/machine/fdm_belt_common.json +++ b/resources/profiles/Custom/machine/fdm_belt_common.json @@ -82,8 +82,8 @@ "z_hop_types": [ "Normal Lift" ], - "preslice_remap_y": "pos_z", - "preslice_remap_z": "pos_y", + "post_gcode_remap_y": "pos_z", + "post_gcode_remap_z": "pos_y", "printer_extruder_id": [ "1" ], From f87a46ec6ec7beb6e9b699fb24a23177c25f7430 Mon Sep 17 00:00:00 2001 From: Joseph Robertson <32232925+HarrierPigeon@users.noreply.github.com> Date: Tue, 19 May 2026 22:54:50 -0500 Subject: [PATCH 029/224] fix X mirroring (#26) Thanks to @hotcubcar for catching this! --- resources/profiles/Custom.json | 2 +- resources/profiles/Custom/machine/fdm_belt_common.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/profiles/Custom.json b/resources/profiles/Custom.json index 85e04e525c..0f54d2f4dd 100644 --- a/resources/profiles/Custom.json +++ b/resources/profiles/Custom.json @@ -1,6 +1,6 @@ { "name": "Custom Printer", - "version": "02.03.02.66", + "version": "02.03.02.67", "force_update": "0", "description": "My configurations", "machine_model_list": [ diff --git a/resources/profiles/Custom/machine/fdm_belt_common.json b/resources/profiles/Custom/machine/fdm_belt_common.json index 1562507631..ce39146042 100644 --- a/resources/profiles/Custom/machine/fdm_belt_common.json +++ b/resources/profiles/Custom/machine/fdm_belt_common.json @@ -82,6 +82,7 @@ "z_hop_types": [ "Normal Lift" ], + "post_gcode_remap_x": "rev_x", "post_gcode_remap_y": "pos_z", "post_gcode_remap_z": "pos_y", "printer_extruder_id": [ From 218881c6f665ce2447aa5c648e2d51d6f32b4250 Mon Sep 17 00:00:00 2001 From: Joseph Robertson <32232925+HarrierPigeon@users.noreply.github.com> Date: Wed, 20 May 2026 02:46:41 -0500 Subject: [PATCH 030/224] fix assembly bounding box truncation problems noticed by hotcubcar (#28) --- src/libslic3r/PrintObjectSlice.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 4454ce87ea..07ba2c63af 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -322,20 +322,31 @@ static std::vector> slices_to_regions( } } else { zs_complex.reserve(zs.size()); + // region.bbox is computed in pre-belt-shear slicer space (see PrintApply.cpp::trafo_for_bbox). + // When belt transforms are active, layer Z values are in post-shear/scale/remap space, + // so the Z components of region.bbox aren't comparable to z. Skipping the Z filter here + // 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_shear(print_config) + || BeltTransformPipeline::has_scale(print_config) + || BeltTransformPipeline::has_preslice_remap(print_config))); for (; z_idx < zs.size() && zs[z_idx] < layer_range.layer_height_range.second; ++ z_idx) { float z = zs[z_idx]; int idx_first_printable_region = -1; bool complex = false; for (int idx_region = 0; idx_region < int(layer_range.volume_regions.size()); ++ idx_region) { const PrintObjectRegions::VolumeRegion ®ion = layer_range.volume_regions[idx_region]; - if (region.bbox->min().z() <= z && region.bbox->max().z() >= z) { + if (!bbox_z_in_layer_frame || (region.bbox->min().z() <= z && region.bbox->max().z() >= z)) { if (idx_first_printable_region == -1 && region.model_volume->is_model_part()) idx_first_printable_region = idx_region; else if (idx_first_printable_region != -1) { // Test for overlap with some other region. for (int idx_region2 = idx_first_printable_region; idx_region2 < idx_region; ++ idx_region2) { const PrintObjectRegions::VolumeRegion ®ion2 = layer_range.volume_regions[idx_region2]; - if (region2.bbox->min().z() <= z && region2.bbox->max().z() >= z && overlap_in_xy(*region.bbox, *region2.bbox)) { + const bool region2_in_z = !bbox_z_in_layer_frame + || (region2.bbox->min().z() <= z && region2.bbox->max().z() >= z); + if (region2_in_z && overlap_in_xy(*region.bbox, *region2.bbox)) { complex = true; break; } From a9bae54f20aaa669748fb104f6dc70e942fa9e50 Mon Sep 17 00:00:00 2001 From: Joseph Robertson <32232925+HarrierPigeon@users.noreply.github.com> Date: Fri, 22 May 2026 15:21:33 -0500 Subject: [PATCH 031/224] Rotate instead of shear for slicing stage (#30) * initial commit * fix upper bounds for assemblies * significantly less Z shift issues, still not quite tamped down yet though * add instrumentation to logs * finally found the issue * update printer defaults --- resources/profiles/Custom.json | 2 +- .../Custom/machine/fdm_belt_common.json | 6 +- src/libslic3r/BeltGCode.cpp | 9 +- src/libslic3r/BeltGCodeWriter.cpp | 26 ++- src/libslic3r/BeltSliceStrategy.cpp | 89 +++++++++-- src/libslic3r/BeltSliceStrategy.hpp | 12 +- src/libslic3r/BeltTransform.cpp | 94 ++++++++++- src/libslic3r/BeltTransform.hpp | 11 ++ src/libslic3r/FirstLayerPlane.cpp | 14 +- src/libslic3r/GCode/BeltBackTransform.cpp | 5 +- src/libslic3r/Preset.cpp | 1 + src/libslic3r/PrintApply.cpp | 6 + src/libslic3r/PrintConfig.cpp | 74 +++++++-- src/libslic3r/PrintConfig.hpp | 25 ++- src/libslic3r/PrintObjectSlice.cpp | 149 +++++++++++++++--- src/slic3r/GUI/Tab.cpp | 86 +++++++--- 16 files changed, 513 insertions(+), 96 deletions(-) diff --git a/resources/profiles/Custom.json b/resources/profiles/Custom.json index 0f54d2f4dd..0ac3597347 100644 --- a/resources/profiles/Custom.json +++ b/resources/profiles/Custom.json @@ -1,6 +1,6 @@ { "name": "Custom Printer", - "version": "02.03.02.67", + "version": "02.03.02.68", "force_update": "0", "description": "My configurations", "machine_model_list": [ diff --git a/resources/profiles/Custom/machine/fdm_belt_common.json b/resources/profiles/Custom/machine/fdm_belt_common.json index ce39146042..0c3a976c0e 100644 --- a/resources/profiles/Custom/machine/fdm_belt_common.json +++ b/resources/profiles/Custom/machine/fdm_belt_common.json @@ -89,9 +89,9 @@ "1" ], "belt_printer": "1", - "belt_shear_z": "pos_tan", - "belt_shear_z_angle": "45", - "belt_scale_y": "inv_cos", + "belt_slice_rotation": "x", + "belt_slice_rotation_angle": "45", + "belt_slice_rotation_global": "1", "gcode_shear_z": "pos_tan", "gcode_scale_y": "inv_cos", "build_plate_tilt_x": "45", diff --git a/src/libslic3r/BeltGCode.cpp b/src/libslic3r/BeltGCode.cpp index 767d280579..a89b20bcec 100644 --- a/src/libslic3r/BeltGCode.cpp +++ b/src/libslic3r/BeltGCode.cpp @@ -53,6 +53,10 @@ void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print) file.write_format("; belt_scale_y_angle = %.1f\n", print.config().belt_scale_y_angle.value); file.write_format("; belt_scale_z = %s\n", full_cfg.opt_serialize("belt_scale_z").c_str()); file.write_format("; belt_scale_z_angle = %.1f\n", print.config().belt_scale_z_angle.value); + // Slicing rotation configs + 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); file.write_format("; belt_mesh_transform_order = %s\n", full_cfg.opt_serialize("belt_mesh_transform_order").c_str()); // Pre-slice remap configs file.write_format("; preslice_remap_x = %s\n", full_cfg.opt_serialize("preslice_remap_x").c_str()); @@ -104,7 +108,10 @@ void BeltGCode::on_set_origin(const PrintObject *obj, const Point &inst_shift) || (m_config.preslice_remap_global.value && BeltTransformPipeline::has_preslice_remap(m_config)) || (m_config.belt_shear_z_global.value - && m_config.belt_shear_z.value != BeltShearMode::None); + && m_config.belt_shear_z.value != BeltShearMode::None) + || (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 && m_config.belt_printer.value) { auto *belt_writer = dynamic_cast(m_writer.get()); if (belt_writer) { diff --git a/src/libslic3r/BeltGCodeWriter.cpp b/src/libslic3r/BeltGCodeWriter.cpp index 1987d0127a..b2a8f5c470 100644 --- a/src/libslic3r/BeltGCodeWriter.cpp +++ b/src/libslic3r/BeltGCodeWriter.cpp @@ -57,17 +57,39 @@ Vec3d BeltGCodeWriter::to_cartesian(const Vec3d &pos) const Vec3d BeltGCodeWriter::to_machine_coords(const Vec3d &pos) const { // Step 1+2: To Cartesian (back_transform + axis_remap). - Vec3d result = to_cartesian(pos); + Vec3d after_back = m_belt_back_transform.apply(pos); + Vec3d result = apply_axis_remap(after_back); + Vec3d after_remap = result; // Step 3: Per-axis origin snap (computed in the Cartesian frame). for (int i = 0; i < 3; ++i) if (m_origin_snap[i]) result[i] -= (m_origin_bbox_min[i] - m_origin_offset[i]); + Vec3d after_snap = result; // Step 4: Machine-frame transform (gcode_shear / gcode_scale / post_gcode_remap) // applied LAST so it acts as a global linear transform on the placed coords. // Order matters: putting it before origin_snap would feed sheared bbox corners // into the snap's per-object min calculation, mis-normalizing non-cubic geometries // (the corners of the original bbox aren't extreme points of the sheared shape). - return m_machine_frame_transform.apply(result); + Vec3d final = m_machine_frame_transform.apply(result); + + // [BELT-DEBUG] One-shot log per layer transition (i.e. when the input Z + // crosses an integer mm boundary) to keep the log volume manageable while + // still capturing one sample per ~5 layers. Shows the full pipeline so + // Case A vs Case B can be compared step-by-step. + static thread_local int s_last_logged_z = std::numeric_limits::min(); + int z_bucket = static_cast(std::floor(pos.z() * 5.0)); // every 0.2mm + if (z_bucket != s_last_logged_z) { + s_last_logged_z = z_bucket; + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] to_machine_coords" + << " slicer_in=(" << pos.x() << "," << pos.y() << "," << pos.z() << ")" + << " after_back=(" << after_back.x() << "," << after_back.y() << "," << after_back.z() << ")" + << " after_remap=(" << after_remap.x() << "," << after_remap.y() << "," << after_remap.z() << ")" + << " after_snap=(" << after_snap.x() << "," << after_snap.y() << "," << after_snap.z() << ")" + << " final=(" << final.x() << "," << final.y() << "," << final.z() << ")" + << " mft_active=" << m_machine_frame_transform.is_active() + << " back_active=" << m_belt_back_transform.is_active(); + } + return final; } // ---- Overridden movement methods ------------------------------------------ diff --git a/src/libslic3r/BeltSliceStrategy.cpp b/src/libslic3r/BeltSliceStrategy.cpp index 850a4ad221..81c4c06f1e 100644 --- a/src/libslic3r/BeltSliceStrategy.cpp +++ b/src/libslic3r/BeltSliceStrategy.cpp @@ -1,6 +1,9 @@ #include "BeltSliceStrategy.hpp" +#include "Model.hpp" #include +#include +#include #include namespace Slic3r { @@ -14,9 +17,10 @@ std::unique_ptr BeltSliceStrategy::create(const PrintConfig & BeltSliceStrategy::BeltSliceStrategy(const PrintConfig &config) { - m_shear = BeltTransformPipeline::build_shear_matrix(config, &m_has_shear); - m_scale = BeltTransformPipeline::build_scale_matrix(config, &m_has_scale); - m_order = config.belt_mesh_transform_order.value; + m_shear = BeltTransformPipeline::build_shear_matrix(config, &m_has_shear); + m_scale = BeltTransformPipeline::build_scale_matrix(config, &m_has_scale); + m_rotation = BeltTransformPipeline::build_rotation_matrix(config, &m_has_rotation); + m_order = config.belt_mesh_transform_order.value; } void BeltSliceStrategy::apply_to_trafo(Transform3d &trafo, @@ -26,27 +30,83 @@ void BeltSliceStrategy::apply_to_trafo(Transform3d &trafo, { // ScaleThenShear: applied to a point, scale runs first then shear (m_shear * m_scale). // ShearThenScale: applied to a point, shear runs first then scale (m_scale * m_shear). - if (m_has_shear || m_has_scale) { - Transform3d belt_xform = Transform3d::Identity(); - belt_xform.linear() = (m_order == BeltTransformOrder::ScaleThenShear) + // Rotation (if active) is applied AFTER shear/scale, matching build_forward_transform. + if (m_has_shear || m_has_scale || m_has_rotation) { + Matrix3d shear_scale = (m_order == BeltTransformOrder::ScaleThenShear) ? Matrix3d(m_shear * m_scale) : Matrix3d(m_scale * m_shear); + Transform3d belt_xform = Transform3d::Identity(); + belt_xform.linear() = Matrix3d(m_rotation * shear_scale); trafo = belt_xform * trafo; } // Z-shift — detect if mesh clips below build plate after transforms. - if (has_remap || m_has_shear || m_has_scale) { + // 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 + // step on assemblies (where per-volume get_matrix() positions each volume + // within the object) causes min_z to be computed against mesh-local vertex + // coordinates rather than object-space coordinates, so volumes translated + // along the slicer's Z axis are silently excluded from the bound check. + if (has_remap || m_has_shear || m_has_scale || m_has_rotation) { + // [BELT-DEBUG] Capture the incoming trafo for diagnostic logging. + // This is the slicer-frame transform AFTER belt_xform but BEFORE z_shift. + const Transform3d trafo_pre_shift = trafo; + auto log_mat = [](const Matrix3d &m) { + std::ostringstream ss; + ss << std::fixed << std::setprecision(4); + ss << "[[" << m(0,0) << "," << m(0,1) << "," << m(0,2) << "]," + << "[" << m(1,0) << "," << m(1,1) << "," << m(1,2) << "]," + << "[" << m(2,0) << "," << m(2,1) << "," << m(2,2) << "]]"; + return ss.str(); + }; + auto log_vec3 = [](const Vec3d &v) { + std::ostringstream ss; + ss << std::fixed << std::setprecision(4); + ss << "(" << v.x() << "," << v.y() << "," << v.z() << ")"; + return ss.str(); + }; + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] apply_to_trafo enter" + << " has_shear=" << m_has_shear + << " has_scale=" << m_has_scale + << " has_rotation=" << m_has_rotation + << " has_remap=" << has_remap + << " trafo.linear=" << log_mat(trafo_pre_shift.linear()) + << " trafo.translation=" << log_vec3(trafo_pre_shift.translation()) + << " volumes=" << model_volumes.size(); + double min_z = std::numeric_limits::max(); + int vol_idx = 0; for (const ModelVolume *mv : model_volumes) { - if (!mv->is_model_part()) continue; - for (const stl_vertex &v : mv->mesh().its.vertices) { - Vec3d pt = trafo * v.cast(); + if (!mv->is_model_part()) { ++vol_idx; continue; } + Transform3d vol_trafo = trafo * mv->get_matrix(); + // [BELT-DEBUG] Per-volume bbox in mesh-frame and post-trafo slicer-frame. + const auto &its = mv->mesh().its; + Vec3d mesh_min(std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::max()); + Vec3d mesh_max(std::numeric_limits::lowest(), std::numeric_limits::lowest(), std::numeric_limits::lowest()); + Vec3d slicer_min(std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::max()); + Vec3d slicer_max(std::numeric_limits::lowest(), std::numeric_limits::lowest(), std::numeric_limits::lowest()); + double vol_min_z = std::numeric_limits::max(); + for (const stl_vertex &v : its.vertices) { + Vec3d vm = v.cast(); + mesh_min = mesh_min.cwiseMin(vm); + mesh_max = mesh_max.cwiseMax(vm); + Vec3d pt = vol_trafo * vm; + slicer_min = slicer_min.cwiseMin(pt); + slicer_max = slicer_max.cwiseMax(pt); + vol_min_z = std::min(vol_min_z, pt.z()); min_z = std::min(min_z, pt.z()); } + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] vol[" << vol_idx + << "] id=" << mv->id().id << " name='" << mv->name << "'" + << " mesh_bbox_min=" << log_vec3(mesh_min) << " mesh_bbox_max=" << log_vec3(mesh_max) + << " get_matrix.translation=" << log_vec3(mv->get_matrix().translation()) + << " slicer_bbox_min=" << log_vec3(slicer_min) << " slicer_bbox_max=" << log_vec3(slicer_max) + << " vol_min_z=" << vol_min_z; + ++vol_idx; } double belt_z_shift_val = (min_z < 0. && min_z != std::numeric_limits::max()) ? -min_z : 0.; - BOOST_LOG_TRIVIAL(warning) << "Belt Z-shift: min_z=" << min_z - << " z_shift=" << belt_z_shift_val; + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] combined min_z=" << min_z + << " z_shift_val=" << belt_z_shift_val; if (belt_z_shift_val > 0.) { Transform3d z_shift = Transform3d::Identity(); z_shift.matrix()(2, 3) = belt_z_shift_val; @@ -54,10 +114,13 @@ void BeltSliceStrategy::apply_to_trafo(Transform3d &trafo, } if (out_belt_min_z) { double new_val = (min_z != std::numeric_limits::max()) ? min_z : 0.; - BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] write m_belt_min_z tid=" << std::this_thread::get_id() + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] write m_belt_min_z tid=" << std::this_thread::get_id() << " target=" << out_belt_min_z << " old=" << *out_belt_min_z << " new=" << new_val; *out_belt_min_z = new_val; } + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] apply_to_trafo exit" + << " final_trafo.linear=" << log_mat(trafo.linear()) + << " final_trafo.translation=" << log_vec3(trafo.translation()); } } diff --git a/src/libslic3r/BeltSliceStrategy.hpp b/src/libslic3r/BeltSliceStrategy.hpp index c2362e3552..6009155c7b 100644 --- a/src/libslic3r/BeltSliceStrategy.hpp +++ b/src/libslic3r/BeltSliceStrategy.hpp @@ -34,11 +34,13 @@ public: private: explicit BeltSliceStrategy(const PrintConfig &config); - bool m_has_shear = false; - bool m_has_scale = false; - Matrix3d m_shear = Matrix3d::Identity(); - Matrix3d m_scale = Matrix3d::Identity(); - BeltTransformOrder m_order = BeltTransformOrder::ScaleThenShear; + bool m_has_shear = false; + bool m_has_scale = false; + bool m_has_rotation = false; + Matrix3d m_shear = Matrix3d::Identity(); + Matrix3d m_scale = Matrix3d::Identity(); + Matrix3d m_rotation = Matrix3d::Identity(); + BeltTransformOrder m_order = BeltTransformOrder::ScaleThenShear; }; } // namespace Slic3r diff --git a/src/libslic3r/BeltTransform.cpp b/src/libslic3r/BeltTransform.cpp index 6c0838b2eb..0d1ae41295 100644 --- a/src/libslic3r/BeltTransform.cpp +++ b/src/libslic3r/BeltTransform.cpp @@ -95,23 +95,47 @@ Matrix3d BeltTransformPipeline::build_scale_matrix(const PrintConfig &config, bo return scale; } +Matrix3d BeltTransformPipeline::build_rotation_matrix(const PrintConfig &config, bool *has_rot_out) +{ + BeltRotationAxis axis = config.belt_slice_rotation.value; + double angle_deg = config.belt_slice_rotation_angle.value; + bool active = axis != BeltRotationAxis::None && std::abs(angle_deg) > EPSILON; + if (has_rot_out) *has_rot_out = active; + if (!active) + return Matrix3d::Identity(); + double angle_rad = Geometry::deg2rad(angle_deg); + Vec3d unit_axis; + switch (axis) { + case BeltRotationAxis::X: unit_axis = Vec3d::UnitX(); break; + case BeltRotationAxis::Y: unit_axis = Vec3d::UnitY(); break; + case BeltRotationAxis::Z: unit_axis = Vec3d::UnitZ(); break; + default: return Matrix3d::Identity(); + } + return Eigen::AngleAxisd(angle_rad, unit_axis).toRotationMatrix(); +} + Transform3d BeltTransformPipeline::build_forward_transform(const PrintConfig &config) { - Transform3d pre_remap = build_preslice_remap(config); + Transform3d pre_remap = build_preslice_remap(config); bool shear_active = false; Matrix3d shear = build_shear_matrix(config, &shear_active); bool scale_active = false; Matrix3d scale = build_scale_matrix(config, &scale_active); + bool rot_active = false; + Matrix3d rot = build_rotation_matrix(config, &rot_active); // Match the mesh-side ordering selected by belt_mesh_transform_order so // BeltBackTransform inverts the same composition that BeltSliceStrategy // applied to the mesh. // ScaleThenShear: applied to p, scale runs first then shear (shear * scale). // ShearThenScale: applied to p, shear runs first then scale (scale * shear). - Transform3d combined = Transform3d::Identity(); - combined.linear() = (config.belt_mesh_transform_order.value == BeltTransformOrder::ScaleThenShear) + // Rotation is applied AFTER shear/scale: rot * shear_scale * pre_remap. + Matrix3d shear_scale = (config.belt_mesh_transform_order.value == BeltTransformOrder::ScaleThenShear) ? Matrix3d(shear * scale) : Matrix3d(scale * shear); + + Transform3d combined = Transform3d::Identity(); + combined.linear() = Matrix3d(rot * shear_scale); combined = combined * pre_remap; return combined; } @@ -166,7 +190,7 @@ BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl( BeltTransformPipeline::BeltHeightResult result; result.object_height = original_height; - // Extract Z-axis shear/scale + per-axis scale + transform order from config. + // Extract Z-axis shear/scale + per-axis scale + transform order + rotation from config. BeltShearMode z_shear_mode; double z_shear_angle; BeltScaleMode z_scale_mode; @@ -175,6 +199,8 @@ BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl( BeltScaleMode from_scale_mode; // scale on the shear's source axis double from_scale_angle; BeltTransformOrder order; + BeltRotationAxis rot_axis; + double rot_angle; if constexpr (std::is_same_v) { z_shear_mode = config.belt_shear_z.value; @@ -183,6 +209,8 @@ BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl( z_scale_angle = config.belt_scale_z_angle.value; z_shear_from = int(config.belt_shear_z_from.value); order = config.belt_mesh_transform_order.value; + rot_axis = config.belt_slice_rotation.value; + rot_angle = config.belt_slice_rotation_angle.value; if (z_shear_from == 0) { from_scale_mode = config.belt_scale_x.value; from_scale_angle = config.belt_scale_x_angle.value; @@ -212,12 +240,18 @@ BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl( auto *opt = config.template option>(key); return opt ? opt->value : BeltTransformOrder::ScaleThenShear; }; + auto get_rot_axis = [&](const char *key) { + auto *opt = config.template option>(key); + return opt ? opt->value : BeltRotationAxis::None; + }; z_shear_mode = get_shear("belt_shear_z"); z_shear_angle = get_float("belt_shear_z_angle"); z_scale_mode = get_scale("belt_scale_z"); z_scale_angle = get_float("belt_scale_z_angle"); z_shear_from = get_axis("belt_shear_z_from"); order = get_order("belt_mesh_transform_order"); + rot_axis = get_rot_axis("belt_slice_rotation"); + rot_angle = get_float("belt_slice_rotation_angle"); if (z_shear_from == 0) { from_scale_mode = get_scale("belt_scale_x"); from_scale_angle = get_float("belt_scale_x_angle"); @@ -227,10 +261,11 @@ BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl( } } - bool has_z_shear = z_shear_mode != BeltShearMode::None; - bool has_z_scale = z_scale_mode != BeltScaleMode::None; + bool has_z_shear = z_shear_mode != BeltShearMode::None; + bool has_z_scale = z_scale_mode != BeltScaleMode::None; + bool has_rotation = rot_axis != BeltRotationAxis::None && std::abs(rot_angle) > EPSILON; - if (!has_z_shear && !has_z_scale) + if (!has_z_shear && !has_z_scale && !has_rotation) return result; double shear_factor = has_z_shear @@ -267,6 +302,51 @@ BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl( result.floor_params.shear_factor = effective_shear; result.floor_params.from_axis = from; result.floor_params.z_shift = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.); + } else if (has_rotation) { + // Rotation-only path (no Z-shear): sweep 8 bbox corners through R. + double angle_rad = Geometry::deg2rad(rot_angle); + Vec3d unit_axis; + switch (rot_axis) { + case BeltRotationAxis::X: unit_axis = Vec3d::UnitX(); break; + case BeltRotationAxis::Y: unit_axis = Vec3d::UnitY(); break; + case BeltRotationAxis::Z: unit_axis = Vec3d::UnitZ(); break; + default: unit_axis = Vec3d::UnitX(); break; + } + Matrix3d R = Eigen::AngleAxisd(angle_rad, unit_axis).toRotationMatrix(); + double min_rz = std::numeric_limits::max(); + double max_rz = std::numeric_limits::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(); + min_rz = std::min(min_rz, z); + max_rz = std::max(max_rz, z); + } + // Optional Z-scale still applies multiplicatively if both are set. + result.object_height = (max_rz - min_rz) * (has_z_scale ? scale_z : 1.0); + + // 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.); } else { result.object_height = original_height * scale_z; } diff --git a/src/libslic3r/BeltTransform.hpp b/src/libslic3r/BeltTransform.hpp index f15a9995c3..1ce47a2e31 100644 --- a/src/libslic3r/BeltTransform.hpp +++ b/src/libslic3r/BeltTransform.hpp @@ -92,6 +92,12 @@ public: std::abs(sz - 1.) > EPSILON; } + static bool has_rotation(const PrintConfig &config) + { + return config.belt_slice_rotation.value != BeltRotationAxis::None && + std::abs(config.belt_slice_rotation_angle.value) > EPSILON; + } + // ---- Matrix builders -------------------------------------------------- // Build the pre-slice axis remap transform (includes Rev-mode translation). @@ -105,6 +111,11 @@ public: // Also sets has_scale_out if non-null. static Matrix3d build_scale_matrix(const PrintConfig &config, bool *has_scale_out = nullptr); + // 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. Shear/scale order is selected by // belt_mesh_transform_order so the result matches what BeltSliceStrategy // applied to the mesh (BeltBackTransform inverts this). diff --git a/src/libslic3r/FirstLayerPlane.cpp b/src/libslic3r/FirstLayerPlane.cpp index 23327aea7a..10866c57f5 100644 --- a/src/libslic3r/FirstLayerPlane.cpp +++ b/src/libslic3r/FirstLayerPlane.cpp @@ -84,12 +84,12 @@ FirstLayerPlane::FirstLayerPlane(const PrintConfig &config) // -------- Resolve Auto ------------------------------------------------- FirstLayerPlaneMode mode = config.first_layer_plane.value; if (mode == FirstLayerPlaneMode::Auto) { - if (config.belt_printer.value && - config.belt_shear_z.value != BeltShearMode::None) { - mode = FirstLayerPlaneMode::BeltShear; - } else { - mode = FirstLayerPlaneMode::XY; - } + bool belt_affine_active = config.belt_printer.value && + (config.belt_shear_z.value != BeltShearMode::None || + (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; @@ -130,7 +130,7 @@ FirstLayerPlane::FirstLayerPlane(const PrintConfig &config) m_active = true; return; - case FirstLayerPlaneMode::BeltShear: { + 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); diff --git a/src/libslic3r/GCode/BeltBackTransform.cpp b/src/libslic3r/GCode/BeltBackTransform.cpp index 893c46e52b..654afe6262 100644 --- a/src/libslic3r/GCode/BeltBackTransform.cpp +++ b/src/libslic3r/GCode/BeltBackTransform.cpp @@ -15,9 +15,12 @@ bool BeltBackTransform::init_from_config(const PrintConfig &config) bool has_global_shear = config.belt_shear_x_global.value || config.belt_shear_y_global.value || config.belt_shear_z_global.value; + 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_shear && !has_preslice_global && !BeltTransformPipeline::has_preslice_remap(config)) + if (!has_global_shear && !has_global_rotation && !has_preslice_global + && !BeltTransformPipeline::has_preslice_remap(config)) return false; // Build the forward pipeline (scale * shear * pre_remap) and store its inverse. diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index b6875f6117..8a5acdd95a 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1329,6 +1329,7 @@ static std::vector s_Preset_printer_options { "belt_shear_y", "belt_shear_y_angle", "belt_shear_y_from", "belt_shear_y_global", "belt_shear_z", "belt_shear_z_angle", "belt_shear_z_from", "belt_shear_z_global", "belt_scale_x", "belt_scale_x_angle", "belt_scale_y", "belt_scale_y_angle", "belt_scale_z", "belt_scale_z_angle", + "belt_slice_rotation", "belt_slice_rotation_angle", "belt_slice_rotation_global", "belt_mesh_transform_order", "preslice_remap_x", "preslice_remap_y", "preslice_remap_z", "preslice_remap_global", "gcode_remap_x", "gcode_remap_y", "gcode_remap_z", "gcode_back_transform", diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index 11b8c4d99c..eb4e7b0bed 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1536,6 +1536,9 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ // so each gets independent layer Z values. bool belt_force_separate = m_config.belt_printer.value && ( (m_config.belt_shear_z_global.value && m_config.belt_shear_z.value != BeltShearMode::None) + || (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))); model_object_status.print_instances = print_objects_from_model_object(*model_object, this->shrinkage_compensation(), belt_force_separate); @@ -1629,6 +1632,9 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ if (belt_instances_shifted && m_config.belt_printer.value && ((m_config.belt_shear_z_global.value && m_config.belt_shear_z.value != BeltShearMode::None) + || (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)))) { for (PrintObject *object : m_objects) diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 5edfb1b0f0..ab6c122fd3 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -326,6 +326,14 @@ static t_config_enum_values s_keys_map_BeltAxis { }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltAxis) +static t_config_enum_values s_keys_map_BeltRotationAxis { + { "none", int(BeltRotationAxis::None) }, + { "x", int(BeltRotationAxis::X) }, + { "y", int(BeltRotationAxis::Y) }, + { "z", int(BeltRotationAxis::Z) }, +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltRotationAxis) + static t_config_enum_values s_keys_map_BeltTransformOrder { { "scale_then_shear", int(BeltTransformOrder::ScaleThenShear) }, { "shear_then_scale", int(BeltTransformOrder::ShearThenScale) }, @@ -374,7 +382,10 @@ static t_config_enum_values s_keys_map_FirstLayerPlaneMode { { "xy", int(FirstLayerPlaneMode::XY) }, { "yz", int(FirstLayerPlaneMode::YZ) }, { "xz", int(FirstLayerPlaneMode::XZ) }, - { "belt_shear", int(FirstLayerPlaneMode::BeltShear) }, + { "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) @@ -6429,9 +6440,9 @@ void PrintConfigDef::init_fff_params() add_belt_axis_enum ("belt_shear_y_from", "From", "Source axis for Y shear.", BeltAxis::Z, comExpert); add_belt_shear_global("belt_shear_y_global", "Global"); - add_belt_shear_mode ("belt_shear_z", "Function", BeltShearMode::PosTan); - add_belt_shear_angle("belt_shear_z_angle", "Angle"); - add_belt_axis_enum ("belt_shear_z_from", "From", "Source axis for Z shear.", BeltAxis::Y); + add_belt_shear_mode ("belt_shear_z", "Function", BeltShearMode::None, comExpert); + add_belt_shear_angle("belt_shear_z_angle", "Angle", comExpert); + add_belt_axis_enum ("belt_shear_z_from", "From", "Source axis for Z shear.", BeltAxis::Y, comExpert); add_belt_shear_global("belt_shear_z_global", "Global", true); // Per-axis scale controls for belt printer @@ -6463,12 +6474,47 @@ void PrintConfigDef::init_fff_params() add_belt_scale_mode ("belt_scale_x", "Function", BeltScaleMode::None, comExpert); add_belt_scale_angle("belt_scale_x_angle", "Angle", comExpert); - add_belt_scale_mode ("belt_scale_y", "Function", BeltScaleMode::None); - add_belt_scale_angle("belt_scale_y_angle", "Angle"); + add_belt_scale_mode ("belt_scale_y", "Function", BeltScaleMode::None, comExpert); + add_belt_scale_angle("belt_scale_y_angle", "Angle", comExpert); add_belt_scale_mode ("belt_scale_z", "Function", BeltScaleMode::None, comExpert); add_belt_scale_angle("belt_scale_z_angle", "Angle", comExpert); + // Global slicing rotation (alternative to per-axis shear+scale). + def = this->add("belt_slice_rotation", coEnum); + def->label = L("Slicing rotation axis"); + def->category = L("Printable space"); + def->tooltip = L("Rotate the mesh by this axis before slicing. Use this for an " + "isometric (no shear distortion) belt slicing transform. " + "Mutually exclusive with the per-axis shear/scale controls " + "in the UI; the pipeline composes them if both are set in JSON."); + def->enum_keys_map = &ConfigOptionEnum::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->set_default_value(new ConfigOptionEnum(BeltRotationAxis::None)); + + def = this->add("belt_slice_rotation_angle", coFloat); + def->label = L("Slicing rotation angle"); + def->category = L("Printable space"); + def->tooltip = L("Magnitude of the slicing rotation, in degrees. Positive values " + "rotate counter-clockwise looking down the positive axis."); + def->sidetext = L("°"); + def->min = -180.; + def->max = 180.; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0.)); + + 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; mirrors belt_shear_z_global. " + "Defaults to on because virtually all rotation-mode printers need it."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(true)); + auto add_belt_transform_order = [this](const char *key, const char *label, const char *tooltip) { auto def = this->add(key, coEnum); def->label = L(label); @@ -6597,7 +6643,8 @@ void PrintConfigDef::init_fff_params() // 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 BeltShear when belt_shear_z != None, otherwise XY (legacy). + // 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"); @@ -6605,14 +6652,15 @@ void PrintConfigDef::init_fff_params() "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 shear plane when belt_shear_z is non-None, " - "otherwise XY (legacy). Pick XY explicitly to opt out and force the " - "legacy slicing-layer-0 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::get_enum_values(); - def->enum_values = {"auto", "xy", "yz", "xz", "belt_shear"}; - def->enum_labels = {L("Auto"), L("XY (machine bed)"), L("YZ"), L("XZ"), L("Belt shear plane")}; + 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; - def->set_default_value(new ConfigOptionEnum(FirstLayerPlaneMode::BeltShear)); + def->set_default_value(new ConfigOptionEnum(FirstLayerPlaneMode::BeltAffine)); def = this->add("first_layer_plane_offset", coFloat); def->label = L("Belt plane offset"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 3536f6d909..a6c6f55394 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -194,6 +194,17 @@ enum class BeltAxis Z = 2, }; +// Axis around which the mesh is rotated before slicing, when +// `belt_slice_rotation` is set. None disables the rotation stage. +// Distinct from BeltAxis because BeltAxis carries no "None" semantics. +enum class BeltRotationAxis +{ + None = 0, + X = 1, + Y = 2, + Z = 3, +}; + // Order in which the belt shear and scale matrices are composed. // ScaleThenShear: applied to a point p, the result is shear(scale(p)). // ShearThenScale: applied to a point p, the result is scale(shear(p)). @@ -231,8 +242,9 @@ enum class BeltSupportZOffsetMode // // Auto resolves to: // - XY (inactive, legacy behavior) for non-belt printers and for belt -// printers with no Z-axis shear. -// - BeltShear for belt printers with belt_shear_z != None. +// 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. @@ -242,7 +254,7 @@ enum class FirstLayerPlaneMode XY, YZ, XZ, - BeltShear, + BeltAffine, // formerly BeltShear; renamed to reflect rotation support }; enum SupportMaterialPattern { @@ -607,6 +619,7 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SlicingMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltShearMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltScaleMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltAxis) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltRotationAxis) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltTransformOrder) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(RemapAxis) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltSupportFloorMode) @@ -1579,6 +1592,12 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionFloat, belt_scale_y_angle)) ((ConfigOptionEnum, belt_scale_z)) ((ConfigOptionFloat, belt_scale_z_angle)) + // Global mesh rotation as an alternative to per-axis shear/scale (isometric + // slicing transform). Composes with shear in the pipeline math; UI gates + // them as mutually exclusive. + ((ConfigOptionEnum, belt_slice_rotation)) + ((ConfigOptionFloat, belt_slice_rotation_angle)) + ((ConfigOptionBool, belt_slice_rotation_global)) ((ConfigOptionEnum, preslice_remap_x)) ((ConfigOptionEnum, preslice_remap_y)) ((ConfigOptionEnum, preslice_remap_z)) diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 07ba2c63af..a0d2b18992 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -174,12 +174,15 @@ static std::vector slice_volumes_inner( belt_strategy->apply_to_trafo(params_base.trafo, model_volumes, has_remap, out_belt_min_z); } // If only remap (no belt), still need z-shift detection. + // Compose per-volume mv->get_matrix() so assembly volumes contribute their + // actual object-space positions — see the matching note in BeltSliceStrategy. if (has_remap && !print_config.belt_printer.value) { double min_z = std::numeric_limits::max(); for (const ModelVolume *mv : model_volumes) { if (!mv->is_model_part()) continue; + Transform3d vol_trafo = params_base.trafo * mv->get_matrix(); for (const stl_vertex &v : mv->mesh().its.vertices) { - Vec3d pt = params_base.trafo * v.cast(); + Vec3d pt = vol_trafo * v.cast(); min_z = std::min(min_z, pt.z()); } } @@ -322,17 +325,35 @@ static std::vector> slices_to_regions( } } else { zs_complex.reserve(zs.size()); - // region.bbox is computed in pre-belt-shear slicer space (see PrintApply.cpp::trafo_for_bbox). - // When belt transforms are active, layer Z values are in post-shear/scale/remap space, + // region.bbox is computed in pre-belt-transform slicer space (see PrintApply.cpp::trafo_for_bbox). + // When belt transforms are active, layer Z values are in post-rotation/shear/scale/remap space, // so the Z components of region.bbox aren't comparable to z. Skipping the Z filter here // 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_shear(print_config) || BeltTransformPipeline::has_scale(print_config) + || BeltTransformPipeline::has_rotation(print_config) || BeltTransformPipeline::has_preslice_remap(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 + // or side-by-side in pre-transform Z (different bbox.z ranges) thus lose + // the volumes that originally sat outside the first volume's Z range — + // showing up as truncation at the top or bottom of the assembly. Force + // every layer in a multi-volume range through the parallel_for path, + // which correctly merges all volumes per layer. + int num_model_parts = 0; + for (const PrintObjectRegions::VolumeRegion &vr : layer_range.volume_regions) + if (vr.model_volume->is_model_part()) + ++num_model_parts; + const bool force_complex_for_belt = !bbox_z_in_layer_frame && num_model_parts > 1; for (; z_idx < zs.size() && zs[z_idx] < layer_range.layer_height_range.second; ++ z_idx) { float z = zs[z_idx]; + if (force_complex_for_belt) { + zs_complex.push_back({ z_idx, z }); + continue; + } int idx_first_printable_region = -1; bool complex = false; for (int idx_region = 0; idx_region < int(layer_range.volume_regions.size()); ++ idx_region) { @@ -949,7 +970,81 @@ void PrintObject::slice() BOOST_LOG_TRIVIAL(warning) << "Belt global: object " << this->model_object()->name << " instances=" << this->instances().size() << " shift=(" << unscale(inst_shift.x()) << ", " << unscale(inst_shift.y()) << ")"; - double global_z_offset = 0.; + + // Per-object Z-shift compensation, applied regardless of global mode. + // + // BeltSliceStrategy::apply_to_trafo lifts the mesh by max(0, -m_belt_min_z) + // so the slicer can slice with slicer_z >= 0. BeltBackTransform inverts + // build_forward_transform() which DOES NOT include this per-object + // Z-shift (it's not known until vertex scan time). Result: G-code + // coords emerge offset by the un-undone Z-shift — for shear that's a + // pure machine_z lift; for rotation it leaks into both machine_y and + // machine_z because the inverse rotation couples slicer_z back into + // both axes. Compensating layer.print_z by shear_min_z here makes the + // back-transform produce correct machine-frame coordinates whether or + // not any global mode is active. + // + // (The original global-mode-only application of shear_min_z was sized + // for cube-vs-inverted-cone-tip differentiation; the same compensation + // is what fixes assemblies and rotation-mode parts where z_shift > 0.) + double belt_surface_z = BeltTransformPipeline::has_preslice_remap(pcfg) + ? BeltTransformPipeline::remap_bbox(*this->model_object(), pcfg).min.z() : 0.; + double shear_min_z = m_belt_min_z - belt_surface_z; + double global_z_offset = shear_min_z; + + // Centering correction: trafo_centered pretranslates by + // -m_center_offset.{x,y}. Under the belt forward transform, the + // Y component of that pretranslate couples into slicer-Z (shear: + // tan*c.y, rotation: sin*c.y). BeltBackTransform inverts the + // rotation/shear but doesn't undo centering, so this Z component + // leaks into machine output as a position offset whenever + // m_center_offset != 0. When a user moves a volume within an + // assembly such that the combined bbox center shifts, this shows + // up as a small Z translation in the print. Compensate by adding + // the Z component of the centering through the forward transform. + { + Transform3d T_fwd = BeltTransformPipeline::build_forward_transform(pcfg); + Vec3d c_off(unscale(m_center_offset.x()), + unscale(m_center_offset.y()), + 0.); + double centering_z_corr = (T_fwd.linear() * c_off).z(); + global_z_offset += centering_z_corr; + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] centering correction" + << " obj=" << this->model_object()->name + << " m_center_offset_mm=(" << c_off.x() << "," << c_off.y() << ")" + << " centering_z_corr=" << centering_z_corr + << " (added to global_z_offset)"; + } + + // [BELT-DEBUG] Per-object summary so Case A vs Case B can be compared + // side-by-side. Lays out every value that feeds into the final layer + // print_z adjustment. + { + BoundingBoxf3 raw_bb = this->model_object()->raw_bounding_box(); + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] slice() per-object summary" + << " obj=" << this->model_object()->name + << " n_volumes=" << this->model_object()->volumes.size() + << " raw_bbox.min=(" << raw_bb.min.x() << "," << raw_bb.min.y() << "," << raw_bb.min.z() << ")" + << " raw_bbox.max=(" << raw_bb.max.x() << "," << raw_bb.max.y() << "," << raw_bb.max.z() << ")" + << " raw_bbox.center=(" << raw_bb.center().x() << "," << raw_bb.center().y() << ")" + << " m_center_offset=(" << unscale(m_center_offset.x()) << "," << unscale(m_center_offset.y()) << ")" + << " inst_shift=(" << unscale(inst_shift.x()) << "," << unscale(inst_shift.y()) << ")" + << " m_belt_min_z=" << m_belt_min_z + << " belt_surface_z=" << belt_surface_z + << " shear_min_z=" << shear_min_z; + // Per-volume bbox + get_matrix translation so order/composition is visible. + int vi = 0; + for (const ModelVolume *mv : this->model_object()->volumes) { + if (!mv->is_model_part()) { ++vi; continue; } + BoundingBoxf3 vol_bb = mv->mesh().transformed_bounding_box(mv->get_matrix()); + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] vol[" << vi + << "] id=" << mv->id().id << " name='" << mv->name << "'" + << " get_matrix.translation=(" << mv->get_matrix().translation().x() << "," << mv->get_matrix().translation().y() << "," << mv->get_matrix().translation().z() << ")" + << " object_bbox.min=(" << vol_bb.min.x() << "," << vol_bb.min.y() << "," << vol_bb.min.z() << ")" + << " object_bbox.max=(" << vol_bb.max.x() << "," << vol_bb.max.y() << "," << vol_bb.max.z() << ")"; + ++vi; + } + } if (pcfg.belt_preslice_global.value) { // Global pre-slice mode: compute full correction c = (T.linear() - I) * d @@ -957,18 +1052,7 @@ void PrintObject::slice() Transform3d T = BeltTransformPipeline::build_forward_transform(pcfg); Vec3d d(unscale(inst_shift.x()), unscale(inst_shift.y()), 0.); Vec3d c = T.linear() * d - d; - - // Per-object shape contribution: BeltSliceStrategy::apply_to_trafo - // lifts the mesh by max(0, -m_belt_min_z) to keep slicer-frame Z - // above 0. Two objects at the same bed position but different - // m_belt_min_z (e.g. cube vs inverted-cone tip) otherwise end up - // at the same print_z, which causes the inverted-cone tip to - // start on the same layer as the cube's lowest sheared corner. - double belt_surface_z = BeltTransformPipeline::has_preslice_remap(pcfg) - ? BeltTransformPipeline::remap_bbox(*this->model_object(), pcfg).min.z() : 0.; - double shear_min_z = m_belt_min_z - belt_surface_z; - - global_z_offset = c.z() + shear_min_z; + global_z_offset += c.z(); BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] write m_belt_global_xy_correction tid=" << std::this_thread::get_id() << " obj=" << this << " old=(" << m_belt_global_xy_correction.x() << "," << m_belt_global_xy_correction.y() << ") new=(" << c.x() << "," << c.y() << ")"; @@ -999,10 +1083,7 @@ void PrintObject::slice() Transform3d T = BeltTransformPipeline::build_forward_transform(pcfg); Vec3d d(unscale(inst_shift.x()), unscale(inst_shift.y()), 0.); Vec3d c = T.linear() * d - d; - double belt_surface_z = BeltTransformPipeline::has_preslice_remap(pcfg) - ? BeltTransformPipeline::remap_bbox(*this->model_object(), pcfg).min.z() : 0.; - double shear_min_z = m_belt_min_z - belt_surface_z; - global_z_offset += c.z() + shear_min_z; + global_z_offset += c.z(); BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] write m_belt_global_xy_correction tid=" << std::this_thread::get_id() << " obj=" << this << " old=(" << m_belt_global_xy_correction.x() << "," << m_belt_global_xy_correction.y() << ") new=(" << c.x() << "," << c.y() << ")"; @@ -1012,6 +1093,19 @@ void PrintObject::slice() << " shear_min_z=" << shear_min_z << " (m_belt_min_z=" << m_belt_min_z << ")"; } + // 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(inst_shift.x()), unscale(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 @@ -1030,6 +1124,15 @@ void PrintObject::slice() BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] write m_belt_global_z_offset tid=" << std::this_thread::get_id() << " obj=" << this << " old=" << m_belt_global_z_offset << " new=" << global_z_offset; m_belt_global_z_offset = global_z_offset; + // [BELT-DEBUG] Final breakdown of all contributions to layer.print_z + // and where the first / last layer end up post-adjustment. + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] global_z_offset breakdown" + << " obj=" << this->model_object()->name + << " shear_min_z=" << shear_min_z + << " total_global_z_offset=" << global_z_offset + << " xy_correction=(" << m_belt_global_xy_correction.x() << "," << m_belt_global_xy_correction.y() << ")" + << " belt_floor_z_shift_before=" << (m_slicing_params.belt_floor_z_shift) + << " n_layers=" << m_layers.size(); if (std::abs(global_z_offset) > EPSILON) { for (Layer *layer : m_layers) layer->print_z += global_z_offset; @@ -1038,6 +1141,12 @@ void PrintObject::slice() // layer print_z, so belt_floor_z_shift must match. m_slicing_params.belt_floor_z_shift += global_z_offset; } + if (!m_layers.empty()) { + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] post-adjustment" + << " first_layer.print_z=" << m_layers.front()->print_z + << " last_layer.print_z=" << m_layers.back()->print_z + << " belt_floor_z_shift_after=" << m_slicing_params.belt_floor_z_shift; + } if (!m_layers.empty()) { BOOST_LOG_TRIVIAL(warning) << "Belt global: first_layer_z=" << m_layers.front()->print_z << " last_layer_z=" << m_layers.back()->print_z diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 93dde99522..470f1d93cb 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4462,7 +4462,23 @@ void TabPrinter::build_fff() belt_og->append_single_option_line("belt_printer"); belt_og->append_single_option_line("belt_printer_angle"); belt_og->append_single_option_line("belt_printer_infinite_y"); - // Per-axis shear: group mode + angle + source on one row per axis + // Mesh rotate (default belt-side transform): isometric, no distortion. + // Shown above the per-axis shear/scale rows because most users should + // pick rotation; shear/scale is the expert escape hatch. + { + Line line = { L("Mesh rotate"), + L("Global mesh rotation applied before slicing. Isometric " + "(no distortion); the back-transform inverts it before the " + "machine-frame remap. Default belt transform. Mutually " + "exclusive with per-axis shear/scale in the UI.") }; + 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); + } + // Per-axis shear/scale: expert escape hatch for non-rigid belt transforms + // (BlackBelt-style 1/sin scaling, asymmetric belt geometries, etc.). + // Group mode + angle + source on one row per axis. { Line line = { L("Mesh shear X"), L("Shear applied to the X axis before slicing") }; line.append_option(belt_og->get_option("belt_shear_x")); @@ -5594,17 +5610,19 @@ void TabPrinter::toggle_options() bool expert_or_above = (m_mode >= comExpert); toggle_line("belt_printer_angle", is_belt); toggle_line("belt_printer_infinite_y", is_belt); - // Mesh shear/scale: only shear Z and scale Y are shown in Advanced; the others - // are Expert-only. Each Line packs all its options on one row, so toggle the - // Line here in addition to the per-option mode gating. + // Mesh rotate: advanced (visible by default in belt mode). + toggle_line("belt_slice_rotation", is_belt); + // Mesh shear/scale: expert-only (the rigid rotation above is the + // primary belt transform; shear/scale is reserved for power users + // matching BlackBelt-style 1/sin scale or asymmetric belt geometries). toggle_line("belt_shear_x", is_belt && expert_or_above); toggle_line("belt_shear_y", is_belt && expert_or_above); - toggle_line("belt_shear_z", is_belt); + toggle_line("belt_shear_z", is_belt && expert_or_above); toggle_line("belt_scale_x", is_belt && expert_or_above); - toggle_line("belt_scale_y", is_belt); + toggle_line("belt_scale_y", is_belt && expert_or_above); toggle_line("belt_scale_z", is_belt && expert_or_above); - for (auto el : {"belt_mesh_transform_order", - "belt_origin_snap_x", "belt_origin_snap_y", "belt_origin_snap_z"}) + toggle_line("belt_mesh_transform_order", is_belt && expert_or_above); + for (auto el : {"belt_origin_snap_x", "belt_origin_snap_y", "belt_origin_snap_z"}) toggle_line(el, is_belt); // Remap, back-transform, and global mesh-transforms toggles are gated by belt @@ -5619,31 +5637,59 @@ void TabPrinter::toggle_options() // preslice_remap_global: superseded by belt_preslice_global toggle_option("preslice_remap_global", is_belt && !belt_global); + // Mutual exclusion (UI only): slicing rotation and per-axis shear/scale are + // alternatives in the printer panel. The pipeline math composes them; this + // just disables the inactive set so users pick one path or the other. + auto rot_axis = m_config->option>("belt_slice_rotation")->value; + bool rotation_active = is_belt + && rot_axis != BeltRotationAxis::None + && std::abs(m_config->opt_float("belt_slice_rotation_angle")) > 1e-9; + bool shear_or_scale_active = is_belt && ( + m_config->option>("belt_shear_x")->value != BeltShearMode::None || + m_config->option>("belt_shear_y")->value != BeltShearMode::None || + m_config->option>("belt_shear_z")->value != BeltShearMode::None || + m_config->option>("belt_scale_x")->value != BeltScaleMode::None || + m_config->option>("belt_scale_y")->value != BeltScaleMode::None || + m_config->option>("belt_scale_z")->value != BeltScaleMode::None); + bool allow_shear_scale = !rotation_active; + bool allow_rotation = !shear_or_scale_active; + + // Disable shear/scale mode dropdowns when rotation is active. + for (auto el : {"belt_shear_x", "belt_shear_y", "belt_shear_z", + "belt_scale_x", "belt_scale_y", "belt_scale_z", + "belt_mesh_transform_order"}) + toggle_option(el, is_belt && allow_shear_scale); + + // Disable rotation controls when any shear/scale is active. + toggle_option("belt_slice_rotation", is_belt && allow_rotation); + toggle_option("belt_slice_rotation_angle", is_belt && allow_rotation && rot_axis != BeltRotationAxis::None); + toggle_option("belt_slice_rotation_global", is_belt && allow_rotation && rot_axis != BeltRotationAxis::None); + // Gray out angle/from sub-options when their parent shear/scale mode is None. // Per-axis globals are superseded when belt_preslice_global is on. auto sx = m_config->option>("belt_shear_x")->value; - toggle_option("belt_shear_x_angle", is_belt && sx != BeltShearMode::None); - toggle_option("belt_shear_x_from", is_belt && sx != BeltShearMode::None); - toggle_option("belt_shear_x_global", is_belt && sx != BeltShearMode::None && !belt_global); + toggle_option("belt_shear_x_angle", is_belt && sx != BeltShearMode::None && allow_shear_scale); + toggle_option("belt_shear_x_from", is_belt && sx != BeltShearMode::None && allow_shear_scale); + toggle_option("belt_shear_x_global", is_belt && sx != BeltShearMode::None && !belt_global && allow_shear_scale); auto sy = m_config->option>("belt_shear_y")->value; - toggle_option("belt_shear_y_angle", is_belt && sy != BeltShearMode::None); - toggle_option("belt_shear_y_from", is_belt && sy != BeltShearMode::None); - toggle_option("belt_shear_y_global", is_belt && sy != BeltShearMode::None && !belt_global); + toggle_option("belt_shear_y_angle", is_belt && sy != BeltShearMode::None && allow_shear_scale); + toggle_option("belt_shear_y_from", is_belt && sy != BeltShearMode::None && allow_shear_scale); + toggle_option("belt_shear_y_global", is_belt && sy != BeltShearMode::None && !belt_global && allow_shear_scale); auto sz = m_config->option>("belt_shear_z")->value; - toggle_option("belt_shear_z_angle", is_belt && sz != BeltShearMode::None); - toggle_option("belt_shear_z_from", is_belt && sz != BeltShearMode::None); - toggle_option("belt_shear_z_global", is_belt && sz != BeltShearMode::None && !belt_global); + toggle_option("belt_shear_z_angle", is_belt && sz != BeltShearMode::None && allow_shear_scale); + toggle_option("belt_shear_z_from", is_belt && sz != BeltShearMode::None && allow_shear_scale); + toggle_option("belt_shear_z_global", is_belt && sz != BeltShearMode::None && !belt_global && allow_shear_scale); auto scx = m_config->option>("belt_scale_x")->value; - toggle_option("belt_scale_x_angle", is_belt && scx != BeltScaleMode::None); + toggle_option("belt_scale_x_angle", is_belt && scx != BeltScaleMode::None && allow_shear_scale); auto scy = m_config->option>("belt_scale_y")->value; - toggle_option("belt_scale_y_angle", is_belt && scy != BeltScaleMode::None); + toggle_option("belt_scale_y_angle", is_belt && scy != BeltScaleMode::None && allow_shear_scale); auto scz = m_config->option>("belt_scale_z")->value; - toggle_option("belt_scale_z_angle", is_belt && scz != BeltScaleMode::None); + toggle_option("belt_scale_z_angle", is_belt && scz != BeltScaleMode::None && allow_shear_scale); // Machine-frame transforms: shown only in belt mode. // Mirror the Advanced/Expert split used for mesh shear/scale. From 48b6db93b8e643655b8aaf11e36d707a46ddb656 Mon Sep 17 00:00:00 2001 From: Joseph Robertson <32232925+HarrierPigeon@users.noreply.github.com> Date: Wed, 27 May 2026 11:45:38 -0500 Subject: [PATCH 032/224] Belt/slice rotate (#33) * initial commit * fix upper bounds for assemblies * significantly less Z shift issues, still not quite tamped down yet though * add instrumentation to logs * finally found the issue * update printer defaults From 0f75d6bc4e28bbbe510a656018aeb7f8aab6bf32 Mon Sep 17 00:00:00 2001 From: Joseph Robertson <32232925+HarrierPigeon@users.noreply.github.com> Date: Wed, 27 May 2026 19:25:02 -0500 Subject: [PATCH 033/224] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/libslic3r/BeltGCodeWriter.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libslic3r/BeltGCodeWriter.cpp b/src/libslic3r/BeltGCodeWriter.cpp index b2a8f5c470..e0e4fb6f46 100644 --- a/src/libslic3r/BeltGCodeWriter.cpp +++ b/src/libslic3r/BeltGCodeWriter.cpp @@ -1,6 +1,7 @@ #include "BeltGCodeWriter.hpp" #include "FirstLayerPlane.hpp" #include "Geometry.hpp" +#include namespace Slic3r { From 2dc49002921aa2db525831a8f2325f69f705e4f9 Mon Sep 17 00:00:00 2001 From: Joseph Robertson <32232925+HarrierPigeon@users.noreply.github.com> Date: Wed, 27 May 2026 21:53:04 -0500 Subject: [PATCH 034/224] Copilot review fixes & upstream code interaction fix (#34) * first pass at review issue 8 * delete detritus * fix build compile error due to upstream changes --- result.json | 7 ------- src/libslic3r/Print.hpp | 8 ++++++++ src/libslic3r/PrintObject.cpp | 19 +++++++++++++------ src/libslic3r/PrintObjectSlice.cpp | 7 +++++++ src/slic3r/GUI/3DScene.cpp | 5 +---- 5 files changed, 29 insertions(+), 17 deletions(-) delete mode 100644 result.json diff --git a/result.json b/result.json deleted file mode 100644 index e309ad45a1..0000000000 --- a/result.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "error_string": "Success.", - "export_time": 0, - "plate_index": 0, - "prepare_time": 0, - "return_code": 0 -} diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index 98d1e6fa5c..269c407762 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -589,6 +589,14 @@ private: double m_belt_min_z { 0.0 }; // Belt printer: XY correction from global pre-slice mode, applied to G-code origin. Vec2d m_belt_global_xy_correction { Vec2d::Zero() }; + // Belt printer: exact belt_floor_z_shift computed during posSlice from a + // vertex-level scan of the post-transform mesh. Cached separately from + // m_slicing_params so that rebuilding m_slicing_params on a non-belt-affecting + // invalidation (e.g. support config change) doesn't replace the exact value + // with the bbox approximation seeded by create_from_config(). Cleared when + // posSlice is invalidated. + double m_belt_floor_z_shift_cached { 0.0 }; + bool m_belt_floor_z_shift_cache_valid { false }; public: double belt_global_z_offset() const { return m_belt_global_z_offset; } double belt_min_z() const { return m_belt_min_z; } diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 309f497412..c1d01c6c68 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1461,15 +1461,16 @@ bool PrintObject::invalidate_step(PrintObjectStep step) invalidated |= this->invalidate_steps({ posPerimeters, posPrepareInfill, posInfill, posIroning, posContouring, posSupportMaterial, posSimplifyPath, posSimplifyInfill }); invalidated |= m_print->invalidate_steps({ psSkirtBrim }); m_slicing_params.valid = false; + // The exact belt_floor_z_shift is recomputed when slice() runs again. + m_belt_floor_z_shift_cache_valid = false; } else if (step == posSupportMaterial) { invalidated |= this->invalidate_steps({ posSimplifySupportPath }); invalidated |= m_print->invalidate_steps({ psSkirtBrim }); - // NOTE: do NOT set m_slicing_params.valid = false here. - // belt_floor_z_shift is patched to an exact value during posSlice - // (PrintObjectSlice.cpp, after slice_volumes). Invalidating slicing - // params here causes update_slicing_parameters() to overwrite that - // exact value with a bounding-box approximation, while posSlice does - // not re-run to correct it — breaking belt support clipping. + // SlicingParameters depend on support config (enable_support / + // raft_layers / enforce_support_layers feed min/max layer height in + // Slicing.cpp), so invalidate them here. The vertex-scan + // belt_floor_z_shift is preserved via m_belt_floor_z_shift_cached. + m_slicing_params.valid = false; } // Wipe tower depends on the ordering of extruders, which in turn depends on everything. @@ -1487,6 +1488,7 @@ bool PrintObject::invalidate_all_steps() bool result = Inherited::invalidate_all_steps() | m_print->invalidate_all_steps(); // Then reset some of the depending values. m_slicing_params.valid = false; + m_belt_floor_z_shift_cache_valid = false; return result; } @@ -3654,6 +3656,11 @@ void PrintObject::update_slicing_parameters() m_slicing_params.belt_floor_shear_factor = belt_floor.shear_factor; m_slicing_params.belt_floor_from_axis = belt_floor.from_axis; m_slicing_params.belt_floor_z_shift = belt_floor.z_shift; + // Prefer the vertex-scan z_shift over the bbox approximation when + // slice() has already produced one (e.g. this rebuild was triggered + // by a support-config change, which doesn't move the belt floor). + if (m_belt_floor_z_shift_cache_valid) + m_slicing_params.belt_floor_z_shift = m_belt_floor_z_shift_cached; } } diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index bb5af8c342..deaf6fdfce 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -1164,6 +1164,13 @@ void PrintObject::slice() << " center_offset=(" << unscale(m_center_offset.x()) << ", " << unscale(m_center_offset.y()) << ")"; } + + // Cache the final patched belt_floor_z_shift so a later support-only + // invalidation can rebuild m_slicing_params without losing this exact + // (vertex-scan-derived) value. update_slicing_parameters() will + // restore it after create_from_config() seeds the bbox approximation. + m_belt_floor_z_shift_cached = m_slicing_params.belt_floor_z_shift; + m_belt_floor_z_shift_cache_valid = true; } } diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index bfa2b933a2..e9910a2eed 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -1117,10 +1117,7 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type, shader->set_uniform("print_volume.type", -1); } - bool enable_support; - int support_threshold_angle = get_selection_support_threshold_angle(enable_support); - - float normal_z = -::cos(Geometry::deg2rad((float) support_threshold_angle)); + const float normal_z = get_selection_support_normal_z(); // Compute up direction accounting for build plate tilt Vec3f up_direction = Vec3f::UnitZ(); From 0bda684dd7a7975726db70593cea1158b1f191ee Mon Sep 17 00:00:00 2001 From: Joseph Robertson <32232925+HarrierPigeon@users.noreply.github.com> Date: Sun, 31 May 2026 05:08:42 -0500 Subject: [PATCH 035/224] delete mesh transforms (#37) * delete mesh shear, scale and refactor logger * clean up config options * reorder UI elements --- .../Custom/machine/fdm_belt_common.json | 7 +- src/libslic3r/BeltGCode.cpp | 49 +--- src/libslic3r/BeltGCodeWriter.cpp | 7 +- src/libslic3r/BeltGCodeWriter.hpp | 3 - src/libslic3r/BeltSliceStrategy.cpp | 28 +- src/libslic3r/BeltSliceStrategy.hpp | 19 +- src/libslic3r/BeltTransform.cpp | 262 ++++-------------- src/libslic3r/BeltTransform.hpp | 72 ++--- src/libslic3r/FirstLayerPlane.cpp | 5 +- src/libslic3r/GCode.cpp | 12 +- src/libslic3r/GCode/BeltBackTransform.cpp | 7 +- src/libslic3r/GCode/GCodeProcessor.cpp | 79 +----- src/libslic3r/GCode/GCodeProcessor.hpp | 39 +-- src/libslic3r/GCode/MachineFrameTransform.cpp | 55 +--- src/libslic3r/GCode/MachineFrameTransform.hpp | 13 +- src/libslic3r/Preset.cpp | 7 +- src/libslic3r/Print.cpp | 35 +-- src/libslic3r/PrintApply.cpp | 6 +- src/libslic3r/PrintConfig.cpp | 239 +++++++--------- src/libslic3r/PrintConfig.hpp | 32 +-- src/libslic3r/PrintObjectSlice.cpp | 95 ++----- src/slic3r/GUI/3DBed.cpp | 25 +- src/slic3r/GUI/3DBed.hpp | 10 +- src/slic3r/GUI/Plater.cpp | 93 ++----- src/slic3r/GUI/Tab.cpp | 186 +++---------- 25 files changed, 377 insertions(+), 1008 deletions(-) diff --git a/resources/profiles/Custom/machine/fdm_belt_common.json b/resources/profiles/Custom/machine/fdm_belt_common.json index 0c3a976c0e..545ad15846 100644 --- a/resources/profiles/Custom/machine/fdm_belt_common.json +++ b/resources/profiles/Custom/machine/fdm_belt_common.json @@ -82,9 +82,9 @@ "z_hop_types": [ "Normal Lift" ], - "post_gcode_remap_x": "rev_x", - "post_gcode_remap_y": "pos_z", - "post_gcode_remap_z": "pos_y", + "gcode_remap_x": "rev_x", + "gcode_remap_y": "pos_z", + "gcode_remap_z": "pos_y", "printer_extruder_id": [ "1" ], @@ -94,7 +94,6 @@ "belt_slice_rotation_global": "1", "gcode_shear_z": "pos_tan", "gcode_scale_y": "inv_cos", - "build_plate_tilt_x": "45", "purge_in_prime_tower": "0", "scan_first_layer": "0", "auxiliary_fan": "0" diff --git a/src/libslic3r/BeltGCode.cpp b/src/libslic3r/BeltGCode.cpp index a89b20bcec..8c8c3d4392 100644 --- a/src/libslic3r/BeltGCode.cpp +++ b/src/libslic3r/BeltGCode.cpp @@ -14,7 +14,6 @@ void BeltGCode::init_belt_writer(Print &print, bool is_bbl_printers) auto belt_writer = std::make_unique(); belt_writer->set_is_bbl_machine(is_bbl_printers); - belt_writer->set_belt_angle(print.config().belt_printer_angle.value); // Axis remap and build volume max are set by base GCode after init_belt_writer returns. belt_writer->set_belt_back_transform(print.config()); belt_writer->set_machine_frame_transform(print.config()); @@ -34,30 +33,12 @@ void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print) if (!print.config().belt_printer.value) return; - file.write_format("; belt_printer_angle = %.1f\n", print.config().belt_printer_angle.value); - // Shear configs const auto &full_cfg = print.full_print_config(); - file.write_format("; belt_shear_x = %s\n", full_cfg.opt_serialize("belt_shear_x").c_str()); - file.write_format("; belt_shear_x_angle = %.1f\n", print.config().belt_shear_x_angle.value); - file.write_format("; belt_shear_x_from = %s\n", full_cfg.opt_serialize("belt_shear_x_from").c_str()); - file.write_format("; belt_shear_y = %s\n", full_cfg.opt_serialize("belt_shear_y").c_str()); - file.write_format("; belt_shear_y_angle = %.1f\n", print.config().belt_shear_y_angle.value); - file.write_format("; belt_shear_y_from = %s\n", full_cfg.opt_serialize("belt_shear_y_from").c_str()); - file.write_format("; belt_shear_z = %s\n", full_cfg.opt_serialize("belt_shear_z").c_str()); - file.write_format("; belt_shear_z_angle = %.1f\n", print.config().belt_shear_z_angle.value); - file.write_format("; belt_shear_z_from = %s\n", full_cfg.opt_serialize("belt_shear_z_from").c_str()); - // Scale configs - file.write_format("; belt_scale_x = %s\n", full_cfg.opt_serialize("belt_scale_x").c_str()); - file.write_format("; belt_scale_x_angle = %.1f\n", print.config().belt_scale_x_angle.value); - file.write_format("; belt_scale_y = %s\n", full_cfg.opt_serialize("belt_scale_y").c_str()); - file.write_format("; belt_scale_y_angle = %.1f\n", print.config().belt_scale_y_angle.value); - file.write_format("; belt_scale_z = %s\n", full_cfg.opt_serialize("belt_scale_z").c_str()); - file.write_format("; belt_scale_z_angle = %.1f\n", print.config().belt_scale_z_angle.value); - // Slicing rotation configs + // Slicing rotation: the belt tilt (axis + angle) and the single source of truth + // 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); - file.write_format("; belt_mesh_transform_order = %s\n", full_cfg.opt_serialize("belt_mesh_transform_order").c_str()); // 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()); @@ -81,9 +62,6 @@ void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print) file.write_format("; gcode_scale_z = %s\n", full_cfg.opt_serialize("gcode_scale_z").c_str()); file.write_format("; gcode_scale_z_angle = %.1f\n", print.config().gcode_scale_z_angle.value); file.write_format("; belt_gcode_transform_order = %s\n", full_cfg.opt_serialize("belt_gcode_transform_order").c_str()); - file.write_format("; post_gcode_remap_x = %s\n", full_cfg.opt_serialize("post_gcode_remap_x").c_str()); - file.write_format("; post_gcode_remap_y = %s\n", full_cfg.opt_serialize("post_gcode_remap_y").c_str()); - file.write_format("; post_gcode_remap_z = %s\n", full_cfg.opt_serialize("post_gcode_remap_z").c_str()); } void BeltGCode::on_set_origin(const PrintObject *obj, const Point &inst_shift) @@ -94,21 +72,16 @@ void BeltGCode::on_set_origin(const PrintObject *obj, const Point &inst_shift) // This replaces the bbox-based axis snap with an exact formula. // // Flags that trigger this path: - // belt_preslice_global — full pipeline (scale * shear * remap) is global - // preslice_remap_global — only the pre-slice remap is global - // belt_shear_z_global — Z-row shear treated as global (matches per-axis - // Z-offset added in PrintObjectSlice.cpp) + // 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 either way, // because the back_transform applied during G-code emission is always the - // inverse of the full pipeline. Without pre-multiplication under - // ShearThenScale order with sy != 1, machine_y of bed position cy ends up - // at cy/sy instead of cy, which also leaves the object bottom off the belt - // plane. + // 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_shear_z_global.value - && m_config.belt_shear_z.value != BeltShearMode::None) || (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); @@ -117,10 +90,10 @@ void BeltGCode::on_set_origin(const PrintObject *obj, const Point &inst_shift) if (belt_writer) { // The per-object lift (z_shift_val = max(0, -m_belt_min_z)) added by // BeltSliceStrategy::apply_to_trafo is already compensated inside - // global_z_offset (via the shear_min_z term in PrintObjectSlice.cpp's - // preslice_global branch). Snap was previously used here for the same - // purpose, but with both active the lift gets subtracted twice. Clear - // any leftover snap state from a prior instance. + // global_z_offset (via the belt_z_shift term in PrintObjectSlice.cpp). + // Snap was previously used here for the same purpose, but with both + // active the lift gets subtracted twice. Clear any leftover snap + // state from a prior instance. for (int a = 0; a < 3; ++a) belt_writer->set_origin_snap(a, false, 0., 0.); } diff --git a/src/libslic3r/BeltGCodeWriter.cpp b/src/libslic3r/BeltGCodeWriter.cpp index e0e4fb6f46..751a7f19d8 100644 --- a/src/libslic3r/BeltGCodeWriter.cpp +++ b/src/libslic3r/BeltGCodeWriter.cpp @@ -25,11 +25,6 @@ inline bool belt_point_on_first_layer( // ---- Belt configuration --------------------------------------------------- -void BeltGCodeWriter::set_belt_angle(double angle_deg) -{ - m_belt_angle_rad = Geometry::deg2rad(angle_deg); -} - void BeltGCodeWriter::set_belt_back_transform(const PrintConfig &config) { m_belt_back_transform.init_from_config(config); @@ -66,7 +61,7 @@ Vec3d BeltGCodeWriter::to_machine_coords(const Vec3d &pos) const if (m_origin_snap[i]) result[i] -= (m_origin_bbox_min[i] - m_origin_offset[i]); Vec3d after_snap = result; - // Step 4: Machine-frame transform (gcode_shear / gcode_scale / post_gcode_remap) + // Step 4: Machine-frame transform (gcode_shear / gcode_scale) // applied LAST so it acts as a global linear transform on the placed coords. // Order matters: putting it before origin_snap would feed sheared bbox corners // into the snap's per-object min calculation, mis-normalizing non-cubic geometries diff --git a/src/libslic3r/BeltGCodeWriter.hpp b/src/libslic3r/BeltGCodeWriter.hpp index 60fd93f3e0..e9d4af0f3a 100644 --- a/src/libslic3r/BeltGCodeWriter.hpp +++ b/src/libslic3r/BeltGCodeWriter.hpp @@ -19,8 +19,6 @@ public: BeltGCodeWriter() : GCodeWriter() {} // Belt configuration (axis remap is inherited from GCodeWriter) - void set_belt_angle(double angle_deg); - bool is_belt_printer() const { return m_belt_angle_rad != 0.; } void set_belt_back_transform(const PrintConfig &config); void set_machine_frame_transform(const PrintConfig &config); void set_origin_snap(int axis, bool enable, double offset, double bbox_min); @@ -52,7 +50,6 @@ protected: std::string _travel_to_z(double z, const std::string &comment) override; private: - double m_belt_angle_rad = 0.; BeltBackTransform m_belt_back_transform; MachineFrameTransform m_machine_frame_transform; bool m_origin_snap[3] = {false, false, false}; diff --git a/src/libslic3r/BeltSliceStrategy.cpp b/src/libslic3r/BeltSliceStrategy.cpp index 81c4c06f1e..ddaabdef0b 100644 --- a/src/libslic3r/BeltSliceStrategy.cpp +++ b/src/libslic3r/BeltSliceStrategy.cpp @@ -17,10 +17,7 @@ std::unique_ptr BeltSliceStrategy::create(const PrintConfig & BeltSliceStrategy::BeltSliceStrategy(const PrintConfig &config) { - m_shear = BeltTransformPipeline::build_shear_matrix(config, &m_has_shear); - m_scale = BeltTransformPipeline::build_scale_matrix(config, &m_has_scale); m_rotation = BeltTransformPipeline::build_rotation_matrix(config, &m_has_rotation); - m_order = config.belt_mesh_transform_order.value; } void BeltSliceStrategy::apply_to_trafo(Transform3d &trafo, @@ -28,15 +25,10 @@ void BeltSliceStrategy::apply_to_trafo(Transform3d &trafo, bool has_remap, double *out_belt_min_z) const { - // ScaleThenShear: applied to a point, scale runs first then shear (m_shear * m_scale). - // ShearThenScale: applied to a point, shear runs first then scale (m_scale * m_shear). - // Rotation (if active) is applied AFTER shear/scale, matching build_forward_transform. - if (m_has_shear || m_has_scale || m_has_rotation) { - Matrix3d shear_scale = (m_order == BeltTransformOrder::ScaleThenShear) - ? Matrix3d(m_shear * m_scale) - : Matrix3d(m_scale * m_shear); + // Rotation is the only mesh-side belt transform (matching build_forward_transform). + if (m_has_rotation) { Transform3d belt_xform = Transform3d::Identity(); - belt_xform.linear() = Matrix3d(m_rotation * shear_scale); + belt_xform.linear() = m_rotation; trafo = belt_xform * trafo; } @@ -47,7 +39,7 @@ void BeltSliceStrategy::apply_to_trafo(Transform3d &trafo, // within the object) causes min_z to be computed against mesh-local vertex // coordinates rather than object-space coordinates, so volumes translated // along the slicer's Z axis are silently excluded from the bound check. - if (has_remap || m_has_shear || m_has_scale || m_has_rotation) { + if (has_remap || m_has_rotation) { // [BELT-DEBUG] Capture the incoming trafo for diagnostic logging. // This is the slicer-frame transform AFTER belt_xform but BEFORE z_shift. const Transform3d trafo_pre_shift = trafo; @@ -65,9 +57,7 @@ void BeltSliceStrategy::apply_to_trafo(Transform3d &trafo, ss << "(" << v.x() << "," << v.y() << "," << v.z() << ")"; return ss.str(); }; - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] apply_to_trafo enter" - << " has_shear=" << m_has_shear - << " has_scale=" << m_has_scale + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] apply_to_trafo enter" << " has_rotation=" << m_has_rotation << " has_remap=" << has_remap << " trafo.linear=" << log_mat(trafo_pre_shift.linear()) @@ -96,7 +86,7 @@ void BeltSliceStrategy::apply_to_trafo(Transform3d &trafo, vol_min_z = std::min(vol_min_z, pt.z()); min_z = std::min(min_z, pt.z()); } - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] vol[" << vol_idx + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] vol[" << vol_idx << "] id=" << mv->id().id << " name='" << mv->name << "'" << " mesh_bbox_min=" << log_vec3(mesh_min) << " mesh_bbox_max=" << log_vec3(mesh_max) << " get_matrix.translation=" << log_vec3(mv->get_matrix().translation()) @@ -105,7 +95,7 @@ void BeltSliceStrategy::apply_to_trafo(Transform3d &trafo, ++vol_idx; } double belt_z_shift_val = (min_z < 0. && min_z != std::numeric_limits::max()) ? -min_z : 0.; - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] combined min_z=" << min_z + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] combined min_z=" << min_z << " z_shift_val=" << belt_z_shift_val; if (belt_z_shift_val > 0.) { Transform3d z_shift = Transform3d::Identity(); @@ -114,11 +104,11 @@ void BeltSliceStrategy::apply_to_trafo(Transform3d &trafo, } if (out_belt_min_z) { double new_val = (min_z != std::numeric_limits::max()) ? min_z : 0.; - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] write m_belt_min_z tid=" << std::this_thread::get_id() + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] write m_belt_min_z tid=" << std::this_thread::get_id() << " target=" << out_belt_min_z << " old=" << *out_belt_min_z << " new=" << new_val; *out_belt_min_z = new_val; } - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] apply_to_trafo exit" + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] apply_to_trafo exit" << " final_trafo.linear=" << log_mat(trafo.linear()) << " final_trafo.translation=" << log_vec3(trafo.translation()); } diff --git a/src/libslic3r/BeltSliceStrategy.hpp b/src/libslic3r/BeltSliceStrategy.hpp index 6009155c7b..ff56f6fece 100644 --- a/src/libslic3r/BeltSliceStrategy.hpp +++ b/src/libslic3r/BeltSliceStrategy.hpp @@ -13,17 +13,17 @@ namespace Slic3r { // Belt printer pre-slice transform strategy. // -// Encapsulates the pre-remap, shear, scale, and Z-shift transforms -// that are applied to model geometry before slicing on belt printers. -// Used by PrintObjectSlice.cpp to isolate belt-specific logic from -// the slicing pipeline. +// Encapsulates the pre-remap, rotation, and Z-shift transforms that are +// applied to model geometry before slicing on belt printers. (Shear & scale +// are applied to the g-code, not the mesh.) Used by PrintObjectSlice.cpp to +// isolate belt-specific logic from the slicing pipeline. class BeltSliceStrategy { public: // Create a strategy if belt_printer is enabled; returns nullptr otherwise. static std::unique_ptr create(const PrintConfig &config); - // Apply belt-specific transforms (shear + scale + z-shift) to the slicing trafo. + // Apply belt-specific transforms (rotation + z-shift) to the slicing trafo. // Pre-slice remap is handled separately (standalone feature). // has_remap: whether pre-slice remap was already applied (affects z-shift detection). void apply_to_trafo(Transform3d &trafo, @@ -34,13 +34,8 @@ public: private: explicit BeltSliceStrategy(const PrintConfig &config); - bool m_has_shear = false; - bool m_has_scale = false; - bool m_has_rotation = false; - Matrix3d m_shear = Matrix3d::Identity(); - Matrix3d m_scale = Matrix3d::Identity(); - Matrix3d m_rotation = Matrix3d::Identity(); - BeltTransformOrder m_order = BeltTransformOrder::ScaleThenShear; + bool m_has_rotation = false; + Matrix3d m_rotation = Matrix3d::Identity(); }; } // namespace Slic3r diff --git a/src/libslic3r/BeltTransform.cpp b/src/libslic3r/BeltTransform.cpp index 0d1ae41295..db23e13417 100644 --- a/src/libslic3r/BeltTransform.cpp +++ b/src/libslic3r/BeltTransform.cpp @@ -51,50 +51,6 @@ Transform3d BeltTransformPipeline::build_preslice_remap(const PrintConfig &confi return pre_remap; } -Matrix3d BeltTransformPipeline::build_shear_matrix(const PrintConfig &config, bool *has_shear_out) -{ - struct AxisShear { BeltShearMode mode; double angle; int from; }; - AxisShear axes[3] = { - { config.belt_shear_x.value, config.belt_shear_x_angle.value, int(config.belt_shear_x_from.value) }, - { config.belt_shear_y.value, config.belt_shear_y_angle.value, int(config.belt_shear_y_from.value) }, - { config.belt_shear_z.value, config.belt_shear_z_angle.value, int(config.belt_shear_z_from.value) }, - }; - - Matrix3d shear = Matrix3d::Identity(); - bool active = false; - for (int row = 0; row < 3; ++row) { - if (axes[row].mode != BeltShearMode::None) { - double factor = compute_shear_factor(axes[row].mode, axes[row].angle); - if (std::abs(factor) > EPSILON) { - shear(row, axes[row].from) += factor; - active = true; - } - } - } - if (has_shear_out) *has_shear_out = active; - return shear; -} - -Matrix3d BeltTransformPipeline::build_scale_matrix(const PrintConfig &config, bool *has_scale_out) -{ - double sx = compute_scale_factor(config.belt_scale_x.value, config.belt_scale_x_angle.value); - double sy = compute_scale_factor(config.belt_scale_y.value, config.belt_scale_y_angle.value); - double sz = compute_scale_factor(config.belt_scale_z.value, config.belt_scale_z_angle.value); - - bool active = (std::abs(sx - 1.) > EPSILON || - std::abs(sy - 1.) > EPSILON || - std::abs(sz - 1.) > EPSILON); - - Matrix3d scale = Matrix3d::Identity(); - if (active) { - scale(0, 0) = sx; - scale(1, 1) = sy; - scale(2, 2) = sz; - } - if (has_scale_out) *has_scale_out = active; - return scale; -} - Matrix3d BeltTransformPipeline::build_rotation_matrix(const PrintConfig &config, bool *has_rot_out) { BeltRotationAxis axis = config.belt_slice_rotation.value; @@ -116,26 +72,13 @@ Matrix3d BeltTransformPipeline::build_rotation_matrix(const PrintConfig &config, Transform3d BeltTransformPipeline::build_forward_transform(const PrintConfig &config) { - Transform3d pre_remap = build_preslice_remap(config); - bool shear_active = false; - Matrix3d shear = build_shear_matrix(config, &shear_active); - bool scale_active = false; - Matrix3d scale = build_scale_matrix(config, &scale_active); - bool rot_active = false; - Matrix3d rot = build_rotation_matrix(config, &rot_active); - - // Match the mesh-side ordering selected by belt_mesh_transform_order so - // BeltBackTransform inverts the same composition that BeltSliceStrategy - // applied to the mesh. - // ScaleThenShear: applied to p, scale runs first then shear (shear * scale). - // ShearThenScale: applied to p, shear runs first then scale (scale * shear). - // Rotation is applied AFTER shear/scale: rot * shear_scale * pre_remap. - Matrix3d shear_scale = (config.belt_mesh_transform_order.value == BeltTransformOrder::ScaleThenShear) - ? Matrix3d(shear * scale) - : Matrix3d(scale * shear); + // 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); Transform3d combined = Transform3d::Identity(); - combined.linear() = Matrix3d(rot * shear_scale); + combined.linear() = rot; combined = combined * pre_remap; return combined; } @@ -190,166 +133,75 @@ BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl( BeltTransformPipeline::BeltHeightResult result; result.object_height = original_height; - // Extract Z-axis shear/scale + per-axis scale + transform order + rotation from config. - BeltShearMode z_shear_mode; - double z_shear_angle; - BeltScaleMode z_scale_mode; - double z_scale_angle; - int z_shear_from; - BeltScaleMode from_scale_mode; // scale on the shear's source axis - double from_scale_angle; - BeltTransformOrder order; - BeltRotationAxis rot_axis; - double rot_angle; + // Extract the mesh rotation from config (the sole mesh-side belt transform). + BeltRotationAxis rot_axis; + double rot_angle; if constexpr (std::is_same_v) { - z_shear_mode = config.belt_shear_z.value; - z_shear_angle = config.belt_shear_z_angle.value; - z_scale_mode = config.belt_scale_z.value; - z_scale_angle = config.belt_scale_z_angle.value; - z_shear_from = int(config.belt_shear_z_from.value); - order = config.belt_mesh_transform_order.value; - rot_axis = config.belt_slice_rotation.value; - rot_angle = config.belt_slice_rotation_angle.value; - if (z_shear_from == 0) { - from_scale_mode = config.belt_scale_x.value; - from_scale_angle = config.belt_scale_x_angle.value; - } else { - from_scale_mode = config.belt_scale_y.value; - from_scale_angle = config.belt_scale_y_angle.value; - } + rot_axis = config.belt_slice_rotation.value; + rot_angle = config.belt_slice_rotation_angle.value; } else { // DynamicPrintConfig path - auto get_shear = [&](const char *key) { - auto *opt = config.template option>(key); - return opt ? opt->value : BeltShearMode::None; - }; - auto get_scale = [&](const char *key) { - auto *opt = config.template option>(key); - return opt ? opt->value : BeltScaleMode::None; - }; auto get_float = [&](const char *key) { auto *opt = config.template option(key); - return opt ? opt->value : 45.0; - }; - auto get_axis = [&](const char *key) { - auto *opt = config.template option>(key); - return opt ? int(opt->value) : 1; - }; - auto get_order = [&](const char *key) { - auto *opt = config.template option>(key); - return opt ? opt->value : BeltTransformOrder::ScaleThenShear; + return opt ? opt->value : 0.0; }; auto get_rot_axis = [&](const char *key) { auto *opt = config.template option>(key); return opt ? opt->value : BeltRotationAxis::None; }; - z_shear_mode = get_shear("belt_shear_z"); - z_shear_angle = get_float("belt_shear_z_angle"); - z_scale_mode = get_scale("belt_scale_z"); - z_scale_angle = get_float("belt_scale_z_angle"); - z_shear_from = get_axis("belt_shear_z_from"); - order = get_order("belt_mesh_transform_order"); - rot_axis = get_rot_axis("belt_slice_rotation"); - rot_angle = get_float("belt_slice_rotation_angle"); - if (z_shear_from == 0) { - from_scale_mode = get_scale("belt_scale_x"); - from_scale_angle = get_float("belt_scale_x_angle"); - } else { - from_scale_mode = get_scale("belt_scale_y"); - from_scale_angle = get_float("belt_scale_y_angle"); - } + rot_axis = get_rot_axis("belt_slice_rotation"); + rot_angle = get_float("belt_slice_rotation_angle"); } - bool has_z_shear = z_shear_mode != BeltShearMode::None; - bool has_z_scale = z_scale_mode != BeltScaleMode::None; bool has_rotation = rot_axis != BeltRotationAxis::None && std::abs(rot_angle) > EPSILON; - - if (!has_z_shear && !has_z_scale && !has_rotation) + if (!has_rotation) return result; - double shear_factor = has_z_shear - ? BeltTransformPipeline::compute_shear_factor(z_shear_mode, z_shear_angle) : 0.; - double scale_z = BeltTransformPipeline::compute_scale_factor(z_scale_mode, z_scale_angle); - double scale_from = BeltTransformPipeline::compute_scale_factor(from_scale_mode, from_scale_angle); - - if (has_z_shear && std::abs(shear_factor) > EPSILON) { - int from = z_shear_from; - double min_rz = std::numeric_limits::max(); - double max_rz = std::numeric_limits::lowest(); - for (double vz : {bb.min.z(), bb.max.z()}) - for (double vs : {bb.min(from), bb.max(from)}) { - // Mesh-frame new_z computed per ordering. - // scale-then-shear: Z_s = sz*Z_m + s_from*tan(α)*from_m - // shear-then-scale: Z_s = sz*(Z_m + tan(α)*from_m) - double new_z = (order == BeltTransformOrder::ScaleThenShear) - ? scale_z * vz + scale_from * shear_factor * vs - : scale_z * (vz + shear_factor * vs); - min_rz = std::min(min_rz, new_z); - max_rz = std::max(max_rz, new_z); - } - result.object_height = max_rz - min_rz; - // Effective slicer-frame slope of the belt surface (Z_m=0 line): - // scale-then-shear: Z_s = tan(α) * Y_s → slope = tan(α) - // shear-then-scale: Z_s = sz/s_from * tan(α) * Y_s → slope = sz*tan(α)/s_from - // The downstream cutoff formula `Y_s = (print_z - z_shift) / slope` - // and floor_print_z(Y_s) = slope * Y_s + z_shift use this slope. - double effective_shear = (order == BeltTransformOrder::ScaleThenShear) - ? shear_factor - : (std::abs(scale_from) > EPSILON - ? scale_z * shear_factor / scale_from - : shear_factor); - result.floor_params.shear_factor = effective_shear; - result.floor_params.from_axis = from; - result.floor_params.z_shift = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.); - } else if (has_rotation) { - // Rotation-only path (no Z-shear): sweep 8 bbox corners through R. - double angle_rad = Geometry::deg2rad(rot_angle); - Vec3d unit_axis; - switch (rot_axis) { - case BeltRotationAxis::X: unit_axis = Vec3d::UnitX(); break; - case BeltRotationAxis::Y: unit_axis = Vec3d::UnitY(); break; - case BeltRotationAxis::Z: unit_axis = Vec3d::UnitZ(); break; - default: unit_axis = Vec3d::UnitX(); break; - } - Matrix3d R = Eigen::AngleAxisd(angle_rad, unit_axis).toRotationMatrix(); - double min_rz = std::numeric_limits::max(); - double max_rz = std::numeric_limits::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(); - min_rz = std::min(min_rz, z); - max_rz = std::max(max_rz, z); - } - // Optional Z-scale still applies multiplicatively if both are set. - result.object_height = (max_rz - min_rz) * (has_z_scale ? scale_z : 1.0); - - // 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.); - } else { - result.object_height = original_height * scale_z; + // Rotation path: sweep the 8 bbox corners through R to get the rotated height, + // then derive the belt floor (the image of machine-Z = 0 under R). + double angle_rad = Geometry::deg2rad(rot_angle); + Vec3d unit_axis; + switch (rot_axis) { + case BeltRotationAxis::X: unit_axis = Vec3d::UnitX(); break; + case BeltRotationAxis::Y: unit_axis = Vec3d::UnitY(); break; + case BeltRotationAxis::Z: unit_axis = Vec3d::UnitZ(); break; + default: unit_axis = Vec3d::UnitX(); break; } + Matrix3d R = Eigen::AngleAxisd(angle_rad, unit_axis).toRotationMatrix(); + double min_rz = std::numeric_limits::max(); + double max_rz = std::numeric_limits::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(); + min_rz = std::min(min_rz, z); + max_rz = std::max(max_rz, z); + } + 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; } diff --git a/src/libslic3r/BeltTransform.hpp b/src/libslic3r/BeltTransform.hpp index 1ce47a2e31..50f56bd75f 100644 --- a/src/libslic3r/BeltTransform.hpp +++ b/src/libslic3r/BeltTransform.hpp @@ -15,11 +15,16 @@ class ModelObject; // Shared belt-printer transform math. // // The pre-slice pipeline applied in PrintObjectSlice.cpp is: -// trafo_out = z_shift * scale * shear * pre_remap * trafo_in +// trafo_out = z_shift * rotation * pre_remap * trafo_in // -// This class provides the building blocks so every call site uses the -// same implementation. z_shift is object-dependent (computed from mesh -// vertex bounds) and is NOT included in build_forward_transform(). +// Rotation is the sole mesh-side belt transform; shear & scale are applied +// to the g-code instead (see MachineFrameTransform). This class provides the +// building blocks so every call site uses the same implementation. z_shift is +// object-dependent (computed from mesh vertex bounds) and is NOT included in +// build_forward_transform(). +// +// The compute_shear_factor / compute_scale_factor math helpers below are still +// used by the g-code-side machine-frame shear/scale. class BeltTransformPipeline { public: @@ -75,50 +80,53 @@ public: get_int("preslice_remap_z") != int(RemapAxis::PosZ); } - static bool has_shear(const PrintConfig &config) - { - return config.belt_shear_x.value != BeltShearMode::None || - config.belt_shear_y.value != BeltShearMode::None || - config.belt_shear_z.value != BeltShearMode::None; - } - - static bool has_scale(const PrintConfig &config) - { - double sx = compute_scale_factor(config.belt_scale_x.value, config.belt_scale_x_angle.value); - double sy = compute_scale_factor(config.belt_scale_y.value, config.belt_scale_y_angle.value); - double sz = compute_scale_factor(config.belt_scale_z.value, config.belt_scale_z_angle.value); - return std::abs(sx - 1.) > EPSILON || - std::abs(sy - 1.) > EPSILON || - std::abs(sz - 1.) > EPSILON; - } - static bool has_rotation(const PrintConfig &config) { return config.belt_slice_rotation.value != BeltRotationAxis::None && std::abs(config.belt_slice_rotation_angle.value) > EPSILON; } + // Physical belt tilt derived from the slicing rotation — the single source of + // truth for bed rendering, support gravity tilt and the bed-exclusion + // projection. Returns the tilt magnitude in degrees split onto the X and Y + // build-plate tilt axes according to the rotation axis: + // rotation about X → tilt_x = angle (gantry tilts in the YZ plane) + // rotation about Y → tilt_y = angle (gantry tilts in the XZ plane) + // rotation about Z / None → no tilt (in-plane spin doesn't tilt the belt) + // The magnitude uses abs(angle) so a negative rotation still reports a positive + // physical tilt. + struct PhysicalTilt { double tilt_x_deg = 0.; double tilt_y_deg = 0.; }; + + static PhysicalTilt physical_tilt(BeltRotationAxis axis, double angle_deg) + { + PhysicalTilt t; + double mag = std::abs(angle_deg); + switch (axis) { + case BeltRotationAxis::X: t.tilt_x_deg = mag; break; + case BeltRotationAxis::Y: t.tilt_y_deg = mag; break; + default: break; // Z / None: no physical tilt + } + return t; + } + + static PhysicalTilt physical_tilt(const PrintConfig &config) + { + return physical_tilt(config.belt_slice_rotation.value, + config.belt_slice_rotation_angle.value); + } + // ---- Matrix builders -------------------------------------------------- // Build the pre-slice axis remap transform (includes Rev-mode translation). static Transform3d build_preslice_remap(const PrintConfig &config); - // Build the 3x3 shear matrix. Returns Identity if no shear is active. - // Also sets has_shear_out if non-null. - static Matrix3d build_shear_matrix(const PrintConfig &config, bool *has_shear_out = nullptr); - - // Build the 3x3 diagonal scale matrix. Returns Identity if no scale. - // Also sets has_scale_out if non-null. - static Matrix3d build_scale_matrix(const PrintConfig &config, bool *has_scale_out = nullptr); - // 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. Shear/scale order is selected by - // belt_mesh_transform_order so the result matches what BeltSliceStrategy - // applied to the mesh (BeltBackTransform inverts this). + // Combined forward transform (rotation * pre_remap) — 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); diff --git a/src/libslic3r/FirstLayerPlane.cpp b/src/libslic3r/FirstLayerPlane.cpp index 10866c57f5..6a4feba886 100644 --- a/src/libslic3r/FirstLayerPlane.cpp +++ b/src/libslic3r/FirstLayerPlane.cpp @@ -85,9 +85,8 @@ FirstLayerPlane::FirstLayerPlane(const PrintConfig &config) FirstLayerPlaneMode mode = config.first_layer_plane.value; if (mode == FirstLayerPlaneMode::Auto) { bool belt_affine_active = config.belt_printer.value && - (config.belt_shear_z.value != BeltShearMode::None || - (config.belt_slice_rotation.value != BeltRotationAxis::None && - std::abs(config.belt_slice_rotation_angle.value) > EPSILON)); + config.belt_slice_rotation.value != BeltRotationAxis::None && + std::abs(config.belt_slice_rotation_angle.value) > EPSILON; mode = belt_affine_active ? FirstLayerPlaneMode::BeltAffine : FirstLayerPlaneMode::XY; } diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 047d00250c..2ad11a710d 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -6520,14 +6520,14 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, auto _mm3_per_mm = path.mm3_per_mm * this->config().print_flow_ratio; _mm3_per_mm *= filament_flow_ratio; - // Belt printer: compensate for the volume change introduced by the mesh + // Belt printer: compensate for any volume change introduced by the mesh // forward transform. path.mm3_per_mm is derived from slicer-frame layer // height × line width, but a slicer-frame slab of volume V maps under the - // back-transform to a machine-frame region of volume V / |det(T)|. When - // det(T) > 1 — e.g. belt_scale_y = 1/cos(α) ≈ 1.155 at α = 30° — the - // machine slab holds less plastic than the slicer thinks it is filling - // and we over-extrude by det(T). Pure shear has det 1, so this is a - // no-op until scale factors deviate from 1. + // back-transform to a machine-frame region of volume V / |det(T)|. The + // mesh transform is now rotation ∘ pre-remap, both orthogonal, so |det(T)| + // is always 1 and this is currently a no-op; it is retained as a guard in + // case a non-orthogonal mesh transform is ever reintroduced. (Machine-frame + // shear/scale acts on the g-code in BeltGCodeWriter, not here.) if (m_config.belt_printer.value) { double det = std::abs(BeltTransformPipeline::build_forward_transform(m_config).linear().determinant()); if (det > EPSILON) diff --git a/src/libslic3r/GCode/BeltBackTransform.cpp b/src/libslic3r/GCode/BeltBackTransform.cpp index 654afe6262..6a25bbd408 100644 --- a/src/libslic3r/GCode/BeltBackTransform.cpp +++ b/src/libslic3r/GCode/BeltBackTransform.cpp @@ -12,18 +12,15 @@ bool BeltBackTransform::init_from_config(const PrintConfig &config) return false; // Require at least one active transform to proceed. - bool has_global_shear = config.belt_shear_x_global.value || - config.belt_shear_y_global.value || - config.belt_shear_z_global.value; 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_shear && !has_global_rotation && !has_preslice_global + if (!has_global_rotation && !has_preslice_global && !BeltTransformPipeline::has_preslice_remap(config)) return false; - // Build the forward pipeline (scale * shear * pre_remap) and store its inverse. + // Build the forward pipeline (rotation * pre_remap) and store its inverse. Transform3d forward = BeltTransformPipeline::build_forward_transform(config); if (forward.isApprox(Transform3d::Identity())) return false; diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index d9fc8bc992..7df84ade84 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -3072,90 +3072,21 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers return; } - // Belt printer angle detection from G-code header comment. - if (boost::starts_with(comment, " belt_printer_angle = ")) { + // Belt printer: derive the physical tilt magnitude from the slicing-rotation + // angle header comment (used to enable the preview's belt view). + if (boost::starts_with(comment, " belt_slice_rotation_angle = ")) { try { - m_result.belt_printer_angle = std::stof(std::string(comment.substr(22))); + m_result.belt_tilt_angle = std::abs(std::stof(std::string(comment.substr(29)))); } catch (...) {} return; } - // Belt printer: parse shear configs from header comments. + // Belt printer: parse pre-slice axis remap from header comments. { - auto parse_shear_mode = [](const std::string &s) -> BeltShearMode { - if (s == "pos_cot") return BeltShearMode::PosCot; - if (s == "neg_cot") return BeltShearMode::NegCot; - if (s == "pos_tan") return BeltShearMode::PosTan; - if (s == "neg_tan") return BeltShearMode::NegTan; - return BeltShearMode::None; - }; - auto parse_axis = [](const std::string &s) -> BeltAxis { - if (s == "y") return BeltAxis::Y; - if (s == "z") return BeltAxis::Z; - return BeltAxis::X; - }; - auto parse_scale_mode = [](const std::string &s) -> BeltScaleMode { - if (s == "inv_sin") return BeltScaleMode::InvSin; - if (s == "inv_cos") return BeltScaleMode::InvCos; - if (s == "sin") return BeltScaleMode::Sin; - if (s == "cos") return BeltScaleMode::Cos; - return BeltScaleMode::None; - }; 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); }; - // Shear X - if (boost::starts_with(comment, " belt_shear_x = ")) { - m_result.belt_shear_x = parse_shear_mode(trim(std::string(comment.substr(16)))); return; - } - if (boost::starts_with(comment, " belt_shear_x_angle = ")) { - try { m_result.belt_shear_x_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; - } - if (boost::starts_with(comment, " belt_shear_x_from = ")) { - m_result.belt_shear_x_from = parse_axis(trim(std::string(comment.substr(21)))); return; - } - // Shear Y - if (boost::starts_with(comment, " belt_shear_y = ")) { - m_result.belt_shear_y = parse_shear_mode(trim(std::string(comment.substr(16)))); return; - } - if (boost::starts_with(comment, " belt_shear_y_angle = ")) { - try { m_result.belt_shear_y_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; - } - if (boost::starts_with(comment, " belt_shear_y_from = ")) { - m_result.belt_shear_y_from = parse_axis(trim(std::string(comment.substr(21)))); return; - } - // Shear Z - if (boost::starts_with(comment, " belt_shear_z = ")) { - m_result.belt_shear_z = parse_shear_mode(trim(std::string(comment.substr(16)))); return; - } - if (boost::starts_with(comment, " belt_shear_z_angle = ")) { - try { m_result.belt_shear_z_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; - } - if (boost::starts_with(comment, " belt_shear_z_from = ")) { - m_result.belt_shear_z_from = parse_axis(trim(std::string(comment.substr(21)))); return; - } - // Scale X - if (boost::starts_with(comment, " belt_scale_x = ")) { - m_result.belt_scale_x = parse_scale_mode(trim(std::string(comment.substr(16)))); return; - } - if (boost::starts_with(comment, " belt_scale_x_angle = ")) { - try { m_result.belt_scale_x_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; - } - // Scale Y - if (boost::starts_with(comment, " belt_scale_y = ")) { - m_result.belt_scale_y = parse_scale_mode(trim(std::string(comment.substr(16)))); return; - } - if (boost::starts_with(comment, " belt_scale_y_angle = ")) { - try { m_result.belt_scale_y_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; - } - // Scale Z - if (boost::starts_with(comment, " belt_scale_z = ")) { - m_result.belt_scale_z = parse_scale_mode(trim(std::string(comment.substr(16)))); return; - } - if (boost::starts_with(comment, " belt_scale_z_angle = ")) { - try { m_result.belt_scale_z_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; - } // Pre-slice axis remap auto parse_remap_axis = [](const std::string &s) -> RemapAxis { if (s == "pos_x") return RemapAxis::PosX; diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index 4e5bb5b746..3047c005ed 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -245,25 +245,9 @@ class Print; bool support_traditional_timelapse{true}; float printable_height; float z_offset; - // Belt printer: angle for coordinate transformation in preview. - float belt_printer_angle{ 0.f }; - // Belt printer: per-axis shear config. - BeltShearMode belt_shear_x{ BeltShearMode::None }; - float belt_shear_x_angle{ 45.f }; - BeltAxis belt_shear_x_from{ BeltAxis::Y }; - BeltShearMode belt_shear_y{ BeltShearMode::None }; - float belt_shear_y_angle{ 45.f }; - BeltAxis belt_shear_y_from{ BeltAxis::Y }; - BeltShearMode belt_shear_z{ BeltShearMode::None }; - float belt_shear_z_angle{ 45.f }; - BeltAxis belt_shear_z_from{ BeltAxis::Y }; - // Belt printer: per-axis scale config. - BeltScaleMode belt_scale_x{ BeltScaleMode::None }; - float belt_scale_x_angle{ 45.f }; - BeltScaleMode belt_scale_y{ BeltScaleMode::None }; - float belt_scale_y_angle{ 45.f }; - BeltScaleMode belt_scale_z{ BeltScaleMode::None }; - float belt_scale_z_angle{ 45.f }; + // Belt printer: physical tilt magnitude (deg) parsed from the slicing-rotation + // header comment; used to enable the preview's belt view. + float belt_tilt_angle{ 0.f }; RemapAxis preslice_remap_x{ RemapAxis::PosX }; RemapAxis preslice_remap_y{ RemapAxis::PosY }; RemapAxis preslice_remap_z{ RemapAxis::PosZ }; @@ -332,22 +316,7 @@ class Print; optimal_assignment = other.optimal_assignment; filament_change_count_map = other.filament_change_count_map; initial_layer_time = other.initial_layer_time; - belt_printer_angle = other.belt_printer_angle; - belt_shear_x = other.belt_shear_x; - belt_shear_x_angle = other.belt_shear_x_angle; - belt_shear_x_from = other.belt_shear_x_from; - belt_shear_y = other.belt_shear_y; - belt_shear_y_angle = other.belt_shear_y_angle; - belt_shear_y_from = other.belt_shear_y_from; - belt_shear_z = other.belt_shear_z; - belt_shear_z_angle = other.belt_shear_z_angle; - belt_shear_z_from = other.belt_shear_z_from; - belt_scale_x = other.belt_scale_x; - belt_scale_x_angle = other.belt_scale_x_angle; - belt_scale_y = other.belt_scale_y; - belt_scale_y_angle = other.belt_scale_y_angle; - belt_scale_z = other.belt_scale_z; - belt_scale_z_angle = other.belt_scale_z_angle; + belt_tilt_angle = other.belt_tilt_angle; preslice_remap_x = other.preslice_remap_x; preslice_remap_y = other.preslice_remap_y; preslice_remap_z = other.preslice_remap_z; diff --git a/src/libslic3r/GCode/MachineFrameTransform.cpp b/src/libslic3r/GCode/MachineFrameTransform.cpp index 10a658c7f7..03f74605fe 100644 --- a/src/libslic3r/GCode/MachineFrameTransform.cpp +++ b/src/libslic3r/GCode/MachineFrameTransform.cpp @@ -6,50 +6,6 @@ namespace Slic3r { namespace { -// Build the post-gcode axis-remap transform (mirrors BeltTransformPipeline::build_preslice_remap -// but reads post_gcode_remap_* keys). Includes Rev-mode translation derived from build volume. -Transform3d build_post_gcode_remap(const PrintConfig &config) -{ - Transform3d remap = Transform3d::Identity(); - - int rx = int(config.post_gcode_remap_x.value); - int ry = int(config.post_gcode_remap_y.value); - int rz = int(config.post_gcode_remap_z.value); - - if (rx == int(RemapAxis::PosX) && ry == int(RemapAxis::PosY) && rz == int(RemapAxis::PosZ)) - return remap; - - auto remap_column = [](int r) -> Vec3d { - int axis = r % 3; - Vec3d col = Vec3d::Zero(); - if (r < 3) col[axis] = 1.0; - else if (r < 6) col[axis] = -1.0; - else col[axis] = -1.0; // Rev: max - pos - return col; - }; - - Matrix3d lin; - lin.col(0) = remap_column(rx); - lin.col(1) = remap_column(ry); - lin.col(2) = remap_column(rz); - remap.linear() = lin; - - if (rx >= 6 || ry >= 6 || 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 trans = Vec3d::Zero(); - auto add_rev = [&](int r, int out) { - if (r >= 6) trans[out] = vol_max[r % 3]; - }; - add_rev(rx, 0); - add_rev(ry, 1); - add_rev(rz, 2); - remap.translation() = trans; - } - - return remap; -} - // Build the 3x3 shear matrix from gcode_shear_* keys. Matrix3d build_gcode_shear_matrix(const PrintConfig &config, bool &active) { @@ -94,13 +50,6 @@ Matrix3d build_gcode_scale_matrix(const PrintConfig &config, bool &active) return scale; } -bool has_post_gcode_remap(const PrintConfig &config) -{ - return int(config.post_gcode_remap_x.value) != int(RemapAxis::PosX) || - int(config.post_gcode_remap_y.value) != int(RemapAxis::PosY) || - int(config.post_gcode_remap_z.value) != int(RemapAxis::PosZ); -} - } // namespace bool MachineFrameTransform::init_from_config(const PrintConfig &config) @@ -111,13 +60,12 @@ bool MachineFrameTransform::init_from_config(const PrintConfig &config) if (!config.belt_printer.value) return false; - Transform3d post_remap = build_post_gcode_remap(config); bool shear_active = false; Matrix3d shear = build_gcode_shear_matrix(config, shear_active); bool scale_active = false; Matrix3d scale = build_gcode_scale_matrix(config, scale_active); - if (!shear_active && !scale_active && !has_post_gcode_remap(config)) + if (!shear_active && !scale_active) return false; // Compose per belt_gcode_transform_order: @@ -127,7 +75,6 @@ bool MachineFrameTransform::init_from_config(const PrintConfig &config) combined.linear() = (config.belt_gcode_transform_order.value == BeltTransformOrder::ScaleThenShear) ? Matrix3d(shear * scale) : Matrix3d(scale * shear); - combined = combined * post_remap; if (combined.isApprox(Transform3d::Identity())) return false; diff --git a/src/libslic3r/GCode/MachineFrameTransform.hpp b/src/libslic3r/GCode/MachineFrameTransform.hpp index 4a2f189802..9fa25b2323 100644 --- a/src/libslic3r/GCode/MachineFrameTransform.hpp +++ b/src/libslic3r/GCode/MachineFrameTransform.hpp @@ -9,15 +9,14 @@ namespace Slic3r { // Post-stage machine-frame transform for belt printers. // -// Applied in BeltGCodeWriter::to_machine_coords AFTER the back-transform -// and the existing gcode_remap_* axis remap, and BEFORE per-axis origin snap. -// Maps Cartesian (axis-permuted) G-code coordinates into the printer's -// physical machine frame using a parallel set of options: +// Applied in BeltGCodeWriter::to_machine_coords AFTER the back-transform, +// the gcode_remap_* axis remap and per-axis origin snap. Maps Cartesian +// (axis-permuted) G-code coordinates into the printer's physical machine +// frame using: // gcode_shear_x/y/z + _angle + _from // gcode_scale_x/y/z + _angle -// post_gcode_remap_x/y/z // -// Composition matches the mesh-side pipeline: shear * scale * post_remap. +// Composition follows belt_gcode_transform_order (shear * scale or scale * shear). class MachineFrameTransform { public: @@ -25,7 +24,7 @@ public: // Initialize from belt printer config. Returns true if a non-identity // transform was computed. Inactive when belt_printer is disabled or - // all three sub-stages are identity. + // both shear and scale are identity. bool init_from_config(const PrintConfig &config); // Apply the transform to a point. Returns pos unchanged if not active. diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index fab3e963a7..05bdb2f262 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1318,12 +1318,8 @@ static std::vector s_Preset_machine_limits_options { static std::vector s_Preset_printer_options { "printer_technology", - "printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "build_plate_tilt_x", "build_plate_tilt_y", "belt_printer", "belt_printer_angle", "belt_printer_infinite_y", "belt_shear_x", "belt_shear_x_angle", "belt_shear_x_from", "belt_shear_x_global", - "belt_shear_y", "belt_shear_y_angle", "belt_shear_y_from", "belt_shear_y_global", - "belt_shear_z", "belt_shear_z_angle", "belt_shear_z_from", "belt_shear_z_global", - "belt_scale_x", "belt_scale_x_angle", "belt_scale_y", "belt_scale_y_angle", "belt_scale_z", "belt_scale_z_angle", + "printable_area", "extruder_printable_area", "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", - "belt_mesh_transform_order", "preslice_remap_x", "preslice_remap_y", "preslice_remap_z", "preslice_remap_global", "gcode_remap_x", "gcode_remap_y", "gcode_remap_z", "gcode_back_transform", "gcode_shear_x", "gcode_shear_x_angle", "gcode_shear_x_from", @@ -1333,7 +1329,6 @@ static std::vector s_Preset_printer_options { "gcode_scale_y", "gcode_scale_y_angle", "gcode_scale_z", "gcode_scale_z_angle", "belt_gcode_transform_order", - "post_gcode_remap_x", "post_gcode_remap_y", "post_gcode_remap_z", "belt_preslice_global", "first_layer_plane", "first_layer_plane_offset", "first_layer_plane_thickness", "belt_origin_snap_x", "belt_origin_offset_x", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index c45334f95e..32f43052af 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -112,7 +112,6 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n "gcode_scale_y", "gcode_scale_y_angle", "gcode_scale_z", "gcode_scale_z_angle", "belt_gcode_transform_order", - "post_gcode_remap_x", "post_gcode_remap_y", "post_gcode_remap_z", "belt_origin_snap_x", "belt_origin_offset_x", "belt_origin_snap_y", "belt_origin_offset_y", "belt_origin_snap_z", "belt_origin_offset_z", @@ -301,26 +300,9 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "build_plate_tilt_y" // Belt printer transform options change the mesh geometry before slicing. || opt_key == "belt_printer" - || opt_key == "belt_printer_angle" - || opt_key == "belt_shear_x" - || opt_key == "belt_shear_x_angle" - || opt_key == "belt_shear_x_from" - || opt_key == "belt_shear_x_global" - || opt_key == "belt_shear_y" - || opt_key == "belt_shear_y_angle" - || opt_key == "belt_shear_y_from" - || opt_key == "belt_shear_y_global" - || opt_key == "belt_shear_z" - || opt_key == "belt_shear_z_angle" - || opt_key == "belt_shear_z_from" - || opt_key == "belt_shear_z_global" - || opt_key == "belt_scale_x" - || opt_key == "belt_scale_x_angle" - || opt_key == "belt_scale_y" - || opt_key == "belt_scale_y_angle" - || opt_key == "belt_scale_z" - || opt_key == "belt_scale_z_angle" - || opt_key == "belt_mesh_transform_order" + || 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" @@ -2284,10 +2266,13 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) int object_count = m_objects.size(); std::set need_slicing_objects; std::set re_slicing_objects; - // Belt global Z shear: each object needs unique layer Z values based on - // its bed position, so sharing layers between "identical" objects is wrong. - bool belt_no_share = m_config.belt_printer.value && m_config.belt_shear_z_global.value - && m_config.belt_shear_z.value != BeltShearMode::None; + // 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); if (!use_cache) { for (int index = 0; index < object_count; index++) { diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index b883b474be..3dddcc40dd 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1537,8 +1537,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ // 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_shear_z_global.value && m_config.belt_shear_z.value != BeltShearMode::None) - || (m_config.belt_slice_rotation_global.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 @@ -1633,8 +1632,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ // min_shift across all objects, so one move affects everyone). if (belt_instances_shifted && m_config.belt_printer.value - && ((m_config.belt_shear_z_global.value && m_config.belt_shear_z.value != BeltShearMode::None) - || (m_config.belt_slice_rotation_global.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 diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 8ee2350487..5c33692db0 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -1,5 +1,6 @@ #include "PrintConfig.hpp" #include "PrintConfigConstants.hpp" +#include "BeltTransform.hpp" #include "ClipperUtils.hpp" #include "Config.hpp" #include "Geometry.hpp" @@ -6388,18 +6389,6 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionBool(false)); - def = this->add("belt_printer_angle", coFloat); - def->label = L("Belt angle"); - def->category = L("Printable space"); - def->tooltip = L("The tilt angle of the belt surface in degrees. " - "Most belt printers use a 45-degree angle. " - "This controls the rotation applied to the slicing plane and G-code coordinates."); - def->sidetext = u8"\u00B0"; - def->min = 0; - def->max = 90; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionFloat(45.)); - def = this->add("belt_printer_infinite_y", coBool); def->label = L("Infinite Y axis"); def->category = L("Printable space"); @@ -6409,134 +6398,42 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionBool(true)); - // Per-axis shear controls for belt printer - auto add_belt_shear_mode = [this](const char *key, const char *label, BeltShearMode default_mode, - ConfigOptionMode mode = comAdvanced) { - auto def = this->add(key, coEnum); - def->label = L(label); - def->category = L("Printable space"); - def->tooltip = L("Shear function applied to this axis in belt printer mode."); - def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); - def->enum_values = {"none", "pos_cot", "neg_cot", "pos_tan", "neg_tan"}; - def->enum_labels = {L("None"), L("+cot(α)"), L("-cot(α)"), L("+tan(α)"), L("-tan(α)")}; - def->mode = mode; - def->set_default_value(new ConfigOptionEnum(default_mode)); - }; - auto add_belt_shear_angle = [this](const char *key, const char *label, - ConfigOptionMode mode = comAdvanced) { - auto def = this->add(key, coFloat); - def->label = L(label); - def->category = L("Printable space"); - def->tooltip = L("Angle (degrees) for the shear function on this axis."); - def->sidetext = L("°"); - def->min = 0.1; - def->max = 89.9; - def->mode = mode; - def->set_default_value(new ConfigOptionFloat(45)); - }; - auto add_belt_axis_enum = [this](const char *key, const char *label, const char *tooltip, - BeltAxis default_axis, ConfigOptionMode mode = comAdvanced) { - auto def = this->add(key, coEnum); - def->label = L(label); - def->category = L("Printable space"); - def->tooltip = L(tooltip); - def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); - def->enum_values = {"x", "y", "z"}; - def->enum_labels = {L("X"), L("Y"), L("Z")}; - def->mode = mode; - def->set_default_value(new ConfigOptionEnum(default_axis)); - }; - - auto add_belt_shear_global = [this](const char *key, const char *label, bool default_val = false) { - auto def = this->add(key, coBool); - def->label = L(label); - def->category = L("Printable space"); - def->tooltip = L("Apply shear in global coordinates (position-aware) rather than object-local coordinates."); - def->mode = comExpert; - def->set_default_value(new ConfigOptionBool(default_val)); - }; - - add_belt_shear_mode ("belt_shear_x", "Function", BeltShearMode::None, comExpert); - add_belt_shear_angle("belt_shear_x_angle", "Angle", comExpert); - add_belt_axis_enum ("belt_shear_x_from", "From", "Source axis for X shear.", BeltAxis::Z, comExpert); - add_belt_shear_global("belt_shear_x_global", "Global"); - - add_belt_shear_mode ("belt_shear_y", "Function", BeltShearMode::None, comExpert); - add_belt_shear_angle("belt_shear_y_angle", "Angle", comExpert); - add_belt_axis_enum ("belt_shear_y_from", "From", "Source axis for Y shear.", BeltAxis::Z, comExpert); - add_belt_shear_global("belt_shear_y_global", "Global"); - - add_belt_shear_mode ("belt_shear_z", "Function", BeltShearMode::None, comExpert); - add_belt_shear_angle("belt_shear_z_angle", "Angle", comExpert); - add_belt_axis_enum ("belt_shear_z_from", "From", "Source axis for Z shear.", BeltAxis::Y, comExpert); - add_belt_shear_global("belt_shear_z_global", "Global", true); - - // Per-axis scale controls for belt printer - auto add_belt_scale_mode = [this](const char *key, const char *label, BeltScaleMode default_mode, - ConfigOptionMode mode = comAdvanced) { - auto def = this->add(key, coEnum); - def->label = L(label); - def->category = L("Printable space"); - def->tooltip = L("Scale factor applied to this axis in belt printer mode."); - def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); - def->enum_values = {"none", "inv_sin", "inv_cos", "sin", "cos"}; - def->enum_labels = {L("None"), L("1/sin(α)"), L("1/cos(α)"), L("sin(α)"), L("cos(α)")}; - def->mode = mode; - def->set_default_value(new ConfigOptionEnum(default_mode)); - }; - auto add_belt_scale_angle = [this](const char *key, const char *label, - ConfigOptionMode mode = comAdvanced) { - auto def = this->add(key, coFloat); - def->label = L(label); - def->category = L("Printable space"); - def->tooltip = L("Angle (degrees) for the scale function on this axis."); - def->sidetext = L("°"); - def->min = 0.1; - def->max = 89.9; - def->mode = mode; - def->set_default_value(new ConfigOptionFloat(45)); - }; - - add_belt_scale_mode ("belt_scale_x", "Function", BeltScaleMode::None, comExpert); - add_belt_scale_angle("belt_scale_x_angle", "Angle", comExpert); - - add_belt_scale_mode ("belt_scale_y", "Function", BeltScaleMode::None, comExpert); - add_belt_scale_angle("belt_scale_y_angle", "Angle", comExpert); - - add_belt_scale_mode ("belt_scale_z", "Function", BeltScaleMode::None, comExpert); - add_belt_scale_angle("belt_scale_z_angle", "Angle", comExpert); - - // Global slicing rotation (alternative to per-axis shear+scale). + // Mesh rotation applied before slicing — the sole mesh-side belt transform AND + // the single source of truth for the physical belt tilt (bed rendering, support + // gravity tilt and bed-exclusion projection all derive their angle from this). def = this->add("belt_slice_rotation", coEnum); - def->label = L("Slicing rotation axis"); + def->label = L("Belt tilt axis"); def->category = L("Printable space"); - def->tooltip = L("Rotate the mesh by this axis before slicing. Use this for an " - "isometric (no shear distortion) belt slicing transform. " - "Mutually exclusive with the per-axis shear/scale controls " - "in the UI; the pipeline composes them if both are set in JSON."); + def->tooltip = L("Axis the mesh is rotated about before slicing. This is the belt " + "printer's tilt: an isometric (no distortion) rotation that also " + "drives bed rendering and support gravity tilt, and that the g-code " + "back-transform inverts before the machine-frame shear/scale and remap. " + "X is the typical gantry tilt (belt travels along Y)."); def->enum_keys_map = &ConfigOptionEnum::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->set_default_value(new ConfigOptionEnum(BeltRotationAxis::None)); + def->set_default_value(new ConfigOptionEnum(BeltRotationAxis::X)); def = this->add("belt_slice_rotation_angle", coFloat); - def->label = L("Slicing rotation angle"); + def->label = L("Belt tilt angle"); def->category = L("Printable space"); - def->tooltip = L("Magnitude of the slicing rotation, in degrees. Positive values " - "rotate counter-clockwise looking down the positive axis."); + def->tooltip = L("Tilt angle of the belt surface, in degrees. Most belt printers use " + "45°. Positive values rotate counter-clockwise looking down the " + "positive tilt axis; the magnitude is also the physical belt tilt " + "used for bed rendering and support gravity."); def->sidetext = L("°"); def->min = -180.; def->max = 180.; def->mode = comAdvanced; - def->set_default_value(new ConfigOptionFloat(0.)); + 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; mirrors belt_shear_z_global. " + "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)); @@ -6553,12 +6450,6 @@ void PrintConfigDef::init_fff_params() def->set_default_value(new ConfigOptionEnum(BeltTransformOrder::ShearThenScale)); }; - add_belt_transform_order("belt_mesh_transform_order", "Mesh transform order", - "Order in which the mesh shear and scale matrices are composed before slicing. " - "'Scale, then shear' applies scale first and then shear (current default). " - "'Shear, then scale' applies shear first and then scale. The g-code back-transform " - "follows the same order so that it correctly inverts the mesh transform."); - // G-code axis remap with sign auto add_belt_remap = [this](const char *key, const char *label, const char *tooltip, RemapAxis default_axis, ConfigOptionMode mode = comSimple) { @@ -6599,7 +6490,7 @@ void PrintConfigDef::init_fff_params() 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 per-axis 'Global' option on belt mesh shears, but for the remap."); + "Mirrors the 'Global' option on the belt slicing rotation, but for the remap."); def->mode = comExpert; def->set_default_value(new ConfigOptionBool(false)); @@ -6609,6 +6500,69 @@ void PrintConfigDef::init_fff_params() // Machine-frame G-code transforms: applied AFTER back-transform and gcode_remap, // before per-axis origin snap. Maps Cartesian G-code to the printer's physical machine frame. + // These shear/scale transforms act on the G-code coordinates (not the mesh) — they are + // the belt printer's only shear/scale stage. + auto add_belt_shear_mode = [this](const char *key, const char *label, BeltShearMode default_mode, + ConfigOptionMode mode = comAdvanced) { + auto def = this->add(key, coEnum); + def->label = L(label); + def->category = L("Printable space"); + def->tooltip = L("Shear function applied to this axis in belt printer mode."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values = {"none", "pos_cot", "neg_cot", "pos_tan", "neg_tan"}; + def->enum_labels = {L("None"), L("+cot(α)"), L("-cot(α)"), L("+tan(α)"), L("-tan(α)")}; + def->mode = mode; + def->set_default_value(new ConfigOptionEnum(default_mode)); + }; + auto add_belt_shear_angle = [this](const char *key, const char *label, + ConfigOptionMode mode = comAdvanced) { + auto def = this->add(key, coFloat); + def->label = L(label); + def->category = L("Printable space"); + def->tooltip = L("Angle (degrees) for the shear function on this axis."); + def->sidetext = L("°"); + def->min = 0.1; + def->max = 89.9; + def->mode = mode; + def->set_default_value(new ConfigOptionFloat(45)); + }; + auto add_belt_axis_enum = [this](const char *key, const char *label, const char *tooltip, + BeltAxis default_axis, ConfigOptionMode mode = comAdvanced) { + auto def = this->add(key, coEnum); + def->label = L(label); + def->category = L("Printable space"); + def->tooltip = L(tooltip); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values = {"x", "y", "z"}; + def->enum_labels = {L("X"), L("Y"), L("Z")}; + def->mode = mode; + def->set_default_value(new ConfigOptionEnum(default_axis)); + }; + auto add_belt_scale_mode = [this](const char *key, const char *label, BeltScaleMode default_mode, + ConfigOptionMode mode = comAdvanced) { + auto def = this->add(key, coEnum); + def->label = L(label); + def->category = L("Printable space"); + def->tooltip = L("Scale factor applied to this axis in belt printer mode."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values = {"none", "inv_sin", "inv_cos", "sin", "cos"}; + def->enum_labels = {L("None"), L("1/sin(α)"), L("1/cos(α)"), L("sin(α)"), L("cos(α)")}; + def->mode = mode; + def->set_default_value(new ConfigOptionEnum(default_mode)); + }; + auto add_belt_scale_angle = [this](const char *key, const char *label, + ConfigOptionMode mode = comAdvanced) { + auto def = this->add(key, coFloat); + def->label = L(label); + def->category = L("Printable space"); + def->tooltip = L("Angle (degrees) for the scale function on this axis."); + def->sidetext = L("°"); + def->min = 0.1; + def->max = 89.9; + def->mode = mode; + def->set_default_value(new ConfigOptionFloat(45)); + }; + add_belt_shear_mode ("gcode_shear_x", "Function", BeltShearMode::None, comExpert); add_belt_shear_angle("gcode_shear_x_angle", "Angle", comExpert); add_belt_axis_enum ("gcode_shear_x_from", "From", "Source axis for X shear in the machine-frame stage.", BeltAxis::Z, comExpert); @@ -6635,19 +6589,6 @@ void PrintConfigDef::init_fff_params() "applied to G-code coordinates. 'Scale, then shear' applies scale first and then " "shear (current default). 'Shear, then scale' applies shear first and then scale."); - add_belt_remap("post_gcode_remap_x", "X", - "Axis remap in the machine-frame stage. Applied AFTER gcode_remap, " - "to put coordinates into the printer's physical axis labelling. Default +X: no change.", - RemapAxis::PosX); - add_belt_remap("post_gcode_remap_y", "Y", - "Axis remap in the machine-frame stage. Applied AFTER gcode_remap, " - "to put coordinates into the printer's physical axis labelling. Default +Y: no change.", - RemapAxis::PosY); - add_belt_remap("post_gcode_remap_z", "Z", - "Axis remap in the machine-frame stage. Applied AFTER gcode_remap, " - "to put coordinates into the printer's physical axis labelling. Default +Z: no change.", - RemapAxis::PosZ); - def = this->add("gcode_back_transform", coBool); def->label = L("G-code back-transform"); def->category = L("Printable space"); @@ -11801,16 +11742,22 @@ Polygons get_bed_excluded_area(const PrintConfig& cfg) { const Pointfs exclude_area_points = cfg.bed_exclude_area.values; - // Belt printer: project exclusion zone points from belt surface to machine-frame XY. - // On the belt surface, Z=0, so machine_Y = belt_Y * cos(angle). + // Belt printer: project exclusion zone points from the belt surface to machine-frame XY. + // On the belt surface Z=0, so the in-plane axis foreshortens by cos(tilt). The tilt + // axis decides which bed axis foreshortens: tilt about X (belt along Y) scales Y, + // tilt about Y (belt along X) scales X. Derived from belt_slice_rotation. const bool is_belt = cfg.belt_printer.value; - const double belt_cos = is_belt ? std::cos(Geometry::deg2rad(cfg.belt_printer_angle.value)) : 1.0; + const auto tilt = BeltTransformPipeline::physical_tilt( + cfg.belt_slice_rotation.value, cfg.belt_slice_rotation_angle.value); + const double cos_x = is_belt ? std::cos(Geometry::deg2rad(tilt.tilt_x_deg)) : 1.0; // foreshortens Y + const double cos_y = is_belt ? std::cos(Geometry::deg2rad(tilt.tilt_y_deg)) : 1.0; // foreshortens X Polygon exclude_poly; for (int i = 0; i < exclude_area_points.size(); i++) { auto pt = exclude_area_points[i]; - double y = is_belt ? pt.y() * belt_cos : pt.y(); - exclude_poly.points.emplace_back(scale_(pt.x()), scale_(y)); + double x = is_belt ? pt.x() * cos_y : pt.x(); + double y = is_belt ? pt.y() * cos_x : pt.y(); + exclude_poly.points.emplace_back(scale_(x), scale_(y)); } exclude_poly.make_counter_clockwise(); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index bcfaab7bbc..f13c04f454 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1573,29 +1573,13 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionFloat, build_plate_tilt_y)) // Belt printer settings (printer-level). ((ConfigOptionBool, belt_printer)) - ((ConfigOptionFloat, belt_printer_angle)) ((ConfigOptionBool, belt_printer_infinite_y)) - ((ConfigOptionEnum, belt_shear_x)) - ((ConfigOptionFloat, belt_shear_x_angle)) - ((ConfigOptionEnum, belt_shear_x_from)) - ((ConfigOptionBool, belt_shear_x_global)) - ((ConfigOptionEnum, belt_shear_y)) - ((ConfigOptionFloat, belt_shear_y_angle)) - ((ConfigOptionEnum, belt_shear_y_from)) - ((ConfigOptionBool, belt_shear_y_global)) - ((ConfigOptionEnum, belt_shear_z)) - ((ConfigOptionFloat, belt_shear_z_angle)) - ((ConfigOptionEnum, belt_shear_z_from)) - ((ConfigOptionBool, belt_shear_z_global)) - ((ConfigOptionEnum, belt_scale_x)) - ((ConfigOptionFloat, belt_scale_x_angle)) - ((ConfigOptionEnum, belt_scale_y)) - ((ConfigOptionFloat, belt_scale_y_angle)) - ((ConfigOptionEnum, belt_scale_z)) - ((ConfigOptionFloat, belt_scale_z_angle)) - // Global mesh rotation as an alternative to per-axis shear/scale (isometric - // slicing transform). Composes with shear in the pipeline math; UI gates - // them as mutually exclusive. + // Mesh rotation applied before slicing — the sole mesh-side belt transform and + // the single source of truth for the physical belt tilt (its angle + axis drive + // bed rendering, support gravity tilt, and the bed-exclusion projection). + // Isometric (no distortion); the g-code back-transform inverts it before the + // machine-frame remap. (Shear & scale are applied to the g-code, not the + // mesh — see gcode_shear_* / gcode_scale_* below.) ((ConfigOptionEnum, belt_slice_rotation)) ((ConfigOptionFloat, belt_slice_rotation_angle)) ((ConfigOptionBool, belt_slice_rotation_global)) @@ -1621,11 +1605,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionFloat, gcode_scale_y_angle)) ((ConfigOptionEnum, gcode_scale_z)) ((ConfigOptionFloat, gcode_scale_z_angle)) - ((ConfigOptionEnum, belt_mesh_transform_order)) ((ConfigOptionEnum, belt_gcode_transform_order)) - ((ConfigOptionEnum, post_gcode_remap_x)) - ((ConfigOptionEnum, post_gcode_remap_y)) - ((ConfigOptionEnum, post_gcode_remap_z)) ((ConfigOptionBool, gcode_back_transform)) ((ConfigOptionBool, belt_preslice_global)) ((ConfigOptionEnum, first_layer_plane)) diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index deaf6fdfce..5fc7df232e 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -341,9 +341,7 @@ static std::vector> 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_shear(print_config) - || BeltTransformPipeline::has_scale(print_config) - || BeltTransformPipeline::has_rotation(print_config) + (BeltTransformPipeline::has_rotation(print_config) || BeltTransformPipeline::has_preslice_remap(print_config))); // Belt-transform addendum: with bbox-Z untrusted, the simple path's // "first model_part wins" logic drops subsequent volumes' slices unless @@ -880,12 +878,12 @@ void groupingVolumesForBrim(PrintObject* object, LayerPtrs& layers, int firstLay // Resulting expolygons of layer regions are marked as Internal. void PrintObject::slice() { - BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] slice request tid=" << std::this_thread::get_id() << " obj=" << this; + BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] slice request tid=" << std::this_thread::get_id() << " obj=" << this; if (! this->set_started(posSlice)) { - BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] slice SKIP tid=" << std::this_thread::get_id() << " obj=" << this << " (already started/done)"; + BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] slice SKIP tid=" << std::this_thread::get_id() << " obj=" << this << " (already started/done)"; return; } - BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] slice ENTER tid=" << std::this_thread::get_id() << " obj=" << this; + BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] slice ENTER tid=" << std::this_thread::get_id() << " obj=" << this; //BBS: add flag to reload scene for shell rendering m_print->set_status(5, L("Slicing mesh"), PrintBase::SlicingStatus::RELOAD_SCENE); std::vector layer_height_profile; @@ -968,16 +966,16 @@ void PrintObject::slice() // regardless of global mode, only the output Z coordinates change. { const auto &pcfg = this->print()->config(); - BOOST_LOG_TRIVIAL(warning) << "Belt global check: belt_printer=" << pcfg.belt_printer.value - << " belt_shear_z=" << int(pcfg.belt_shear_z.value) - << " belt_shear_z_global=" << pcfg.belt_shear_z_global.value + 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) { Point inst_shift = this->instances().empty() ? Point(0, 0) : this->instances().front().shift - this->center_offset(); - BOOST_LOG_TRIVIAL(warning) << "Belt global: object " << this->model_object()->name + BOOST_LOG_TRIVIAL(trace) << "Belt global: object " << this->model_object()->name << " instances=" << this->instances().size() << " shift=(" << unscale(inst_shift.x()) << ", " << unscale(inst_shift.y()) << ")"; @@ -987,20 +985,14 @@ void PrintObject::slice() // so the slicer can slice with slicer_z >= 0. BeltBackTransform inverts // build_forward_transform() which DOES NOT include this per-object // Z-shift (it's not known until vertex scan time). Result: G-code - // coords emerge offset by the un-undone Z-shift — for shear that's a - // pure machine_z lift; for rotation it leaks into both machine_y and - // machine_z because the inverse rotation couples slicer_z back into - // both axes. Compensating layer.print_z by shear_min_z here makes the - // back-transform produce correct machine-frame coordinates whether or - // not any global mode is active. - // - // (The original global-mode-only application of shear_min_z was sized - // for cube-vs-inverted-cone-tip differentiation; the same compensation - // is what fixes assemblies and rotation-mode parts where z_shift > 0.) + // coords emerge offset by the un-undone Z-shift — the inverse rotation + // 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.; - double shear_min_z = m_belt_min_z - belt_surface_z; - double global_z_offset = shear_min_z; + double belt_z_shift = m_belt_min_z - belt_surface_z; + double global_z_offset = belt_z_shift; // Centering correction: trafo_centered pretranslates by // -m_center_offset.{x,y}. Under the belt forward transform, the @@ -1019,7 +1011,7 @@ void PrintObject::slice() 0.); double centering_z_corr = (T_fwd.linear() * c_off).z(); global_z_offset += centering_z_corr; - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] centering correction" + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] centering correction" << " obj=" << this->model_object()->name << " m_center_offset_mm=(" << c_off.x() << "," << c_off.y() << ")" << " centering_z_corr=" << centering_z_corr @@ -1031,7 +1023,7 @@ void PrintObject::slice() // print_z adjustment. { BoundingBoxf3 raw_bb = this->model_object()->raw_bounding_box(); - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] slice() per-object summary" + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] slice() per-object summary" << " obj=" << this->model_object()->name << " n_volumes=" << this->model_object()->volumes.size() << " raw_bbox.min=(" << raw_bb.min.x() << "," << raw_bb.min.y() << "," << raw_bb.min.z() << ")" @@ -1041,13 +1033,13 @@ void PrintObject::slice() << " inst_shift=(" << unscale(inst_shift.x()) << "," << unscale(inst_shift.y()) << ")" << " m_belt_min_z=" << m_belt_min_z << " belt_surface_z=" << belt_surface_z - << " shear_min_z=" << shear_min_z; + << " belt_z_shift=" << belt_z_shift; // Per-volume bbox + get_matrix translation so order/composition is visible. int vi = 0; for (const ModelVolume *mv : this->model_object()->volumes) { if (!mv->is_model_part()) { ++vi; continue; } BoundingBoxf3 vol_bb = mv->mesh().transformed_bounding_box(mv->get_matrix()); - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] vol[" << vi + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] vol[" << vi << "] id=" << mv->id().id << " name='" << mv->name << "'" << " get_matrix.translation=(" << mv->get_matrix().translation().x() << "," << mv->get_matrix().translation().y() << "," << mv->get_matrix().translation().z() << ")" << " object_bbox.min=(" << vol_bb.min.x() << "," << vol_bb.min.y() << "," << vol_bb.min.z() << ")" @@ -1063,46 +1055,15 @@ void PrintObject::slice() Vec3d d(unscale(inst_shift.x()), unscale(inst_shift.y()), 0.); Vec3d c = T.linear() * d - d; global_z_offset += c.z(); - BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] write m_belt_global_xy_correction tid=" << std::this_thread::get_id() + BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] write m_belt_global_xy_correction tid=" << std::this_thread::get_id() << " obj=" << this << " old=(" << m_belt_global_xy_correction.x() << "," << m_belt_global_xy_correction.y() << ") new=(" << c.x() << "," << c.y() << ")"; m_belt_global_xy_correction = Vec2d(c.x(), c.y()); - BOOST_LOG_TRIVIAL(warning) << "Belt preslice_global: correction=(" + BOOST_LOG_TRIVIAL(trace) << "Belt preslice_global: correction=(" << c.x() << ", " << c.y() << ", " << c.z() << ")" - << " shear_min_z=" << shear_min_z << " (m_belt_min_z=" << m_belt_min_z << ")"; + << " belt_z_shift=" << belt_z_shift << " (m_belt_min_z=" << m_belt_min_z << ")"; } else { - struct GAxis { BeltShearMode mode; double angle; int from; bool global; }; - GAxis gaxes[3] = { - { pcfg.belt_shear_x.value, pcfg.belt_shear_x_angle.value, int(pcfg.belt_shear_x_from.value), pcfg.belt_shear_x_global.value }, - { pcfg.belt_shear_y.value, pcfg.belt_shear_y_angle.value, int(pcfg.belt_shear_y_from.value), pcfg.belt_shear_y_global.value }, - { pcfg.belt_shear_z.value, pcfg.belt_shear_z_angle.value, int(pcfg.belt_shear_z_from.value), pcfg.belt_shear_z_global.value }, - }; - - // Only the Z-row shear contributes a Z offset from global mode. - // (X/Y row shears with global would offset X/Y, not Z — not useful here.) - const auto &za = gaxes[2]; // Z row - if (za.global && za.mode != BeltShearMode::None && za.from < 2) { - // Use the full forward-transform correction (same formula as - // preslice_global) so the per-bed-position offset matches what - // BeltGCode::on_set_origin's T.linear() pre-multiplication - // expects after back-transform. The simple `cy*tan(α)` form - // is exact only for ScaleThenShear; under ShearThenScale with - // sy != 1 it leaves the object bottom off the belt plane by - // cy*tan(α)*(sy-1)/sy. - Transform3d T = BeltTransformPipeline::build_forward_transform(pcfg); - Vec3d d(unscale(inst_shift.x()), unscale(inst_shift.y()), 0.); - Vec3d c = T.linear() * d - d; - global_z_offset += c.z(); - BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] write m_belt_global_xy_correction tid=" << std::this_thread::get_id() - << " obj=" << this << " old=(" << m_belt_global_xy_correction.x() << "," << m_belt_global_xy_correction.y() - << ") new=(" << c.x() << "," << c.y() << ")"; - m_belt_global_xy_correction = Vec2d(c.x(), c.y()); - BOOST_LOG_TRIVIAL(warning) << "Belt per-axis Z-shear-global: correction=(" - << c.x() << ", " << c.y() << ", " << c.z() << ")" - << " shear_min_z=" << shear_min_z << " (m_belt_min_z=" << m_belt_min_z << ")"; - } - // 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. @@ -1129,16 +1090,16 @@ void PrintObject::slice() } } - BOOST_LOG_TRIVIAL(warning) << "Belt global: z_offset=" << global_z_offset + BOOST_LOG_TRIVIAL(trace) << "Belt global: z_offset=" << global_z_offset << " (relative to min across " << this->print()->objects().size() << " objects)"; - BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] write m_belt_global_z_offset tid=" << std::this_thread::get_id() + BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] write m_belt_global_z_offset tid=" << std::this_thread::get_id() << " obj=" << this << " old=" << m_belt_global_z_offset << " new=" << global_z_offset; m_belt_global_z_offset = global_z_offset; // [BELT-DEBUG] Final breakdown of all contributions to layer.print_z // and where the first / last layer end up post-adjustment. - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] global_z_offset breakdown" + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] global_z_offset breakdown" << " obj=" << this->model_object()->name - << " shear_min_z=" << shear_min_z + << " belt_z_shift=" << belt_z_shift << " total_global_z_offset=" << global_z_offset << " xy_correction=(" << m_belt_global_xy_correction.x() << "," << m_belt_global_xy_correction.y() << ")" << " belt_floor_z_shift_before=" << (m_slicing_params.belt_floor_z_shift) @@ -1152,13 +1113,13 @@ void PrintObject::slice() m_slicing_params.belt_floor_z_shift += global_z_offset; } if (!m_layers.empty()) { - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] post-adjustment" + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] post-adjustment" << " first_layer.print_z=" << m_layers.front()->print_z << " last_layer.print_z=" << m_layers.back()->print_z << " belt_floor_z_shift_after=" << m_slicing_params.belt_floor_z_shift; } if (!m_layers.empty()) { - BOOST_LOG_TRIVIAL(warning) << "Belt global: first_layer_z=" << m_layers.front()->print_z + BOOST_LOG_TRIVIAL(trace) << "Belt global: first_layer_z=" << m_layers.front()->print_z << " last_layer_z=" << m_layers.back()->print_z << " num_layers=" << m_layers.size() << " center_offset=(" << unscale(m_center_offset.x()) @@ -1175,7 +1136,7 @@ void PrintObject::slice() } // BBS - BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] slice EXIT tid=" << std::this_thread::get_id() << " obj=" << this + BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] slice EXIT tid=" << std::this_thread::get_id() << " obj=" << this << " layers=" << m_layers.size() << " belt_min_z=" << m_belt_min_z << " belt_global_z_offset=" << m_belt_global_z_offset << " belt_xy=(" << m_belt_global_xy_correction.x() << "," << m_belt_global_xy_correction.y() << ")"; diff --git a/src/slic3r/GUI/3DBed.cpp b/src/slic3r/GUI/3DBed.cpp index 5663882f1c..fa9acffb50 100644 --- a/src/slic3r/GUI/3DBed.cpp +++ b/src/slic3r/GUI/3DBed.cpp @@ -700,11 +700,11 @@ void Bed3D::render_model(const Transform3d& view_matrix, const Transform3d& proj shader->start_using(); shader->set_uniform("emission_factor", 0.0f); Transform3d model_matrix = Geometry::assemble_transform(m_model_offset); - // Belt printer: rotate the bed model about X so the belt tilt is visible. - // Negative angle: belt surface tilts downward away from the nozzle. + // Belt printer: rotate the bed model about the tilt axis so the belt tilt + // is visible. Negative angle: belt surface tilts downward away from the nozzle. if (m_is_belt_printer && m_belt_angle > 0.f) { double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle)); - model_matrix = Eigen::AngleAxisd(-angle_rad, Vec3d::UnitX()) * model_matrix; + model_matrix = Eigen::AngleAxisd(-angle_rad, belt_tilt_unit_axis()) * model_matrix; } shader->set_uniform("volume_world_matrix", model_matrix); shader->set_uniform("view_model_matrix", view_matrix * model_matrix); @@ -747,12 +747,10 @@ void Bed3D::render_custom(GLCanvas3D& canvas, const Transform3d& view_matrix, co void Bed3D::render_gravity_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix) { const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + // build_plate_tilt_{x,y} are kept in sync with the belt tilt (see TabPrinter), so + // reading them here covers both belt and non-belt tilted printers. double tilt_x_deg = cfg.opt_float("build_plate_tilt_x"); double tilt_y_deg = cfg.opt_float("build_plate_tilt_y"); - // Belt printer: auto-derive gravity direction from belt angle if belt mode is active. - if (m_is_belt_printer && m_belt_angle > 0.f) { - tilt_x_deg = m_belt_angle; - } if (tilt_x_deg == 0. && tilt_y_deg == 0.) { m_gravity_arrow.reset(); return; @@ -819,11 +817,12 @@ void Bed3D::render_slicing_arrow(const Transform3d& view_matrix, const Transform m_slicing_arrow.init_from(stilized_arrow(16, tip_radius, tip_length, stem_radius, stem_length)); } - // The slicing direction: layers stack along the gantry normal. - // With mesh rotation R(-alpha, X), the slicing Z-axis in the original frame - // points in direction R(+alpha, X) * (0, 0, 1) = (0, -sin(alpha), cos(alpha)). + // The slicing direction: layers stack along the gantry normal, i.e. the image of + // +Z under the mesh rotation about the tilt axis. Use the same AngleAxis as the + // slicing pipeline so the arrow matches whichever tilt axis is configured. double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle)); - Vec3d slice_dir = Vec3d(0., -std::sin(angle_rad), std::cos(angle_rad)).normalized(); + Vec3d slice_dir = (Eigen::AngleAxisd(angle_rad, belt_tilt_unit_axis()).toRotationMatrix() + * Vec3d::UnitZ()).normalized(); // Compute rotation to align +Z (arrow default) with slice_dir. Vec3d from = Vec3d::UnitZ(); @@ -895,12 +894,12 @@ void Bed3D::render_slicing_plane(const Transform3d& view_matrix, const Transform shader->start_using(); // Show a tilted plane representing the slicing direction. - // The slicing plane is rotated by belt_angle about X from horizontal. + // The slicing plane is rotated by belt_angle about the tilt axis from horizontal. // Raise it slightly so it's visible above the bed surface. double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle)); Transform3d model_matrix = Transform3d::Identity(); model_matrix.translate(Vec3d(0., 0., 30.)); - model_matrix.rotate(Eigen::AngleAxisd(angle_rad, Vec3d::UnitX())); + model_matrix.rotate(Eigen::AngleAxisd(angle_rad, belt_tilt_unit_axis())); shader->set_uniform("view_model_matrix", view_matrix * model_matrix); shader->set_uniform("projection_matrix", projection_matrix); diff --git a/src/slic3r/GUI/3DBed.hpp b/src/slic3r/GUI/3DBed.hpp index d01273961d..788d8f52a3 100644 --- a/src/slic3r/GUI/3DBed.hpp +++ b/src/slic3r/GUI/3DBed.hpp @@ -125,6 +125,8 @@ private: // Belt printer state for rendering. bool m_is_belt_printer = false; float m_belt_angle = 0.f; + // Tilt axis: 0 = X (belt travels along Y, the common case), 1 = Y. + int m_belt_tilt_axis = 0; public: Bed3D() = default; @@ -146,10 +148,14 @@ public: const BuildVolume& build_volume() const { return m_build_volume; } BuildVolume& build_volume() { return m_build_volume; } - // Belt printer bed settings. - void set_belt_printer(bool enabled, float angle_deg) { m_is_belt_printer = enabled; m_belt_angle = angle_deg; } + // Belt printer bed settings. tilt_axis: 0 = X (belt along Y), 1 = Y. + void set_belt_printer(bool enabled, float angle_deg, int tilt_axis = 0) { + m_is_belt_printer = enabled; m_belt_angle = angle_deg; m_belt_tilt_axis = tilt_axis; + } bool is_belt_printer() const { return m_is_belt_printer; } float belt_angle() const { return m_belt_angle; } + // Unit vector of the tilt axis in bed space. + Vec3d belt_tilt_unit_axis() const { return m_belt_tilt_axis == 1 ? Vec3d::UnitY() : Vec3d::UnitX(); } // Was the model provided, or was it generated procedurally? Type get_type() const { return m_type; } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 2bd1f5fc8d..c4a38fc12e 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -11312,80 +11312,33 @@ void Plater::priv::set_bed_shape(const Pointfs &shape, const auto *belt_opt = config->option("belt_printer"); bool is_belt = belt_opt && belt_opt->value; if (is_belt) { - double belt_angle = config->opt_float("belt_printer_angle"); + // The slicing rotation is the single source of truth for the belt tilt: + // its magnitude is the physical tilt angle and its axis is the tilt axis. + auto rot_axis = config->option>("belt_slice_rotation")->value; + double rot_angle = config->opt_float("belt_slice_rotation_angle"); + double belt_angle = std::abs(rot_angle); // physical tilt magnitude + int tilt_axis = (rot_axis == BeltRotationAxis::Y) ? 1 : 0; bool infinite_y = config->opt_bool("belt_printer_infinite_y"); bed.build_volume().set_belt_printer(true, belt_angle, infinite_y); - bed.set_belt_printer(true, static_cast(belt_angle)); + bed.set_belt_printer(true, static_cast(belt_angle), tilt_axis); if (preview) preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(true, static_cast(belt_angle)); - // Compute the inverse of the full belt shear+scale transform for the G-code viewer. - auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { - double angle_rad = Geometry::deg2rad(angle_deg); - double sin_a = std::sin(angle_rad); - double cos_a = std::cos(angle_rad); - switch (mode) { - case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; - case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; - case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; - case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; - default: return 0.; - } - }; - auto compute_scale_factor = [](BeltScaleMode mode, double angle_deg) -> double { - if (mode == BeltScaleMode::None) return 1.; - double angle_rad = Geometry::deg2rad(angle_deg); - double sin_a = std::sin(angle_rad); - double cos_a = std::cos(angle_rad); - switch (mode) { - case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; - case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; - case BeltScaleMode::Sin: return sin_a; - case BeltScaleMode::Cos: return cos_a; - default: return 1.; - } - }; - - // Read shear configs. - auto get_shear_mode = [this](const char *key) -> BeltShearMode { - auto opt = config->option>(key); - return opt ? opt->value : BeltShearMode::None; - }; - auto get_axis = [this](const char *key) -> BeltAxis { - auto opt = config->option>(key); - return opt ? opt->value : BeltAxis::X; - }; - auto get_scale_mode = [this](const char *key) -> BeltScaleMode { - auto opt = config->option>(key); - return opt ? opt->value : BeltScaleMode::None; - }; - - struct AxisShear { BeltShearMode mode; double angle; int from; }; - AxisShear axes[3] = { - { get_shear_mode("belt_shear_x"), config->opt_float("belt_shear_x_angle"), int(get_axis("belt_shear_x_from")) }, - { get_shear_mode("belt_shear_y"), config->opt_float("belt_shear_y_angle"), int(get_axis("belt_shear_y_from")) }, - { get_shear_mode("belt_shear_z"), config->opt_float("belt_shear_z_angle"), int(get_axis("belt_shear_z_from")) }, - }; - - Transform3d belt_shear = Transform3d::Identity(); - for (int row = 0; row < 3; ++row) { - if (axes[row].mode != BeltShearMode::None) { - double factor = compute_shear_factor(axes[row].mode, axes[row].angle); - if (std::abs(factor) > EPSILON) - belt_shear.matrix()(row, axes[row].from) += factor; + // Compute the inverse of the mesh-side belt transform for the G-code + // viewer. The sole mesh transform is the slicing rotation; build its + // matrix from config and hand the viewer its inverse (rotation is + // orthogonal, so the inverse is the transpose). + Transform3d forward = Transform3d::Identity(); + if (rot_axis != BeltRotationAxis::None && std::abs(rot_angle) > EPSILON) { + Vec3d unit_axis = Vec3d::UnitX(); + switch (rot_axis) { + case BeltRotationAxis::X: unit_axis = Vec3d::UnitX(); break; + case BeltRotationAxis::Y: unit_axis = Vec3d::UnitY(); break; + case BeltRotationAxis::Z: unit_axis = Vec3d::UnitZ(); break; + default: break; } + forward.linear() = Eigen::AngleAxisd(Geometry::deg2rad(rot_angle), unit_axis).toRotationMatrix(); } - - double sx = compute_scale_factor(get_scale_mode("belt_scale_x"), config->opt_float("belt_scale_x_angle")); - double sy = compute_scale_factor(get_scale_mode("belt_scale_y"), config->opt_float("belt_scale_y_angle")); - double sz = compute_scale_factor(get_scale_mode("belt_scale_z"), config->opt_float("belt_scale_z_angle")); - Transform3d belt_scale = Transform3d::Identity(); - belt_scale.matrix()(0, 0) = sx; - belt_scale.matrix()(1, 1) = sy; - belt_scale.matrix()(2, 2) = sz; - - // Forward transform: scale * shear. Inverse for the viewer. - Transform3d forward = belt_scale * belt_shear; Transform3d inverse = forward.inverse(); if (preview) preview->get_canvas3d()->get_gcode_viewer().set_belt_inverse_transform(inverse); @@ -13730,10 +13683,10 @@ void Plater::load_gcode(const wxString& filename) current_print.set_gcode_file_ready(); - // Belt printer: detect belt_printer_angle from loaded G-code header and enable + // Belt printer: detect the belt tilt from the loaded G-code header and enable // belt view mode on the GCodeViewer so the "Show designed view" toggle appears. - if (current_result->belt_printer_angle > 0.f) { - float angle = current_result->belt_printer_angle; + if (current_result->belt_tilt_angle > 0.f) { + float angle = current_result->belt_tilt_angle; p->preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(true, angle); } else { p->preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(false, 0.f); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index fa90d97f54..1126d176c5 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4,6 +4,7 @@ #include "PresetHints.hpp" #include "libslic3r/PresetBundle.hpp" #include "libslic3r/PrintConfig.hpp" +#include "libslic3r/BeltTransform.hpp" #include "libslic3r/Utils.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/GCode/GCodeProcessor.hpp" @@ -4464,68 +4465,22 @@ void TabPrinter::build_fff() // checkbox is hidden when belt_printer is off (see TabPrinter::toggle_options). auto belt_og = page->new_optgroup(L("Belt printer"), L"param_advanced"); belt_og->append_single_option_line("belt_printer"); - belt_og->append_single_option_line("belt_printer_angle"); belt_og->append_single_option_line("belt_printer_infinite_y"); - // Mesh rotate (default belt-side transform): isometric, no distortion. - // Shown above the per-axis shear/scale rows because most users should - // pick rotation; shear/scale is the expert escape hatch. + // 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("Mesh rotate"), - L("Global mesh rotation applied before slicing. Isometric " - "(no distortion); the back-transform inverts it before the " - "machine-frame remap. Default belt transform. Mutually " - "exclusive with per-axis shear/scale in the UI.") }; + 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.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); } - // Per-axis shear/scale: expert escape hatch for non-rigid belt transforms - // (BlackBelt-style 1/sin scaling, asymmetric belt geometries, etc.). - // Group mode + angle + source on one row per axis. - { - Line line = { L("Mesh shear X"), L("Shear applied to the X axis before slicing") }; - line.append_option(belt_og->get_option("belt_shear_x")); - line.append_option(belt_og->get_option("belt_shear_x_angle")); - line.append_option(belt_og->get_option("belt_shear_x_from")); - line.append_option(belt_og->get_option("belt_shear_x_global")); - belt_og->append_line(line); - } - { - Line line = { L("Mesh shear Y"), L("Shear applied to the Y axis before slicing") }; - line.append_option(belt_og->get_option("belt_shear_y")); - line.append_option(belt_og->get_option("belt_shear_y_angle")); - line.append_option(belt_og->get_option("belt_shear_y_from")); - line.append_option(belt_og->get_option("belt_shear_y_global")); - belt_og->append_line(line); - } - { - Line line = { L("Mesh shear Z"), L("Shear applied to the Z axis before slicing") }; - line.append_option(belt_og->get_option("belt_shear_z")); - line.append_option(belt_og->get_option("belt_shear_z_angle")); - line.append_option(belt_og->get_option("belt_shear_z_from")); - line.append_option(belt_og->get_option("belt_shear_z_global")); - belt_og->append_line(line); - } - { - Line line = { L("Mesh scale X"), L("Scale applied to the X axis before slicing") }; - line.append_option(belt_og->get_option("belt_scale_x")); - line.append_option(belt_og->get_option("belt_scale_x_angle")); - belt_og->append_line(line); - } - { - Line line = { L("Mesh scale Y"), L("Scale applied to the Y axis before slicing") }; - line.append_option(belt_og->get_option("belt_scale_y")); - line.append_option(belt_og->get_option("belt_scale_y_angle")); - belt_og->append_line(line); - } - { - Line line = { L("Mesh scale Z"), L("Scale applied to the Z axis before slicing") }; - line.append_option(belt_og->get_option("belt_scale_z")); - line.append_option(belt_og->get_option("belt_scale_z_angle")); - belt_og->append_line(line); - } - belt_og->append_single_option_line("belt_mesh_transform_order"); { Line line = { L("Pre-slice axis remap"), L("Remap model axes before slicing so the slicer's coordinate system matches " @@ -4537,13 +4492,6 @@ void TabPrinter::build_fff() line.append_option(belt_og->get_option("preslice_remap_global")); belt_og->append_line(line); } - { - Line line = { L("G-code axis remap (post-slice)"), L("Remap slicing-frame axes to machine axes in G-code output. Applied AFTER slicing, during G-code generation.") }; - line.append_option(belt_og->get_option("gcode_remap_x")); - line.append_option(belt_og->get_option("gcode_remap_y")); - line.append_option(belt_og->get_option("gcode_remap_z")); - belt_og->append_line(line); - } belt_og->append_single_option_line("belt_preslice_global"); belt_og->append_single_option_line("gcode_back_transform"); { @@ -4588,12 +4536,10 @@ void TabPrinter::build_fff() { auto mf = page->new_optgroup(L("Machine frame transforms"), L"param_advanced"); { - Line line = { L("Post-gcode axis remap"), - L("Axis remap in the machine-frame stage. Applied AFTER gcode_remap, " - "to put coordinates into the printer's physical axis labelling.") }; - line.append_option(mf->get_option("post_gcode_remap_x")); - line.append_option(mf->get_option("post_gcode_remap_y")); - line.append_option(mf->get_option("post_gcode_remap_z")); + Line line = { L("G-code axis remap (post-slice)"), L("Remap slicing-frame axes to machine axes in G-code output. Applied AFTER slicing, during G-code generation.") }; + line.append_option(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); } { @@ -5612,20 +5558,9 @@ void TabPrinter::toggle_options() // Belt printer: show belt-specific settings only when belt_printer is enabled. bool is_belt = m_config->opt_bool("belt_printer"); bool expert_or_above = (m_mode >= comExpert); - toggle_line("belt_printer_angle", is_belt); toggle_line("belt_printer_infinite_y", is_belt); - // Mesh rotate: advanced (visible by default in belt mode). + // Belt tilt: the sole mesh-side belt transform (visible by default in belt mode). toggle_line("belt_slice_rotation", is_belt); - // Mesh shear/scale: expert-only (the rigid rotation above is the - // primary belt transform; shear/scale is reserved for power users - // matching BlackBelt-style 1/sin scale or asymmetric belt geometries). - toggle_line("belt_shear_x", is_belt && expert_or_above); - toggle_line("belt_shear_y", is_belt && expert_or_above); - toggle_line("belt_shear_z", is_belt && expert_or_above); - toggle_line("belt_scale_x", is_belt && expert_or_above); - toggle_line("belt_scale_y", is_belt && expert_or_above); - toggle_line("belt_scale_z", is_belt && expert_or_above); - toggle_line("belt_mesh_transform_order", is_belt && expert_or_above); for (auto el : {"belt_origin_snap_x", "belt_origin_snap_y", "belt_origin_snap_z"}) toggle_line(el, is_belt); @@ -5641,59 +5576,11 @@ void TabPrinter::toggle_options() // preslice_remap_global: superseded by belt_preslice_global toggle_option("preslice_remap_global", is_belt && !belt_global); - // Mutual exclusion (UI only): slicing rotation and per-axis shear/scale are - // alternatives in the printer panel. The pipeline math composes them; this - // just disables the inactive set so users pick one path or the other. + // Rotation is the only mesh-side belt transform. Gray out its angle/global + // sub-options when no rotation axis is selected. auto rot_axis = m_config->option>("belt_slice_rotation")->value; - bool rotation_active = is_belt - && rot_axis != BeltRotationAxis::None - && std::abs(m_config->opt_float("belt_slice_rotation_angle")) > 1e-9; - bool shear_or_scale_active = is_belt && ( - m_config->option>("belt_shear_x")->value != BeltShearMode::None || - m_config->option>("belt_shear_y")->value != BeltShearMode::None || - m_config->option>("belt_shear_z")->value != BeltShearMode::None || - m_config->option>("belt_scale_x")->value != BeltScaleMode::None || - m_config->option>("belt_scale_y")->value != BeltScaleMode::None || - m_config->option>("belt_scale_z")->value != BeltScaleMode::None); - bool allow_shear_scale = !rotation_active; - bool allow_rotation = !shear_or_scale_active; - - // Disable shear/scale mode dropdowns when rotation is active. - for (auto el : {"belt_shear_x", "belt_shear_y", "belt_shear_z", - "belt_scale_x", "belt_scale_y", "belt_scale_z", - "belt_mesh_transform_order"}) - toggle_option(el, is_belt && allow_shear_scale); - - // Disable rotation controls when any shear/scale is active. - toggle_option("belt_slice_rotation", is_belt && allow_rotation); - toggle_option("belt_slice_rotation_angle", is_belt && allow_rotation && rot_axis != BeltRotationAxis::None); - toggle_option("belt_slice_rotation_global", is_belt && allow_rotation && rot_axis != BeltRotationAxis::None); - - // Gray out angle/from sub-options when their parent shear/scale mode is None. - // Per-axis globals are superseded when belt_preslice_global is on. - auto sx = m_config->option>("belt_shear_x")->value; - toggle_option("belt_shear_x_angle", is_belt && sx != BeltShearMode::None && allow_shear_scale); - toggle_option("belt_shear_x_from", is_belt && sx != BeltShearMode::None && allow_shear_scale); - toggle_option("belt_shear_x_global", is_belt && sx != BeltShearMode::None && !belt_global && allow_shear_scale); - - auto sy = m_config->option>("belt_shear_y")->value; - toggle_option("belt_shear_y_angle", is_belt && sy != BeltShearMode::None && allow_shear_scale); - toggle_option("belt_shear_y_from", is_belt && sy != BeltShearMode::None && allow_shear_scale); - toggle_option("belt_shear_y_global", is_belt && sy != BeltShearMode::None && !belt_global && allow_shear_scale); - - auto sz = m_config->option>("belt_shear_z")->value; - toggle_option("belt_shear_z_angle", is_belt && sz != BeltShearMode::None && allow_shear_scale); - toggle_option("belt_shear_z_from", is_belt && sz != BeltShearMode::None && allow_shear_scale); - toggle_option("belt_shear_z_global", is_belt && sz != BeltShearMode::None && !belt_global && allow_shear_scale); - - auto scx = m_config->option>("belt_scale_x")->value; - toggle_option("belt_scale_x_angle", is_belt && scx != BeltScaleMode::None && allow_shear_scale); - - auto scy = m_config->option>("belt_scale_y")->value; - toggle_option("belt_scale_y_angle", is_belt && scy != BeltScaleMode::None && allow_shear_scale); - - auto scz = m_config->option>("belt_scale_z")->value; - toggle_option("belt_scale_z_angle", is_belt && scz != BeltScaleMode::None && allow_shear_scale); + 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 transforms: shown only in belt mode. // Mirror the Advanced/Expert split used for mesh shear/scale. @@ -5703,8 +5590,7 @@ void TabPrinter::toggle_options() toggle_line("gcode_scale_x", is_belt && expert_or_above); toggle_line("gcode_scale_y", is_belt); toggle_line("gcode_scale_z", is_belt && expert_or_above); - for (auto el : {"belt_gcode_transform_order", "post_gcode_remap_x"}) - toggle_line(el, is_belt); + toggle_line("belt_gcode_transform_order", is_belt); auto gsx = m_config->option>("gcode_shear_x")->value; toggle_option("gcode_shear_x_angle", is_belt && gsx != BeltShearMode::None); @@ -5939,21 +5825,29 @@ void TabPrinter::update_fff() m_use_silent_mode = m_config->opt_bool("silent_mode"); } - // Belt printer: auto-sync build_plate_tilt_x to belt_printer_angle when belt mode is active. - // When belt mode is off, reset build_plate_tilt_x to 0 if it was set by belt mode. + // Belt printer: auto-sync build_plate_tilt_{x,y} (which drives support gravity tilt) + // from the belt slicing rotation, the single source of truth for the physical tilt. + // Tilt about X drives tilt_x, tilt about Y drives tilt_y. When belt mode is off, + // reset whichever tilt axis matches a leftover belt value so we don't clobber a + // manually-set tilt on a non-belt tilted printer. if (m_config->opt_bool("belt_printer")) { - double belt_angle = m_config->opt_float("belt_printer_angle"); - if (m_config->opt_float("build_plate_tilt_x") != belt_angle) { - m_config->set_key_value("build_plate_tilt_x", new ConfigOptionFloat(belt_angle)); - } + auto rot_axis = m_config->option>("belt_slice_rotation")->value; + const auto tilt = BeltTransformPipeline::physical_tilt( + rot_axis, m_config->opt_float("belt_slice_rotation_angle")); + if (m_config->opt_float("build_plate_tilt_x") != tilt.tilt_x_deg) + m_config->set_key_value("build_plate_tilt_x", new ConfigOptionFloat(tilt.tilt_x_deg)); + if (m_config->opt_float("build_plate_tilt_y") != tilt.tilt_y_deg) + m_config->set_key_value("build_plate_tilt_y", new ConfigOptionFloat(tilt.tilt_y_deg)); } else { - // Only reset if build_plate_tilt_x matches a typical belt angle (was set by auto-sync). - // Avoid clobbering a manually-set tilt value for non-belt tilted printers. - double current_tilt = m_config->opt_float("build_plate_tilt_x"); - double belt_angle = m_config->opt_float("belt_printer_angle"); - if (current_tilt != 0. && std::abs(current_tilt - belt_angle) < 0.01) { + const auto tilt = BeltTransformPipeline::physical_tilt( + m_config->option>("belt_slice_rotation")->value, + m_config->opt_float("belt_slice_rotation_angle")); + double tx = m_config->opt_float("build_plate_tilt_x"); + double ty = m_config->opt_float("build_plate_tilt_y"); + if (tx != 0. && std::abs(tx - tilt.tilt_x_deg) < 0.01) m_config->set_key_value("build_plate_tilt_x", new ConfigOptionFloat(0.)); - } + if (ty != 0. && std::abs(ty - tilt.tilt_y_deg) < 0.01) + m_config->set_key_value("build_plate_tilt_y", new ConfigOptionFloat(0.)); } toggle_options(); From 02d45c32584aeb72bdb8f55336568f9a24520ee4 Mon Sep 17 00:00:00 2001 From: Joseph Robertson <32232925+HarrierPigeon@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:40:45 -0500 Subject: [PATCH 036/224] Finish Fixes from Copilot Review (#39) * fix: restore BuildVolume bounds when toggling belt mode set_belt_printer() mutated m_bboxf when enabling but never restored the original extents on disable or when switching infinite_y true->false, leaving stale max.y/max.z values that broke collision and object_state checks. Recompute m_bboxf from m_bed_shape + m_max_print_height at the top of each call, then apply belt-specific adjustments on top. Addresses Copilot review comment on PR #12998 (BuildVolume.cpp:196). * chore: drop [BELT-DEBUG] to_machine_coords log to trace Was emitting at warning level once per 0.2mm Z bucket during every belt print export, polluting default user logs. Trace level matches the rest of the belt diagnostics and is silent in production. Addresses Copilot review comment on PR #12998 (BeltGCodeWriter.cpp:86). * chore: drop [BELTRACE] make_perimeters/support logs to trace Eight warning-level traces around make_perimeters and generate_support_material were emitting on every call/exit during normal slicing, cluttering default logs. They're concurrency-debug breadcrumbs not user-facing diagnostics, so drop them to trace. Addresses Copilot review comment on PR #12998 (PrintObject.cpp:438). * perf: gate BeltSliceStrategy diagnostic bbox tracking behind compile flag apply_to_trafo() walked every model vertex twice (once for min_z, once for per-volume mesh/slicer bboxes) and emitted seven trace logs per call. The bboxes and logs are diagnostic only; min_z is the load-bearing output. Wrap the bbox accumulation, logging, and supporting headers in SLIC3R_BELT_DIAGNOSTIC_LOG so production builds do the bare min_z scan. Addresses Copilot review comment on PR #12998 (BeltSliceStrategy.cpp:95). * fix: apply part_cooling_fan_min_pwm to first-layer plane fan crossings apply_first_layer_plane_fan_eval emitted band-crossing M106 commands through GCodeWriter::set_fan() without the per-printer PWM floor that every other set_fan call in CoolingBuffer applies. On printers with a non-zero part_cooling_fan_min_pwm, fans could fail to spin up at low requested speeds near the belt surface. Addresses Copilot review comment on PR #12998 (CoolingBuffer.cpp:1227). --- src/libslic3r/BeltGCodeWriter.cpp | 2 +- src/libslic3r/BeltSliceStrategy.cpp | 30 +++++++++++++++++++++++---- src/libslic3r/BuildVolume.cpp | 6 ++++++ src/libslic3r/GCode/CoolingBuffer.cpp | 6 +++++- src/libslic3r/PrintObject.cpp | 16 +++++++------- 5 files changed, 46 insertions(+), 14 deletions(-) diff --git a/src/libslic3r/BeltGCodeWriter.cpp b/src/libslic3r/BeltGCodeWriter.cpp index 751a7f19d8..9f1f32a375 100644 --- a/src/libslic3r/BeltGCodeWriter.cpp +++ b/src/libslic3r/BeltGCodeWriter.cpp @@ -76,7 +76,7 @@ Vec3d BeltGCodeWriter::to_machine_coords(const Vec3d &pos) const int z_bucket = static_cast(std::floor(pos.z() * 5.0)); // every 0.2mm if (z_bucket != s_last_logged_z) { s_last_logged_z = z_bucket; - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] to_machine_coords" + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] to_machine_coords" << " slicer_in=(" << pos.x() << "," << pos.y() << "," << pos.z() << ")" << " after_back=(" << after_back.x() << "," << after_back.y() << "," << after_back.z() << ")" << " after_remap=(" << after_remap.x() << "," << after_remap.y() << "," << after_remap.z() << ")" diff --git a/src/libslic3r/BeltSliceStrategy.cpp b/src/libslic3r/BeltSliceStrategy.cpp index ddaabdef0b..fd1bfcc191 100644 --- a/src/libslic3r/BeltSliceStrategy.cpp +++ b/src/libslic3r/BeltSliceStrategy.cpp @@ -2,9 +2,11 @@ #include "Model.hpp" #include +#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG #include #include #include +#endif namespace Slic3r { @@ -40,7 +42,8 @@ void BeltSliceStrategy::apply_to_trafo(Transform3d &trafo, // coordinates rather than object-space coordinates, so volumes translated // along the slicer's Z axis are silently excluded from the bound check. if (has_remap || m_has_rotation) { - // [BELT-DEBUG] Capture the incoming trafo for diagnostic logging. +#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG + // Capture the incoming trafo for diagnostic logging. // This is the slicer-frame transform AFTER belt_xform but BEFORE z_shift. const Transform3d trafo_pre_shift = trafo; auto log_mat = [](const Matrix3d &m) { @@ -63,29 +66,41 @@ void BeltSliceStrategy::apply_to_trafo(Transform3d &trafo, << " trafo.linear=" << log_mat(trafo_pre_shift.linear()) << " trafo.translation=" << log_vec3(trafo_pre_shift.translation()) << " volumes=" << model_volumes.size(); +#endif double min_z = std::numeric_limits::max(); +#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG int vol_idx = 0; +#endif for (const ModelVolume *mv : model_volumes) { +#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG if (!mv->is_model_part()) { ++vol_idx; continue; } +#else + if (!mv->is_model_part()) continue; +#endif Transform3d vol_trafo = trafo * mv->get_matrix(); - // [BELT-DEBUG] Per-volume bbox in mesh-frame and post-trafo slicer-frame. const auto &its = mv->mesh().its; +#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG + // Per-volume bbox in mesh-frame and post-trafo slicer-frame. Vec3d mesh_min(std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::max()); Vec3d mesh_max(std::numeric_limits::lowest(), std::numeric_limits::lowest(), std::numeric_limits::lowest()); Vec3d slicer_min(std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::max()); Vec3d slicer_max(std::numeric_limits::lowest(), std::numeric_limits::lowest(), std::numeric_limits::lowest()); double vol_min_z = std::numeric_limits::max(); +#endif for (const stl_vertex &v : its.vertices) { Vec3d vm = v.cast(); + Vec3d pt = vol_trafo * vm; + min_z = std::min(min_z, pt.z()); +#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG mesh_min = mesh_min.cwiseMin(vm); mesh_max = mesh_max.cwiseMax(vm); - Vec3d pt = vol_trafo * vm; slicer_min = slicer_min.cwiseMin(pt); slicer_max = slicer_max.cwiseMax(pt); vol_min_z = std::min(vol_min_z, pt.z()); - min_z = std::min(min_z, pt.z()); +#endif } +#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] vol[" << vol_idx << "] id=" << mv->id().id << " name='" << mv->name << "'" << " mesh_bbox_min=" << log_vec3(mesh_min) << " mesh_bbox_max=" << log_vec3(mesh_max) @@ -93,10 +108,13 @@ void BeltSliceStrategy::apply_to_trafo(Transform3d &trafo, << " slicer_bbox_min=" << log_vec3(slicer_min) << " slicer_bbox_max=" << log_vec3(slicer_max) << " vol_min_z=" << vol_min_z; ++vol_idx; +#endif } double belt_z_shift_val = (min_z < 0. && min_z != std::numeric_limits::max()) ? -min_z : 0.; +#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] combined min_z=" << min_z << " z_shift_val=" << belt_z_shift_val; +#endif if (belt_z_shift_val > 0.) { Transform3d z_shift = Transform3d::Identity(); z_shift.matrix()(2, 3) = belt_z_shift_val; @@ -104,13 +122,17 @@ void BeltSliceStrategy::apply_to_trafo(Transform3d &trafo, } if (out_belt_min_z) { double new_val = (min_z != std::numeric_limits::max()) ? min_z : 0.; +#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] write m_belt_min_z tid=" << std::this_thread::get_id() << " target=" << out_belt_min_z << " old=" << *out_belt_min_z << " new=" << new_val; +#endif *out_belt_min_z = new_val; } +#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] apply_to_trafo exit" << " final_trafo.linear=" << log_mat(trafo.linear()) << " final_trafo.translation=" << log_vec3(trafo.translation()); +#endif } } diff --git a/src/libslic3r/BuildVolume.cpp b/src/libslic3r/BuildVolume.cpp index d6fae8f0a8..2e194bdd7e 100644 --- a/src/libslic3r/BuildVolume.cpp +++ b/src/libslic3r/BuildVolume.cpp @@ -182,6 +182,12 @@ void BuildVolume::set_belt_printer(bool enabled, double angle_deg, bool infinite m_belt_angle = angle_deg; m_belt_infinite_y = infinite_y; + // Restart from the unmodified bbox each call. Without this, toggling + // belt mode off (or switching infinite_y true→false) would leave the + // extents inflated and break collision / object_state checks. + BoundingBoxf bboxf = get_extents(m_bed_shape); + m_bboxf = BoundingBoxf3{ to_3d(bboxf.min, 0.), to_3d(bboxf.max, m_max_print_height) }; + if (enabled) { if (infinite_y) { // Extend the Y bound to a very large value for infinite belt. diff --git a/src/libslic3r/GCode/CoolingBuffer.cpp b/src/libslic3r/GCode/CoolingBuffer.cpp index a9991f6f62..a92bb7e4cf 100644 --- a/src/libslic3r/GCode/CoolingBuffer.cpp +++ b/src/libslic3r/GCode/CoolingBuffer.cpp @@ -1091,6 +1091,10 @@ std::string CoolingBuffer::apply_first_layer_plane_fan_eval( std::string out; out.reserve(gcode.size() + 256); + // Match the PWM floor applied at every other set_fan call in this file so + // band-crossing M106 emissions start the fan reliably at low speeds. + const unsigned int part_cooling_fan_min_pwm = static_cast(std::max(0, m_config.part_cooling_fan_min_pwm.value)); + // Track position in slicing-frame mm. Seed from m_current_pos which the // CoolingBuffer keeps up-to-date across layers. Vec3d cur_pos_mm(m_current_pos[0], m_current_pos[1], m_current_pos[2]); @@ -1221,7 +1225,7 @@ std::string CoolingBuffer::apply_first_layer_plane_fan_eval( target_fan = pre_band_main_fan; } if (target_fan != current_main_fan) { - out += GCodeWriter::set_fan(m_config.gcode_flavor, target_fan); + out += GCodeWriter::set_fan(m_config.gcode_flavor, target_fan, part_cooling_fan_min_pwm); current_main_fan = target_fan; m_fan_speed = target_fan; m_current_fan_speed = target_fan; diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 31565b9a6b..54a3f7bdb9 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -427,15 +427,15 @@ std::vector> PrintObject::detect_extruder_geometric_unprintables() // 3) Generates perimeters, gap fills and fill regions (fill regions of type stInternal). void PrintObject::make_perimeters() { - BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] make_perimeters request tid=" << std::this_thread::get_id() << " obj=" << this; + BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] make_perimeters request tid=" << std::this_thread::get_id() << " obj=" << this; // prerequisites this->slice(); if (! this->set_started(posPerimeters)) { - BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] make_perimeters SKIP tid=" << std::this_thread::get_id() << " obj=" << this << " (already started/done)"; + BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] make_perimeters SKIP tid=" << std::this_thread::get_id() << " obj=" << this << " (already started/done)"; return; } - BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] make_perimeters ENTER tid=" << std::this_thread::get_id() << " obj=" << this; + BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] make_perimeters ENTER tid=" << std::this_thread::get_id() << " obj=" << this; m_print->set_status(15, L("Generating walls")); BOOST_LOG_TRIVIAL(info) << "Generating walls..." << log_memory_info(); @@ -533,7 +533,7 @@ void PrintObject::make_perimeters() m_print->throw_if_canceled(); BOOST_LOG_TRIVIAL(debug) << "Generating perimeters in parallel - end"; - BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] make_perimeters EXIT tid=" << std::this_thread::get_id() << " obj=" << this; + BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] make_perimeters EXIT tid=" << std::this_thread::get_id() << " obj=" << this; this->set_done(posPerimeters); } @@ -829,9 +829,9 @@ void PrintObject::detect_overhangs_for_lift() void PrintObject::generate_support_material() { - BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] generate_support_material request tid=" << std::this_thread::get_id() << " obj=" << this; + BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] generate_support_material request tid=" << std::this_thread::get_id() << " obj=" << this; if (this->set_started(posSupportMaterial)) { - BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] generate_support_material ENTER tid=" << std::this_thread::get_id() << " obj=" << this; + BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] generate_support_material ENTER tid=" << std::this_thread::get_id() << " obj=" << this; this->clear_support_layers(); if(!has_support() && !m_print->get_no_check_flag()) { @@ -872,10 +872,10 @@ void PrintObject::generate_support_material() this->_generate_support_material(); m_print->throw_if_canceled(); } - BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] generate_support_material EXIT tid=" << std::this_thread::get_id() << " obj=" << this; + BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] generate_support_material EXIT tid=" << std::this_thread::get_id() << " obj=" << this; this->set_done(posSupportMaterial); } else { - BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] generate_support_material SKIP tid=" << std::this_thread::get_id() << " obj=" << this << " (already started/done)"; + BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] generate_support_material SKIP tid=" << std::this_thread::get_id() << " obj=" << this << " (already started/done)"; } } From 573e1c65441c0c88cc219a1844567608e31a1b35 Mon Sep 17 00:00:00 2001 From: Joseph Robertson <32232925+HarrierPigeon@users.noreply.github.com> Date: Fri, 5 Jun 2026 03:11:38 -0500 Subject: [PATCH 037/224] Belt/fix profiles and minor oopsies (#42) * fix duplicate printer, bump version * clean up extra tab in space * fix generic defaults --- resources/profiles/Custom.json | 6 +----- resources/profiles/Custom/machine/fdm_belt_common.json | 1 + src/libslic3r/Preset.cpp | 5 ++--- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/resources/profiles/Custom.json b/resources/profiles/Custom.json index 602631c7ff..ba1ddc14f4 100644 --- a/resources/profiles/Custom.json +++ b/resources/profiles/Custom.json @@ -1,6 +1,6 @@ { "name": "Custom Printer", - "version": "02.04.00.00", + "version": "02.04.00.03", "force_update": "0", "description": "My configurations", "machine_model_list": [ @@ -298,10 +298,6 @@ "name": "MyRepetier 0.4 nozzle", "sub_path": "machine/MyRepetier 0.4 nozzle.json" }, - { - "name": "MyRRF 0.4 nozzle", - "sub_path": "machine/MyRRF 0.4 nozzle.json" - }, { "name": "MyToolChanger 0.2 nozzle", "sub_path": "machine/MyToolChanger 0.2 nozzle.json" diff --git a/resources/profiles/Custom/machine/fdm_belt_common.json b/resources/profiles/Custom/machine/fdm_belt_common.json index 545ad15846..0530999f15 100644 --- a/resources/profiles/Custom/machine/fdm_belt_common.json +++ b/resources/profiles/Custom/machine/fdm_belt_common.json @@ -92,6 +92,7 @@ "belt_slice_rotation": "x", "belt_slice_rotation_angle": "45", "belt_slice_rotation_global": "1", + "build_plate_tilt_x": "45", "gcode_shear_z": "pos_tan", "gcode_scale_y": "inv_cos", "purge_in_prime_tower": "0", diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 5aab208049..2fc76a4998 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1323,8 +1323,7 @@ static std::vector s_Preset_machine_limits_options { static std::vector 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", + "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", @@ -1341,7 +1340,7 @@ static std::vector s_Preset_printer_options { "belt_origin_snap_y", "belt_origin_offset_y", "belt_origin_snap_z", "belt_origin_offset_z", "belt_support_floor_offset", "belt_support_floor_mode", "belt_support_z_offset_mode", - "gcode_flavor", + "gcode_flavor", "fan_kickstart", "part_cooling_fan_min_pwm", "fan_speedup_time", "fan_speedup_overhangs", "single_extruder_multi_material", "manual_filament_change", "file_start_gcode", "machine_start_gcode", "machine_end_gcode", "before_layer_change_gcode", "printing_by_object_gcode", "layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode", "change_filament_gcode", "change_extrusion_role_gcode", "printer_model", "printer_variant", "printer_extruder_id", "printer_extruder_variant", "extruder_variant_list", "default_nozzle_volume_type", From 6ed24378484cb12bf83ed831024cbe1da1d8fac5 Mon Sep 17 00:00:00 2001 From: Tommaso Bianchi <163908227+tommasobbianchi@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:13:56 +0200 Subject: [PATCH 038/224] Add IdeaFormer IR3 V2 belt printer profile - credit: tommasobbianchi (#43) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add IdeaFormer IR3 V2 belt printer profile Self-contained vendor profile for the IdeaFormer IR3 V2 (45 deg belt printer): machine (0.4 nozzle) + 0.20mm process + Generic PLA/PETG filaments, with the belt machine-frame transforms set explicitly on the machine preset (belt_printer, belt_slice_rotation x/45/global, build_plate_tilt_x=45, gcode_remap_x/y/z, gcode_shear_z=pos_tan, gcode_scale_y=inv_cos). The vendor bundles its own machine/process commons (fdm_belt_common, fdm_klipper_common, fdm_machine_common, fdm_process_common) on purpose: OrcaSlicer resolves system-preset inheritance per-vendor, so a profile that inherits the Custom vendor's commons cross-vendor fails to resolve its parent and the whole IdeaFormer vendor silently fails to load. Bundling the commons (and listing them in IdeaFormer.json in dependency order) keeps the vendor self-contained, matching how every other vendor folder is structured. Machine limits, bed temperature (75 C for belt PLA) and start/end G-code are taken from a working IdeaFormer IR3 V2. * feat(belt/profile): eSUN PLA @IdeaFormer IR3 V2 — HW-calibrated belt filament Add an eSUN PLA belt profile for the IR3 V2, inheriting Generic PLA @IdeaFormer IR3 V2 (self-contained: parent is in the same IdeaFormer vendor, registered after it in filament_list). HW-calibrated on the IR3 V2: - nozzle_temperature 200/200 (temp-tower calibration) - pressure_advance 0.12 (PA calibration) - filament_max_volumetric_speed 10 mm³/s (max-vol-speed calibration: wall failed at 126 mm/s → 126 × 0.0798 mm³/mm ≈ 10 mm³/s) --- resources/profiles/IdeaFormer.json | 54 +++++++ .../IdeaFormer/IdeaFormer IR3 V2_cover.png | Bin 0 -> 187734 bytes .../Generic PETG @IdeaFormer IR3 V2.json | 112 ++++++++++++++ .../Generic PLA @IdeaFormer IR3 V2.json | 112 ++++++++++++++ .../filament/eSUN PLA @IdeaFormer IR3 V2.json | 34 +++++ .../machine/IdeaFormer IR3 V2 0.4 nozzle.json | 105 +++++++++++++ .../IdeaFormer/machine/IdeaFormer IR3 V2.json | 12 ++ .../IdeaFormer/machine/fdm_belt_common.json | 101 +++++++++++++ .../machine/fdm_klipper_common.json | 141 ++++++++++++++++++ .../machine/fdm_machine_common.json | 119 +++++++++++++++ .../0.20mm Standard @IdeaFormer IR3 V2.json | 21 +++ .../process/fdm_process_common.json | 108 ++++++++++++++ 12 files changed, 919 insertions(+) create mode 100644 resources/profiles/IdeaFormer.json create mode 100644 resources/profiles/IdeaFormer/IdeaFormer IR3 V2_cover.png create mode 100644 resources/profiles/IdeaFormer/filament/Generic PETG @IdeaFormer IR3 V2.json create mode 100644 resources/profiles/IdeaFormer/filament/Generic PLA @IdeaFormer IR3 V2.json create mode 100644 resources/profiles/IdeaFormer/filament/eSUN PLA @IdeaFormer IR3 V2.json create mode 100644 resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json create mode 100644 resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2.json create mode 100644 resources/profiles/IdeaFormer/machine/fdm_belt_common.json create mode 100644 resources/profiles/IdeaFormer/machine/fdm_klipper_common.json create mode 100644 resources/profiles/IdeaFormer/machine/fdm_machine_common.json create mode 100644 resources/profiles/IdeaFormer/process/0.20mm Standard @IdeaFormer IR3 V2.json create mode 100644 resources/profiles/IdeaFormer/process/fdm_process_common.json diff --git a/resources/profiles/IdeaFormer.json b/resources/profiles/IdeaFormer.json new file mode 100644 index 0000000000..7e3e108c3d --- /dev/null +++ b/resources/profiles/IdeaFormer.json @@ -0,0 +1,54 @@ +{ + "name": "IdeaFormer", + "version": "02.00.00.00", + "force_update": "0", + "description": "IdeaFormer belt printer configurations", + "machine_model_list": [ + { + "name": "IdeaFormer IR3 V2", + "sub_path": "machine/IdeaFormer IR3 V2.json" + } + ], + "process_list": [ + { + "name": "fdm_process_common", + "sub_path": "process/fdm_process_common.json" + }, + { + "name": "0.20mm Standard @IdeaFormer IR3 V2", + "sub_path": "process/0.20mm Standard @IdeaFormer IR3 V2.json" + } + ], + "filament_list": [ + { + "name": "Generic PLA @IdeaFormer IR3 V2", + "sub_path": "filament/Generic PLA @IdeaFormer IR3 V2.json" + }, + { + "name": "eSUN PLA @IdeaFormer IR3 V2", + "sub_path": "filament/eSUN PLA @IdeaFormer IR3 V2.json" + }, + { + "name": "Generic PETG @IdeaFormer IR3 V2", + "sub_path": "filament/Generic PETG @IdeaFormer IR3 V2.json" + } + ], + "machine_list": [ + { + "name": "fdm_machine_common", + "sub_path": "machine/fdm_machine_common.json" + }, + { + "name": "fdm_klipper_common", + "sub_path": "machine/fdm_klipper_common.json" + }, + { + "name": "fdm_belt_common", + "sub_path": "machine/fdm_belt_common.json" + }, + { + "name": "IdeaFormer IR3 V2 0.4 nozzle", + "sub_path": "machine/IdeaFormer IR3 V2 0.4 nozzle.json" + } + ] +} \ No newline at end of file diff --git a/resources/profiles/IdeaFormer/IdeaFormer IR3 V2_cover.png b/resources/profiles/IdeaFormer/IdeaFormer IR3 V2_cover.png new file mode 100644 index 0000000000000000000000000000000000000000..71485f54f5e6ef9718dc75c15074e024bcc33521 GIT binary patch literal 187734 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%0D$UT-uWJzIjt@%SEI^#J|{6 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
C3_L%_7FAVMB^t$#GuWjE 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
^_raqh`Us~Q@~=bR~X#_R$TGq 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

~76lX`owCq9-Lz?Mn!ATfhg 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 literal 0 HcmV?d00001 diff --git a/resources/profiles/IdeaFormer/filament/Generic PETG @IdeaFormer IR3 V2.json b/resources/profiles/IdeaFormer/filament/Generic PETG @IdeaFormer IR3 V2.json new file mode 100644 index 0000000000..c51b20abbc --- /dev/null +++ b/resources/profiles/IdeaFormer/filament/Generic PETG @IdeaFormer IR3 V2.json @@ -0,0 +1,112 @@ +{ + "type": "filament", + "name": "Generic PETG @IdeaFormer IR3 V2", + "inherits": "Generic PETG @System", + "from": "system", + "instantiation": "true", + "compatible_printers": [ + "IdeaFormer IR3 V2 0.4 nozzle" + ], + "filament_type": [ + "PETG" + ], + "filament_vendor": [ + "Generic" + ], + "filament_settings_id": [ + "Generic PETG @IdeaFormer IR3 V2" + ], + "filament_diameter": [ + "1.75" + ], + "filament_density": [ + "1.27" + ], + "filament_flow_ratio": [ + "0.95" + ], + "filament_cost": [ + "25" + ], + "filament_max_volumetric_speed": [ + "10" + ], + "nozzle_temperature": [ + "240" + ], + "nozzle_temperature_initial_layer": [ + "245" + ], + "nozzle_temperature_range_low": [ + "220" + ], + "nozzle_temperature_range_high": [ + "260" + ], + "temperature_vitrification": [ + "70" + ], + "hot_plate_temp": [ + "80" + ], + "hot_plate_temp_initial_layer": [ + "80" + ], + "cool_plate_temp": [ + "80" + ], + "cool_plate_temp_initial_layer": [ + "80" + ], + "textured_plate_temp": [ + "80" + ], + "textured_plate_temp_initial_layer": [ + "80" + ], + "fan_min_speed": [ + "40" + ], + "fan_max_speed": [ + "60" + ], + "overhang_fan_threshold": [ + "25%" + ], + "overhang_fan_speed": [ + "80" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "full_fan_speed_layer": [ + "8" + ], + "slow_down_min_speed": [ + "20" + ], + "slow_down_layer_time": [ + "4" + ], + "fan_cooling_layer_time": [ + "100" + ], + "reduce_fan_stop_start_freq": [ + "1" + ], + "filament_retraction_length": [ + "2" + ], + "filament_retraction_speed": [ + "40" + ], + "filament_deretraction_speed": [ + "40" + ], + "filament_z_hop": [ + "0.4" + ], + "filament_start_gcode": [ + "; Generic PETG @IdeaFormer IR3 V2 — belt PETG, bed 80C" + ] +} diff --git a/resources/profiles/IdeaFormer/filament/Generic PLA @IdeaFormer IR3 V2.json b/resources/profiles/IdeaFormer/filament/Generic PLA @IdeaFormer IR3 V2.json new file mode 100644 index 0000000000..c86ba651da --- /dev/null +++ b/resources/profiles/IdeaFormer/filament/Generic PLA @IdeaFormer IR3 V2.json @@ -0,0 +1,112 @@ +{ + "type": "filament", + "name": "Generic PLA @IdeaFormer IR3 V2", + "inherits": "Generic PLA @System", + "from": "system", + "instantiation": "true", + "compatible_printers": [ + "IdeaFormer IR3 V2 0.4 nozzle" + ], + "filament_type": [ + "PLA" + ], + "filament_vendor": [ + "Generic" + ], + "filament_settings_id": [ + "Generic PLA @IdeaFormer IR3 V2" + ], + "filament_diameter": [ + "1.75" + ], + "filament_density": [ + "1.24" + ], + "filament_flow_ratio": [ + "0.98" + ], + "filament_cost": [ + "20" + ], + "filament_max_volumetric_speed": [ + "12" + ], + "nozzle_temperature": [ + "215" + ], + "nozzle_temperature_initial_layer": [ + "220" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "temperature_vitrification": [ + "45" + ], + "hot_plate_temp": [ + "75" + ], + "hot_plate_temp_initial_layer": [ + "75" + ], + "cool_plate_temp": [ + "75" + ], + "cool_plate_temp_initial_layer": [ + "75" + ], + "textured_plate_temp": [ + "75" + ], + "textured_plate_temp_initial_layer": [ + "75" + ], + "fan_min_speed": [ + "100" + ], + "fan_max_speed": [ + "100" + ], + "overhang_fan_threshold": [ + "50%" + ], + "overhang_fan_speed": [ + "100" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "full_fan_speed_layer": [ + "8" + ], + "slow_down_min_speed": [ + "20" + ], + "slow_down_layer_time": [ + "4" + ], + "fan_cooling_layer_time": [ + "100" + ], + "reduce_fan_stop_start_freq": [ + "1" + ], + "filament_retraction_length": [ + "1.5" + ], + "filament_retraction_speed": [ + "35" + ], + "filament_deretraction_speed": [ + "30" + ], + "filament_z_hop": [ + "0.4" + ], + "filament_start_gcode": [ + "; Generic PLA @IdeaFormer IR3 V2 — belt PLA, bed 75C" + ] +} diff --git a/resources/profiles/IdeaFormer/filament/eSUN PLA @IdeaFormer IR3 V2.json b/resources/profiles/IdeaFormer/filament/eSUN PLA @IdeaFormer IR3 V2.json new file mode 100644 index 0000000000..c438aac0fd --- /dev/null +++ b/resources/profiles/IdeaFormer/filament/eSUN PLA @IdeaFormer IR3 V2.json @@ -0,0 +1,34 @@ +{ + "type": "filament", + "name": "eSUN PLA @IdeaFormer IR3 V2", + "inherits": "Generic PLA @IdeaFormer IR3 V2", + "from": "system", + "instantiation": "true", + "compatible_printers": [ + "IdeaFormer IR3 V2 0.4 nozzle" + ], + "filament_type": [ + "PLA" + ], + "filament_vendor": [ + "eSUN" + ], + "filament_settings_id": [ + "eSUN PLA @IdeaFormer IR3 V2" + ], + "nozzle_temperature_initial_layer": [ + "200" + ], + "nozzle_temperature": [ + "200" + ], + "enable_pressure_advance": [ + "1" + ], + "pressure_advance": [ + "0.12" + ], + "filament_max_volumetric_speed": [ + "10" + ] +} diff --git a/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json b/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json new file mode 100644 index 0000000000..b1a2cc2c3b --- /dev/null +++ b/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json @@ -0,0 +1,105 @@ +{ + "type": "machine", + "name": "IdeaFormer IR3 V2 0.4 nozzle", + "inherits": "fdm_belt_common", + "from": "system", + "setting_id": "GMIF001", + "instantiation": "true", + "printer_model": "IdeaFormer IR3 V2", + "printer_variant": "0.4", + "nozzle_diameter": [ + "0.4" + ], + "printable_area": [ + "0x0", + "250x0", + "250x2000", + "0x2000" + ], + "printable_height": "250", + "belt_printer": "1", + "belt_printer_infinite_y": "1", + "belt_slice_rotation": "x", + "belt_slice_rotation_angle": "45", + "belt_slice_rotation_global": "1", + "build_plate_tilt_x": "45", + "build_plate_tilt_y": "0", + "gcode_remap_x": "rev_x", + "gcode_remap_y": "pos_z", + "gcode_remap_z": "pos_y", + "gcode_shear_z": "pos_tan", + "gcode_scale_y": "inv_cos", + "thumbnails": [ + "48x48/PNG", + "300x300/PNG" + ], + "default_filament_profile": [ + "Generic PLA @IdeaFormer IR3 V2" + ], + "default_print_profile": "0.20mm Standard @IdeaFormer IR3 V2", + "use_relative_e_distances": "1", + "machine_max_acceleration_e": [ + "5000" + ], + "machine_max_acceleration_extruding": [ + "5000" + ], + "machine_max_acceleration_retracting": [ + "1000" + ], + "machine_max_acceleration_travel": [ + "9000" + ], + "machine_max_acceleration_x": [ + "5000" + ], + "machine_max_acceleration_y": [ + "5000" + ], + "machine_max_acceleration_z": [ + "100" + ], + "machine_max_jerk_e": [ + "2.5" + ], + "machine_max_jerk_x": [ + "10" + ], + "machine_max_jerk_y": [ + "10" + ], + "machine_max_jerk_z": [ + "0.4" + ], + "machine_max_speed_e": [ + "60" + ], + "machine_max_speed_x": [ + "500" + ], + "machine_max_speed_y": [ + "500" + ], + "machine_max_speed_z": [ + "20" + ], + "retraction_length": [ + "2" + ], + "retraction_speed": [ + "40" + ], + "deretraction_speed": [ + "40" + ], + "z_hop": [ + "0.4" + ], + "retract_lift_below": [ + "300" + ], + "machine_start_gcode": "; === IdeaFormer IR3 V2 Belt Printer Start ===\n; Axes: X=lateral, Y=gantry height (probe), Z=belt\nG90 ; absolute positioning\nM82 ; absolute extruder\nG21 ; millimeters\nG28 ; home all axes\nG1 Y20 F500 ; lift nozzle 20mm from belt\n; Bed + hotend temps come from the active filament profile. Belt PLA requires 75 C bed — use Generic/eSun PLA @IdeaFormer IR3 V2 filament presets to get it automatically.\nM140 S[hot_plate_temp_initial_layer] ; set bed temp\nM104 S[nozzle_temperature_initial_layer] ; hotend temp\nM109 S[nozzle_temperature_initial_layer] ; wait hotend\nM190 S[hot_plate_temp_initial_layer] ; wait bed\n; --- Purge blob ---\nG92 E0 ; zero extruder\nG1 Y.1 ; nozzle 0.1mm above belt\nG1 E15 F1000 ; purge 15mm blob\nG1 Z20 E25 F800 ; belt advance 20mm + extrude\nG1 E23 ; retract 2mm\nG28 Y ; re-probe belt surface\nG1 E25 ; de-retract\n; --- Prime lines (full 250mm bed width) ---\nFMS_on ; filament motion sensor\nG1 X250 E50 F2000 ; prime line 1\nG92 Z0 ; reset belt origin\nG1 Z.4 ; belt advance 0.4mm\nG1 X0 E75 ; prime line 2\nG1 F1000 ; default feedrate\nG92 E0 Z0 ; zero extruder + belt = print origin\n", + "machine_end_gcode": "; === IdeaFormer IR3 V2 Belt Printer End ===\nM400 ; wait for moves to finish\nG28 X ; home X axis\nM104 S0 ; heater off\nM140 S0 ; bed off\nG92 E0 ; zero extruder\nG1 E-5 F300 ; retract 5mm\nG4 P7000 ; wait 7s for ooze\nG28 ; home all\nG1 Y50 F1000 ; lift nozzle 50mm\nFMS_off ; filament motion sensor off\nBED_MESH_CLEAR\nM84 ; disable motors\n", + "machine_pause_gcode": "PAUSE", + "layer_change_gcode": "G92 E0 ; belt: reset extruder at layer change (relative E)" +} diff --git a/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2.json b/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2.json new file mode 100644 index 0000000000..bc6d1a6e77 --- /dev/null +++ b/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2.json @@ -0,0 +1,12 @@ +{ + "type": "machine_model", + "name": "IdeaFormer IR3 V2", + "model_id": "IdeaFormer_IR3_V2", + "nozzle_diameter": "0.4", + "machine_tech": "FFF", + "family": "IdeaFormer", + "bed_model": "", + "bed_texture": "", + "hotend_model": "", + "default_materials": "Generic PLA @IdeaFormer IR3 V2;Generic PETG @IdeaFormer IR3 V2" +} diff --git a/resources/profiles/IdeaFormer/machine/fdm_belt_common.json b/resources/profiles/IdeaFormer/machine/fdm_belt_common.json new file mode 100644 index 0000000000..0530999f15 --- /dev/null +++ b/resources/profiles/IdeaFormer/machine/fdm_belt_common.json @@ -0,0 +1,101 @@ +{ + "type": "machine", + "name": "fdm_belt_common", + "inherits": "fdm_klipper_common", + "from": "system", + "instantiation": "false", + "gcode_flavor": "klipper", + "single_extruder_multi_material": "0", + "default_filament_profile": [ + "Generic PLA @System" + ], + "default_print_profile": "0.20mm Standard @System", + "max_layer_height": [ + "0.32" + ], + "min_layer_height": [ + "0.08" + ], + "deretraction_speed": [ + "30" + ], + "extruder_colour": [ + "#FCE94F" + ], + "extruder_offset": [ + "0x0" + ], + "long_retractions_when_cut": [ + "0" + ], + "nozzle_diameter": [ + "0.4" + ], + "retract_before_wipe": [ + "70%" + ], + "retract_length_toolchange": [ + "2" + ], + "retract_lift_above": [ + "0" + ], + "retract_lift_below": [ + "0" + ], + "retract_lift_enforce": [ + "All Surfaces" + ], + "retract_restart_extra": [ + "0" + ], + "retract_restart_extra_toolchange": [ + "0" + ], + "retract_when_changing_layer": [ + "1" + ], + "retraction_distances_when_cut": [ + "18" + ], + "retraction_length": [ + "0.8" + ], + "retraction_minimum_travel": [ + "1" + ], + "retraction_speed": [ + "30" + ], + "travel_slope": [ + "3" + ], + "wipe": [ + "1" + ], + "wipe_distance": [ + "1" + ], + "z_hop": [ + "0.4" + ], + "z_hop_types": [ + "Normal Lift" + ], + "gcode_remap_x": "rev_x", + "gcode_remap_y": "pos_z", + "gcode_remap_z": "pos_y", + "printer_extruder_id": [ + "1" + ], + "belt_printer": "1", + "belt_slice_rotation": "x", + "belt_slice_rotation_angle": "45", + "belt_slice_rotation_global": "1", + "build_plate_tilt_x": "45", + "gcode_shear_z": "pos_tan", + "gcode_scale_y": "inv_cos", + "purge_in_prime_tower": "0", + "scan_first_layer": "0", + "auxiliary_fan": "0" +} diff --git a/resources/profiles/IdeaFormer/machine/fdm_klipper_common.json b/resources/profiles/IdeaFormer/machine/fdm_klipper_common.json new file mode 100644 index 0000000000..36f3fe13c6 --- /dev/null +++ b/resources/profiles/IdeaFormer/machine/fdm_klipper_common.json @@ -0,0 +1,141 @@ +{ + "type": "machine", + "name": "fdm_klipper_common", + "inherits": "fdm_machine_common", + "from": "system", + "instantiation": "false", + "gcode_flavor": "klipper", + "machine_max_acceleration_e": [ + "5000", + "5000" + ], + "machine_max_acceleration_extruding": [ + "20000", + "20000" + ], + "machine_max_acceleration_retracting": [ + "5000", + "5000" + ], + "machine_max_acceleration_travel": [ + "20000", + "20000" + ], + "machine_max_acceleration_x": [ + "20000", + "20000" + ], + "machine_max_acceleration_y": [ + "20000", + "20000" + ], + "machine_max_acceleration_z": [ + "500", + "200" + ], + "machine_max_speed_e": [ + "25", + "25" + ], + "machine_max_speed_x": [ + "500", + "200" + ], + "machine_max_speed_y": [ + "500", + "200" + ], + "machine_max_speed_z": [ + "12", + "12" + ], + "machine_max_jerk_e": [ + "2.5", + "2.5" + ], + "machine_max_jerk_x": [ + "9", + "9" + ], + "machine_max_jerk_y": [ + "9", + "9" + ], + "machine_max_jerk_z": [ + "0.2", + "0.4" + ], + "machine_min_extruding_rate": [ + "0", + "0" + ], + "machine_min_travel_rate": [ + "0", + "0" + ], + "max_layer_height": [ + "0.32" + ], + "min_layer_height": [ + "0.08" + ], + "printable_height": "250", + "extruder_clearance_radius": "65", + "extruder_clearance_height_to_rod": "36", + "extruder_clearance_height_to_lid": "140", + "printer_settings_id": "", + "printer_technology": "FFF", + "printer_variant": "0.4", + "retraction_minimum_travel": [ + "1" + ], + "retract_before_wipe": [ + "70%" + ], + "retract_when_changing_layer": [ + "1" + ], + "retraction_length": [ + "0.8" + ], + "retract_length_toolchange": [ + "2" + ], + "z_hop": [ + "0.4" + ], + "retract_restart_extra": [ + "0" + ], + "retract_restart_extra_toolchange": [ + "0" + ], + "retraction_speed": [ + "30" + ], + "deretraction_speed": [ + "30" + ], + "z_hop_types": "Normal Lift", + "silent_mode": "0", + "single_extruder_multi_material": "1", + "change_filament_gcode": "", + "wipe": [ + "1" + ], + "default_filament_profile": [ + "Generic PLA @System" + ], + "default_print_profile": "0.20mm Standard @MyKlipper", + "bed_exclude_area": [ + "0x0" + ], + "machine_start_gcode": "M190 S[bed_temperature_initial_layer_single]\nM109 S[nozzle_temperature_initial_layer]\nPRINT_START EXTRUDER=[nozzle_temperature_initial_layer] BED=[bed_temperature_initial_layer_single]\n", + "machine_end_gcode": "PRINT_END", + "layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]", + "before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n", + "machine_pause_gcode": "PAUSE", + "scan_first_layer": "0", + "nozzle_type": "undefine", + "auxiliary_fan": "0" +} diff --git a/resources/profiles/IdeaFormer/machine/fdm_machine_common.json b/resources/profiles/IdeaFormer/machine/fdm_machine_common.json new file mode 100644 index 0000000000..963f39c9d8 --- /dev/null +++ b/resources/profiles/IdeaFormer/machine/fdm_machine_common.json @@ -0,0 +1,119 @@ +{ + "type": "machine", + "name": "fdm_machine_common", + "from": "system", + "instantiation": "false", + "printer_technology": "FFF", + "deretraction_speed": [ + "40" + ], + "extruder_colour": [ + "#FCE94F" + ], + "extruder_offset": [ + "0x0" + ], + "gcode_flavor": "marlin", + "silent_mode": "0", + "machine_max_acceleration_e": [ + "5000" + ], + "machine_max_acceleration_extruding": [ + "10000" + ], + "machine_max_acceleration_retracting": [ + "1000" + ], + "machine_max_acceleration_x": [ + "10000" + ], + "machine_max_acceleration_y": [ + "10000" + ], + "machine_max_acceleration_z": [ + "500" + ], + "machine_max_speed_e": [ + "60" + ], + "machine_max_speed_x": [ + "500" + ], + "machine_max_speed_y": [ + "500" + ], + "machine_max_speed_z": [ + "10" + ], + "machine_max_jerk_e": [ + "5" + ], + "machine_max_jerk_x": [ + "8" + ], + "machine_max_jerk_y": [ + "8" + ], + "machine_max_jerk_z": [ + "0.4" + ], + "machine_min_extruding_rate": [ + "0" + ], + "machine_min_travel_rate": [ + "0" + ], + "max_layer_height": [ + "0.32" + ], + "min_layer_height": [ + "0.08" + ], + "printable_height": "250", + "extruder_clearance_radius": "65", + "extruder_clearance_height_to_rod": "36", + "extruder_clearance_height_to_lid": "140", + "nozzle_diameter": [ + "0.4" + ], + "printer_settings_id": "", + "printer_variant": "0.4", + "retraction_minimum_travel": [ + "2" + ], + "retract_before_wipe": [ + "70%" + ], + "retract_when_changing_layer": [ + "1" + ], + "retraction_length": [ + "1" + ], + "retract_length_toolchange": [ + "1" + ], + "z_hop": [ + "0" + ], + "retract_restart_extra": [ + "0" + ], + "retract_restart_extra_toolchange": [ + "0" + ], + "retraction_speed": [ + "60" + ], + "single_extruder_multi_material": "1", + "change_filament_gcode": "", + "wipe": [ + "1" + ], + "default_print_profile": "", + "machine_start_gcode": "G0 Z20 F9000\nG92 E0; G1 E-10 F1200\nG28\nM970 Q1 A10 B10 C130 K0\nM970 Q1 A10 B131 C250 K1\nM974 Q1 S1 P0\nM970 Q0 A10 B10 C130 H20 K0\nM970 Q0 A10 B131 C250 K1\nM974 Q0 S1 P0\nM220 S100 ;Reset Feedrate\nM221 S100 ;Reset Flowrate\nG29 ;Home\nG90;\nG92 E0 ;Reset Extruder \nG1 Z2.0 F3000 ;Move Z Axis up \nG1 X10.1 Y20 Z0.28 F5000.0 ;Move to start position\nM109 S205;\nG1 X10.1 Y200.0 Z0.28 F1500.0 E15 ;Draw the first line\nG1 X10.4 Y200.0 Z0.28 F5000.0 ;Move to side a little\nG1 X10.4 Y20 Z0.28 F1500.0 E30 ;Draw the second line\nG92 E0 ;Reset Extruder \nG1 X110 Y110 Z2.0 F3000 ;Move Z Axis up", + "machine_end_gcode": "M400 ; wait for buffer to clear\nG92 E0 ; zero the extruder\nG1 E-4.0 F3600; retract \nG91\nG1 Z3;\nM104 S0 ; turn off hotend\nM140 S0 ; turn off bed\nM106 S0 ; turn off fan\nG90 \nG0 X110 Y200 F3600 \nprint_end", + "layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]", + "before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n", + "machine_pause_gcode": "M601" +} diff --git a/resources/profiles/IdeaFormer/process/0.20mm Standard @IdeaFormer IR3 V2.json b/resources/profiles/IdeaFormer/process/0.20mm Standard @IdeaFormer IR3 V2.json new file mode 100644 index 0000000000..f8e8a69e0f --- /dev/null +++ b/resources/profiles/IdeaFormer/process/0.20mm Standard @IdeaFormer IR3 V2.json @@ -0,0 +1,21 @@ +{ + "type": "process", + "name": "0.20mm Standard @IdeaFormer IR3 V2", + "inherits": "fdm_process_common", + "from": "system", + "instantiation": "true", + "layer_height": "0.2", + "initial_layer_print_height": "0.2", + "initial_layer_line_width": "0.42", + "wall_loops": "2", + "reduce_infill_retraction": "1", + "detect_overhang_wall": "1", + "skirt_loops": "0", + "skirt_distance": "0", + "sparse_infill_pattern": "grid", + "support_base_pattern": "rectilinear", + "support_interface_pattern": "rectilinear", + "compatible_printers": [ + "IdeaFormer IR3 V2 0.4 nozzle" + ] +} diff --git a/resources/profiles/IdeaFormer/process/fdm_process_common.json b/resources/profiles/IdeaFormer/process/fdm_process_common.json new file mode 100644 index 0000000000..b8e882770a --- /dev/null +++ b/resources/profiles/IdeaFormer/process/fdm_process_common.json @@ -0,0 +1,108 @@ +{ + "type": "process", + "name": "fdm_process_common", + "from": "system", + "instantiation": "false", + "adaptive_layer_height": "0", + "reduce_crossing_wall": "0", + "max_travel_detour_distance": "0", + "bottom_surface_pattern": "monotonic", + "bottom_shell_thickness": "0", + "bridge_speed": "50", + "brim_width": "5", + "brim_object_gap": "0.1", + "compatible_printers": [], + "compatible_printers_condition": "", + "print_sequence": "by layer", + "default_acceleration": "1000", + "initial_layer_acceleration": "500", + "top_surface_acceleration": "1000", + "travel_acceleration": "1000", + "inner_wall_acceleration": "1000", + "outer_wall_acceleration": "700", + "bridge_no_support": "0", + "draft_shield": "disabled", + "elefant_foot_compensation": "0", + "enable_arc_fitting": "0", + "wall_infill_order": "inner wall/outer wall/infill", + "infill_direction": "45", + "sparse_infill_density": "15%", + "sparse_infill_pattern": "crosshatch", + "initial_layer_print_height": "0.2", + "infill_combination": "0", + "infill_wall_overlap": "25%", + "interface_shells": "0", + "ironing_flow": "10%", + "ironing_spacing": "0.15", + "ironing_speed": "30", + "ironing_type": "no ironing", + "reduce_infill_retraction": "1", + "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[initial_tool]}_{printer_model}_{print_time}.gcode", + "detect_overhang_wall": "1", + "slowdown_for_curled_perimeters": "1", + "overhang_1_4_speed": "0", + "overhang_2_4_speed": "50", + "overhang_3_4_speed": "30", + "overhang_4_4_speed": "10", + "line_width": "110%", + "inner_wall_line_width": "110%", + "outer_wall_line_width": "100%", + "top_surface_line_width": "93.75%", + "sparse_infill_line_width": "110%", + "initial_layer_line_width": "120%", + "internal_solid_infill_line_width": "120%", + "support_line_width": "96%", + "wall_loops": "3", + "print_settings_id": "", + "raft_layers": "0", + "seam_position": "aligned", + "skirt_distance": "2", + "skirt_height": "3", + "min_skirt_length": "4", + "skirt_loops": "0", + "minimum_sparse_infill_area": "15", + "spiral_mode": "0", + "standby_temperature_delta": "-5", + "enable_support": "0", + "resolution": "0.012", + "support_type": "normal(auto)", + "support_on_build_plate_only": "0", + "support_top_z_distance": "0.2", + "support_bottom_z_distance": "0.2", + "support_filament": "0", + "support_interface_loop_pattern": "0", + "support_interface_filament": "0", + "support_interface_top_layers": "2", + "support_interface_bottom_layers": "2", + "support_interface_spacing": "0.5", + "support_interface_speed": "80", + "support_base_pattern": "default", + "support_base_pattern_spacing": "2.5", + "support_speed": "150", + "support_threshold_angle": "30", + "support_object_xy_distance": "0.35", + "tree_support_branch_angle": "30", + "tree_support_wall_count": "0", + "tree_support_with_infill": "0", + "detect_thin_wall": "0", + "top_surface_pattern": "monotonicline", + "top_shell_thickness": "0.8", + "enable_prime_tower": "1", + "wipe_tower_no_sparse_layers": "0", + "prime_tower_width": "60", + "xy_hole_compensation": "0", + "xy_contour_compensation": "0", + "layer_height": "0.2", + "bottom_shell_layers": "3", + "top_shell_layers": "4", + "bridge_flow": "1", + "initial_layer_speed": "45", + "initial_layer_infill_speed": "45", + "outer_wall_speed": "45", + "inner_wall_speed": "80", + "sparse_infill_speed": "150", + "internal_solid_infill_speed": "150", + "top_surface_speed": "50", + "gap_infill_speed": "30", + "travel_speed": "200" +} From 9405ac59766e8e12c697d6e921ac457ecc6daf39 Mon Sep 17 00:00:00 2001 From: Joseph Robertson <32232925+HarrierPigeon@users.noreply.github.com> Date: Mon, 8 Jun 2026 05:36:46 -0500 Subject: [PATCH 039/224] remove mesh origin snapping --- src/libslic3r/BeltGCode.cpp | 101 ++++-------------------------- src/libslic3r/BeltGCode.hpp | 7 +-- src/libslic3r/BeltGCodeWriter.cpp | 28 +-------- src/libslic3r/BeltGCodeWriter.hpp | 12 +--- src/libslic3r/Preset.cpp | 3 - src/libslic3r/Print.cpp | 3 - src/libslic3r/PrintConfig.cpp | 25 -------- src/libslic3r/PrintConfig.hpp | 6 -- src/slic3r/GUI/Tab.cpp | 25 -------- 9 files changed, 17 insertions(+), 193 deletions(-) diff --git a/src/libslic3r/BeltGCode.cpp b/src/libslic3r/BeltGCode.cpp index 8c8c3d4392..8184692862 100644 --- a/src/libslic3r/BeltGCode.cpp +++ b/src/libslic3r/BeltGCode.cpp @@ -3,8 +3,6 @@ #include "BeltTransform.hpp" #include "Print.hpp" -#include - namespace Slic3r { void BeltGCode::init_belt_writer(Print &print, bool is_bbl_printers) @@ -18,14 +16,6 @@ void BeltGCode::init_belt_writer(Print &print, bool is_bbl_printers) belt_writer->set_belt_back_transform(print.config()); belt_writer->set_machine_frame_transform(print.config()); m_writer = std::move(belt_writer); - - // Per-axis origin snap config. - m_origin_snap[0] = print.config().belt_origin_snap_x.value; - m_origin_snap[1] = print.config().belt_origin_snap_y.value; - m_origin_snap[2] = print.config().belt_origin_snap_z.value; - m_origin_snap_offset[0] = print.config().belt_origin_offset_x.value; - m_origin_snap_offset[1] = print.config().belt_origin_offset_y.value; - m_origin_snap_offset[2] = print.config().belt_origin_offset_z.value; } void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print) @@ -64,103 +54,36 @@ void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print) file.write_format("; belt_gcode_transform_order = %s\n", full_cfg.opt_serialize("belt_gcode_transform_order").c_str()); } -void BeltGCode::on_set_origin(const PrintObject *obj, const Point &inst_shift) +void BeltGCode::on_set_origin(const PrintObject * /*obj*/, const Point & /*inst_shift*/) { // 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). - // This replaces the bbox-based axis snap with an exact formula. // // 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 either way, - // because the back_transform applied during G-code emission is always the - // inverse of the full pipeline. + // 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 && m_config.belt_printer.value) { - auto *belt_writer = dynamic_cast(m_writer.get()); - if (belt_writer) { - // The per-object lift (z_shift_val = max(0, -m_belt_min_z)) added by - // BeltSliceStrategy::apply_to_trafo is already compensated inside - // global_z_offset (via the belt_z_shift term in PrintObjectSlice.cpp). - // Snap was previously used here for the same purpose, but with both - // active the lift gets subtracted twice. Clear any leftover snap - // state from a prior instance. - for (int a = 0; a < 3; ++a) - belt_writer->set_origin_snap(a, false, 0., 0.); - } - - // Adjust origin: transform through belt forward pipeline so that - // the back-transform correctly recovers model-space positions. - Transform3d T = BeltTransformPipeline::build_forward_transform(m_config); - Vec2d cur_origin = this->origin(); - Vec3d origin3d(cur_origin.x(), cur_origin.y(), 0.); - Vec3d adjusted = T.linear() * origin3d; - this->set_origin(Vec2d(adjusted.x(), adjusted.y())); - return; - } - - if (!m_origin_snap[0] && !m_origin_snap[1] && !m_origin_snap[2]) + if (!use_global || !m_config.belt_printer.value) return; - auto *belt_writer = dynamic_cast(m_writer.get()); - if (!belt_writer) - return; - - // Clear existing snap so to_machine_coords gives raw machine coords for bbox computation. - for (int a = 0; a < 3; ++a) - belt_writer->set_origin_snap(a, false, 0., 0.); - - // Reconstruct the belt pipeline transform for this object. - Transform3d belt = BeltTransformPipeline::build_forward_transform(m_config); - - // Z-shift - double zs = (obj->belt_min_z() < 0.) ? -obj->belt_min_z() : 0.; - if (zs > 0.) { - Transform3d zsh = Transform3d::Identity(); - zsh.matrix()(2, 3) = zs; - belt = zsh * belt; - } - - // Full transform: belt * trafo_centered - Transform3d full = belt * obj->trafo_centered(); - - // Instance shift in slicer space + global Z offset - Vec3d shift(unscale(inst_shift.x()), - unscale(inst_shift.y()), - obj->belt_global_z_offset()); - - // Compute this instance's bbox min in the Cartesian frame (post back_transform - // + axis_remap, before machine_frame_transform). Using to_cartesian instead of - // to_machine_coords ensures the 8 axis-aligned bbox corners coincide with the - // geometry's extreme points — a property that breaks under shear, which would - // mis-normalize non-cubic shapes (inverted cone, benchy) by their bbox-volume - // corners rather than their actual lowest geometry point. - BoundingBoxf3 bb = obj->model_object()->raw_bounding_box(); - Vec3d mn = bb.min.cast(), mx = bb.max.cast(); - Vec3d inst_min(std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max()); - 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 mc = belt_writer->to_cartesian(full * c + shift); - for (int a = 0; a < 3; ++a) - inst_min[a] = std::min(inst_min[a], mc[a]); - } - - // Update writer snap for each enabled axis - for (int a = 0; a < 3; ++a) - belt_writer->set_origin_snap(a, m_origin_snap[a], m_origin_snap_offset[a], inst_min[a]); + // Adjust origin: transform through belt forward pipeline so that + // the back-transform correctly recovers model-space positions. + Transform3d T = BeltTransformPipeline::build_forward_transform(m_config); + Vec2d cur_origin = this->origin(); + Vec3d origin3d(cur_origin.x(), cur_origin.y(), 0.); + Vec3d adjusted = T.linear() * origin3d; + this->set_origin(Vec2d(adjusted.x(), adjusted.y())); } } // namespace Slic3r diff --git a/src/libslic3r/BeltGCode.hpp b/src/libslic3r/BeltGCode.hpp index 682deb7d65..2a3718b49f 100644 --- a/src/libslic3r/BeltGCode.hpp +++ b/src/libslic3r/BeltGCode.hpp @@ -9,7 +9,7 @@ namespace Slic3r { // Inherits from GCode and overrides virtual hooks to: // - Create a BeltGCodeWriter instead of a plain GCodeWriter // - Write belt configuration to the G-code header -// - Update per-axis origin snap when switching instances +// - Adjust the origin for global pre-slice transforms when switching instances // - Disable arc fitting (G2/G3 not supported on belt printers) class BeltGCode : public GCode { @@ -18,11 +18,6 @@ protected: void write_belt_header(GCodeOutputStream &file, const Print &print) override; void on_set_origin(const PrintObject *obj, const Point &inst_shift) override; bool should_disable_arc_fitting() const override { return true; } - -private: - // Per-axis origin snap config (set during init, used in on_set_origin). - bool m_origin_snap[3] = {false, false, false}; - double m_origin_snap_offset[3] = {0., 0., 0.}; }; } // namespace Slic3r diff --git a/src/libslic3r/BeltGCodeWriter.cpp b/src/libslic3r/BeltGCodeWriter.cpp index 9f1f32a375..a2322c9243 100644 --- a/src/libslic3r/BeltGCodeWriter.cpp +++ b/src/libslic3r/BeltGCodeWriter.cpp @@ -35,37 +35,14 @@ void BeltGCodeWriter::set_machine_frame_transform(const PrintConfig &config) m_machine_frame_transform.init_from_config(config); } -void BeltGCodeWriter::set_origin_snap(int axis, bool enable, double offset, double bbox_min) -{ - if (axis >= 0 && axis < 3) { - m_origin_snap[axis] = enable; - m_origin_offset[axis] = offset; - m_origin_bbox_min[axis] = bbox_min; - } -} - -Vec3d BeltGCodeWriter::to_cartesian(const Vec3d &pos) const -{ - // back_transform → axis_remap, no origin_snap, no machine_frame_transform. - return apply_axis_remap(m_belt_back_transform.apply(pos)); -} - Vec3d BeltGCodeWriter::to_machine_coords(const Vec3d &pos) const { // Step 1+2: To Cartesian (back_transform + axis_remap). Vec3d after_back = m_belt_back_transform.apply(pos); Vec3d result = apply_axis_remap(after_back); Vec3d after_remap = result; - // Step 3: Per-axis origin snap (computed in the Cartesian frame). - for (int i = 0; i < 3; ++i) - if (m_origin_snap[i]) - result[i] -= (m_origin_bbox_min[i] - m_origin_offset[i]); - Vec3d after_snap = result; - // Step 4: Machine-frame transform (gcode_shear / gcode_scale) - // applied LAST so it acts as a global linear transform on the placed coords. - // Order matters: putting it before origin_snap would feed sheared bbox corners - // into the snap's per-object min calculation, mis-normalizing non-cubic geometries - // (the corners of the original bbox aren't extreme points of the sheared shape). + // Step 3: Machine-frame transform (belt frame tilt) applied LAST so it acts + // as a global linear transform on the placed coords. Vec3d final = m_machine_frame_transform.apply(result); // [BELT-DEBUG] One-shot log per layer transition (i.e. when the input Z @@ -80,7 +57,6 @@ Vec3d BeltGCodeWriter::to_machine_coords(const Vec3d &pos) const << " slicer_in=(" << pos.x() << "," << pos.y() << "," << pos.z() << ")" << " after_back=(" << after_back.x() << "," << after_back.y() << "," << after_back.z() << ")" << " after_remap=(" << after_remap.x() << "," << after_remap.y() << "," << after_remap.z() << ")" - << " after_snap=(" << after_snap.x() << "," << after_snap.y() << "," << after_snap.z() << ")" << " final=(" << final.x() << "," << final.y() << "," << final.z() << ")" << " mft_active=" << m_machine_frame_transform.is_active() << " back_active=" << m_belt_back_transform.is_active(); diff --git a/src/libslic3r/BeltGCodeWriter.hpp b/src/libslic3r/BeltGCodeWriter.hpp index e9d4af0f3a..30159f4f33 100644 --- a/src/libslic3r/BeltGCodeWriter.hpp +++ b/src/libslic3r/BeltGCodeWriter.hpp @@ -11,8 +11,8 @@ class FirstLayerPlane; // Belt-printer-specific GCode writer. // // Inherits from GCodeWriter and overrides movement methods to apply -// coordinate transformation (back-transform, axis remap, origin snap) -// and emit coupled XYZ moves (Y and Z are coupled due to belt tilt). +// coordinate transformation (back-transform, axis remap, machine-frame +// transform) and emit coupled XYZ moves (Y and Z are coupled due to belt tilt). class BeltGCodeWriter : public GCodeWriter { public: @@ -21,12 +21,7 @@ public: // Belt configuration (axis remap is inherited from GCodeWriter) void set_belt_back_transform(const PrintConfig &config); void set_machine_frame_transform(const PrintConfig &config); - void set_origin_snap(int axis, bool enable, double offset, double bbox_min); Vec3d to_machine_coords(const Vec3d &pos) const; - // back_transform + axis_remap only (no origin_snap, no machine_frame_transform). - // Used by on_set_origin for bbox computation in the Cartesian frame, where - // axis-aligned bbox corners coincide with the geometry's extreme points. - Vec3d to_cartesian(const Vec3d &pos) const; // First-layer plane: when set to a non-null active evaluator, travel // speed selection consults the plane per-move and uses @@ -52,9 +47,6 @@ protected: private: BeltBackTransform m_belt_back_transform; MachineFrameTransform m_machine_frame_transform; - bool m_origin_snap[3] = {false, false, false}; - double m_origin_offset[3] = {0., 0., 0.}; - double m_origin_bbox_min[3] = {0., 0., 0.}; // Borrowed pointer; lifetime owned by GCode. null = inactive. const FirstLayerPlane *m_first_layer_plane = nullptr; double m_first_layer_thickness_mm = 0.; diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 694cd284e3..9b514bb291 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1339,9 +1339,6 @@ static std::vector s_Preset_printer_options { "belt_gcode_transform_order", "belt_preslice_global", "first_layer_plane", "first_layer_plane_offset", "first_layer_plane_thickness", - "belt_origin_snap_x", "belt_origin_offset_x", - "belt_origin_snap_y", "belt_origin_offset_y", - "belt_origin_snap_z", "belt_origin_offset_z", "belt_support_floor_offset", "belt_support_floor_mode", "belt_support_z_offset_mode", "gcode_flavor", "fan_kickstart", "part_cooling_fan_min_pwm", "fan_speedup_time", "fan_speedup_overhangs", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index f55dd31fc1..099a592509 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -112,9 +112,6 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n "gcode_scale_y", "gcode_scale_y_angle", "gcode_scale_z", "gcode_scale_z_angle", "belt_gcode_transform_order", - "belt_origin_snap_x", "belt_origin_offset_x", - "belt_origin_snap_y", "belt_origin_offset_y", - "belt_origin_snap_z", "belt_origin_offset_z", //BBS "additional_cooling_fan_speed", "reduce_crossing_wall", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index c1c411c7b6..cdb2ec1d5d 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -6782,31 +6782,6 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(-1.0)); - auto add_belt_origin_snap = [this](const char *key_snap, const char *key_offset, - const char *axis_label) { - auto def = this->add(key_snap, coBool); - def->label = L(axis_label); - def->category = L("Printable space"); - std::string tip = std::string("Shift G-code output so the object's bounding box minimum on machine ") - + axis_label + " equals the offset value."; - def->tooltip = L(tip); - def->mode = comExpert; - def->set_default_value(new ConfigOptionBool(false)); - - def = this->add(key_offset, coFloat); - def->label = L("Offset"); - def->category = L("Printable space"); - def->tooltip = L("Target coordinate for the bounding box minimum on this machine axis."); - def->sidetext = L("mm"); - def->min = -10000; - def->max = 10000; - def->mode = comExpert; - def->set_default_value(new ConfigOptionFloat(0)); - }; - add_belt_origin_snap("belt_origin_snap_x", "belt_origin_offset_x", "X"); - add_belt_origin_snap("belt_origin_snap_y", "belt_origin_offset_y", "Y"); - add_belt_origin_snap("belt_origin_snap_z", "belt_origin_offset_z", "Z"); - // Belt support floor debug controls def = this->add("belt_support_floor_offset", coFloat); def->label = L("Support Floor Z offset"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 4a53aa7ac2..a31179cd9b 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1619,12 +1619,6 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionEnum, first_layer_plane)) ((ConfigOptionFloat, first_layer_plane_offset)) ((ConfigOptionFloat, first_layer_plane_thickness)) - ((ConfigOptionBool, belt_origin_snap_x)) - ((ConfigOptionFloat, belt_origin_offset_x)) - ((ConfigOptionBool, belt_origin_snap_y)) - ((ConfigOptionFloat, belt_origin_offset_y)) - ((ConfigOptionBool, belt_origin_snap_z)) - ((ConfigOptionFloat, belt_origin_offset_z)) ((ConfigOptionFloat, belt_support_floor_offset)) ((ConfigOptionEnum, belt_support_floor_mode)) ((ConfigOptionEnum, belt_support_z_offset_mode)) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index bcbcce1d66..4027a671e7 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4535,24 +4535,6 @@ void TabPrinter::build_fff() line.append_option(belt_og->get_option("first_layer_plane_thickness")); belt_og->append_line(line); } - { - Line line = { L("Origin snap X"), L("Snap object bbox min X to offset in G-code output") }; - line.append_option(belt_og->get_option("belt_origin_snap_x")); - line.append_option(belt_og->get_option("belt_origin_offset_x")); - belt_og->append_line(line); - } - { - Line line = { L("Origin snap Y"), L("Snap object bbox min Y to offset in G-code output") }; - line.append_option(belt_og->get_option("belt_origin_snap_y")); - line.append_option(belt_og->get_option("belt_origin_offset_y")); - belt_og->append_line(line); - } - { - Line line = { L("Origin snap Z"), L("Snap object bbox min Z to offset in G-code output") }; - line.append_option(belt_og->get_option("belt_origin_snap_z")); - line.append_option(belt_og->get_option("belt_origin_offset_z")); - 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). belt_og->append_single_option_line("belt_support_floor_offset"); @@ -5591,8 +5573,6 @@ void TabPrinter::toggle_options() 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", is_belt); - for (auto el : {"belt_origin_snap_x", "belt_origin_snap_y", "belt_origin_snap_z"}) - toggle_line(el, is_belt); // Remap, back-transform, and global mesh-transforms toggles are gated by belt // mode here; finer mode-based visibility (Advanced vs Expert) is handled by @@ -5643,11 +5623,6 @@ void TabPrinter::toggle_options() auto gscz = m_config->option>("gcode_scale_z")->value; toggle_option("gcode_scale_z_angle", is_belt && gscz != BeltScaleMode::None); - // Origin snap is superseded by belt_preslice_global - toggle_option("belt_origin_offset_x", is_belt && m_config->opt_bool("belt_origin_snap_x") && !belt_global); - toggle_option("belt_origin_offset_y", is_belt && m_config->opt_bool("belt_origin_snap_y") && !belt_global); - toggle_option("belt_origin_offset_z", is_belt && m_config->opt_bool("belt_origin_snap_z") && !belt_global); - // 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); From fee6be98b2bc9953b3f5ba4a948b269a5f153a40 Mon Sep 17 00:00:00 2001 From: Joseph Robertson <32232925+HarrierPigeon@users.noreply.github.com> Date: Mon, 8 Jun 2026 06:16:48 -0500 Subject: [PATCH 040/224] unify frame tilt work --- .../Custom/machine/fdm_belt_common.json | 2 - src/libslic3r/BeltGCode.cpp | 21 +-- src/libslic3r/BeltTransform.hpp | 37 +---- src/libslic3r/GCode/MachineFrameTransform.cpp | 102 +++++------- src/libslic3r/GCode/MachineFrameTransform.hpp | 15 +- src/libslic3r/Preset.cpp | 8 +- src/libslic3r/Print.cpp | 10 +- src/libslic3r/PrintConfig.cpp | 157 +++--------------- src/libslic3r/PrintConfig.hpp | 75 ++------- src/slic3r/GUI/Tab.cpp | 86 ++-------- 10 files changed, 112 insertions(+), 401 deletions(-) diff --git a/resources/profiles/Custom/machine/fdm_belt_common.json b/resources/profiles/Custom/machine/fdm_belt_common.json index 0530999f15..b010742d78 100644 --- a/resources/profiles/Custom/machine/fdm_belt_common.json +++ b/resources/profiles/Custom/machine/fdm_belt_common.json @@ -93,8 +93,6 @@ "belt_slice_rotation_angle": "45", "belt_slice_rotation_global": "1", "build_plate_tilt_x": "45", - "gcode_shear_z": "pos_tan", - "gcode_scale_y": "inv_cos", "purge_in_prime_tower": "0", "scan_first_layer": "0", "auxiliary_fan": "0" diff --git a/src/libslic3r/BeltGCode.cpp b/src/libslic3r/BeltGCode.cpp index 8184692862..3ba2f79e55 100644 --- a/src/libslic3r/BeltGCode.cpp +++ b/src/libslic3r/BeltGCode.cpp @@ -35,23 +35,10 @@ void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print) 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 configs - file.write_format("; gcode_shear_x = %s\n", full_cfg.opt_serialize("gcode_shear_x").c_str()); - file.write_format("; gcode_shear_x_angle = %.1f\n", print.config().gcode_shear_x_angle.value); - file.write_format("; gcode_shear_x_from = %s\n", full_cfg.opt_serialize("gcode_shear_x_from").c_str()); - file.write_format("; gcode_shear_y = %s\n", full_cfg.opt_serialize("gcode_shear_y").c_str()); - file.write_format("; gcode_shear_y_angle = %.1f\n", print.config().gcode_shear_y_angle.value); - file.write_format("; gcode_shear_y_from = %s\n", full_cfg.opt_serialize("gcode_shear_y_from").c_str()); - file.write_format("; gcode_shear_z = %s\n", full_cfg.opt_serialize("gcode_shear_z").c_str()); - file.write_format("; gcode_shear_z_angle = %.1f\n", print.config().gcode_shear_z_angle.value); - file.write_format("; gcode_shear_z_from = %s\n", full_cfg.opt_serialize("gcode_shear_z_from").c_str()); - file.write_format("; gcode_scale_x = %s\n", full_cfg.opt_serialize("gcode_scale_x").c_str()); - file.write_format("; gcode_scale_x_angle = %.1f\n", print.config().gcode_scale_x_angle.value); - file.write_format("; gcode_scale_y = %s\n", full_cfg.opt_serialize("gcode_scale_y").c_str()); - file.write_format("; gcode_scale_y_angle = %.1f\n", print.config().gcode_scale_y_angle.value); - file.write_format("; gcode_scale_z = %s\n", full_cfg.opt_serialize("gcode_scale_z").c_str()); - file.write_format("; gcode_scale_z_angle = %.1f\n", print.config().gcode_scale_z_angle.value); - file.write_format("; belt_gcode_transform_order = %s\n", full_cfg.opt_serialize("belt_gcode_transform_order").c_str()); + // 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); + file.write_format("; belt_frame_tilt_angle = %.1f\n", print.config().belt_frame_tilt_angle.value); } void BeltGCode::on_set_origin(const PrintObject * /*obj*/, const Point & /*inst_shift*/) diff --git a/src/libslic3r/BeltTransform.hpp b/src/libslic3r/BeltTransform.hpp index 50f56bd75f..b67ac2007d 100644 --- a/src/libslic3r/BeltTransform.hpp +++ b/src/libslic3r/BeltTransform.hpp @@ -21,44 +21,11 @@ class ModelObject; // to the g-code instead (see MachineFrameTransform). This class provides the // building blocks so every call site uses the same implementation. z_shift is // object-dependent (computed from mesh vertex bounds) and is NOT included in -// build_forward_transform(). -// -// The compute_shear_factor / compute_scale_factor math helpers below are still -// used by the g-code-side machine-frame shear/scale. +// build_forward_transform(). The machine-frame shear/scale is derived directly +// from the tilt angle in MachineFrameTransform and no longer lives here. class BeltTransformPipeline { public: - // ---- Pure math helpers ------------------------------------------------ - - static double compute_shear_factor(BeltShearMode mode, double angle_deg) - { - double angle_rad = Geometry::deg2rad(angle_deg); - double sin_a = std::sin(angle_rad); - double cos_a = std::cos(angle_rad); - switch (mode) { - case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; - case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; - case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; - case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; - default: return 0.; - } - } - - static double compute_scale_factor(BeltScaleMode mode, double angle_deg) - { - if (mode == BeltScaleMode::None) return 1.; - double angle_rad = Geometry::deg2rad(angle_deg); - double sin_a = std::sin(angle_rad); - double cos_a = std::cos(angle_rad); - switch (mode) { - case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; - case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; - case BeltScaleMode::Sin: return sin_a; - case BeltScaleMode::Cos: return cos_a; - default: return 1.; - } - } - // ---- Identity checks -------------------------------------------------- static bool has_preslice_remap(const PrintConfig &config) diff --git a/src/libslic3r/GCode/MachineFrameTransform.cpp b/src/libslic3r/GCode/MachineFrameTransform.cpp index 03f74605fe..bdc172d7a4 100644 --- a/src/libslic3r/GCode/MachineFrameTransform.cpp +++ b/src/libslic3r/GCode/MachineFrameTransform.cpp @@ -1,57 +1,10 @@ #include "MachineFrameTransform.hpp" -#include "../BeltTransform.hpp" -#include "../BoundingBox.hpp" +#include "../Geometry.hpp" + +#include namespace Slic3r { -namespace { - -// Build the 3x3 shear matrix from gcode_shear_* keys. -Matrix3d build_gcode_shear_matrix(const PrintConfig &config, bool &active) -{ - struct AxisShear { BeltShearMode mode; double angle; int from; }; - AxisShear axes[3] = { - { config.gcode_shear_x.value, config.gcode_shear_x_angle.value, int(config.gcode_shear_x_from.value) }, - { config.gcode_shear_y.value, config.gcode_shear_y_angle.value, int(config.gcode_shear_y_from.value) }, - { config.gcode_shear_z.value, config.gcode_shear_z_angle.value, int(config.gcode_shear_z_from.value) }, - }; - - Matrix3d shear = Matrix3d::Identity(); - active = false; - for (int row = 0; row < 3; ++row) { - if (axes[row].mode != BeltShearMode::None) { - double factor = BeltTransformPipeline::compute_shear_factor(axes[row].mode, axes[row].angle); - if (std::abs(factor) > EPSILON) { - shear(row, axes[row].from) += factor; - active = true; - } - } - } - return shear; -} - -// Build the 3x3 diagonal scale matrix from gcode_scale_* keys. -Matrix3d build_gcode_scale_matrix(const PrintConfig &config, bool &active) -{ - double sx = BeltTransformPipeline::compute_scale_factor(config.gcode_scale_x.value, config.gcode_scale_x_angle.value); - double sy = BeltTransformPipeline::compute_scale_factor(config.gcode_scale_y.value, config.gcode_scale_y_angle.value); - double sz = BeltTransformPipeline::compute_scale_factor(config.gcode_scale_z.value, config.gcode_scale_z_angle.value); - - active = (std::abs(sx - 1.) > EPSILON || - std::abs(sy - 1.) > EPSILON || - std::abs(sz - 1.) > EPSILON); - - Matrix3d scale = Matrix3d::Identity(); - if (active) { - scale(0, 0) = sx; - scale(1, 1) = sy; - scale(2, 2) = sz; - } - return scale; -} - -} // namespace - bool MachineFrameTransform::init_from_config(const PrintConfig &config) { m_active = false; @@ -60,21 +13,48 @@ bool MachineFrameTransform::init_from_config(const PrintConfig &config) if (!config.belt_printer.value) return false; - bool shear_active = false; - Matrix3d shear = build_gcode_shear_matrix(config, shear_active); - bool scale_active = false; - Matrix3d scale = build_gcode_scale_matrix(config, scale_active); + // The machine-frame transform is derived from the single belt tilt (axis + + // angle) that also drives the pre-slice mesh rotation. Expert decouple lets + // the machine-frame angle differ from the slicing rotation; otherwise both + // use belt_slice_rotation_angle. + const BeltRotationAxis axis = config.belt_slice_rotation.value; + if (axis == BeltRotationAxis::None || axis == BeltRotationAxis::Z) + return false; // Z is an in-plane spin: no machine-frame tilt. - if (!shear_active && !scale_active) + const double angle_deg = config.belt_frame_tilt_decouple.value + ? config.belt_frame_tilt_angle.value + : config.belt_slice_rotation_angle.value; + if (std::abs(angle_deg) <= EPSILON) return false; - // Compose per belt_gcode_transform_order: - // ScaleThenShear: applied to p, scale runs first then shear (shear * scale). - // ShearThenScale: applied to p, shear runs first then scale (scale * shear). + const double angle_rad = Geometry::deg2rad(angle_deg); + const double cos_a = std::cos(angle_rad); + if (std::abs(cos_a) <= EPSILON) + return false; + const double tan_a = std::sin(angle_rad) / cos_a; + const double inv_cos = 1.0 / cos_a; + + // Couple the height axis (Z) to the belt-feed axis and stretch the belt-feed + // axis by 1/cos so a unit slicing move maps to the correct belt travel. The + // shear sign matches the belt-floor slope derived from the same rotation in + // BeltTransformPipeline::compute_belt_height_and_floor: + // tilt about X: feed axis Y, Z += +tan·Y, scale Y *= 1/cos + // tilt about Y: feed axis X, Z += -tan·X, scale X *= 1/cos + Matrix3d shear = Matrix3d::Identity(); + Matrix3d scale = Matrix3d::Identity(); + if (axis == BeltRotationAxis::X) { + shear(2, 1) = tan_a; // Z from Y + scale(1, 1) = inv_cos; // Y + } else { // BeltRotationAxis::Y + shear(2, 0) = -tan_a; // Z from X + scale(0, 0) = inv_cos; // X + } + + // Apply shear first, then scale (the historical default ShearThenScale order: + // result = scale * shear * p). For the canonical 45°/X belt this maps + // (x,y,z) -> (x, y/cos, y + z), matching the previous per-axis config. Transform3d combined = Transform3d::Identity(); - combined.linear() = (config.belt_gcode_transform_order.value == BeltTransformOrder::ScaleThenShear) - ? Matrix3d(shear * scale) - : Matrix3d(scale * shear); + combined.linear() = scale * shear; if (combined.isApprox(Transform3d::Identity())) return false; diff --git a/src/libslic3r/GCode/MachineFrameTransform.hpp b/src/libslic3r/GCode/MachineFrameTransform.hpp index 9fa25b2323..425fe3004b 100644 --- a/src/libslic3r/GCode/MachineFrameTransform.hpp +++ b/src/libslic3r/GCode/MachineFrameTransform.hpp @@ -9,14 +9,15 @@ namespace Slic3r { // Post-stage machine-frame transform for belt printers. // -// Applied in BeltGCodeWriter::to_machine_coords AFTER the back-transform, -// the gcode_remap_* axis remap and per-axis origin snap. Maps Cartesian -// (axis-permuted) G-code coordinates into the printer's physical machine -// frame using: -// gcode_shear_x/y/z + _angle + _from -// gcode_scale_x/y/z + _angle +// Applied in BeltGCodeWriter::to_machine_coords AFTER the back-transform and +// the gcode_remap_* axis remap. Maps Cartesian (axis-permuted) G-code +// coordinates into the printer's physical machine frame. // -// Composition follows belt_gcode_transform_order (shear * scale or scale * shear). +// Derived entirely from the single belt tilt (belt_slice_rotation axis + +// belt_slice_rotation_angle): a shear coupling the height axis to the belt-feed +// axis (factor tan a) plus a 1/cos a scale on the belt-feed axis. The expert +// belt_frame_tilt_decouple flag lets the machine-frame angle differ from the +// pre-slice rotation angle via belt_frame_tilt_angle. class MachineFrameTransform { public: diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 9b514bb291..a9ccfdbd6b 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1330,13 +1330,7 @@ static std::vector s_Preset_printer_options { "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", - "gcode_shear_x", "gcode_shear_x_angle", "gcode_shear_x_from", - "gcode_shear_y", "gcode_shear_y_angle", "gcode_shear_y_from", - "gcode_shear_z", "gcode_shear_z_angle", "gcode_shear_z_from", - "gcode_scale_x", "gcode_scale_x_angle", - "gcode_scale_y", "gcode_scale_y_angle", - "gcode_scale_z", "gcode_scale_z_angle", - "belt_gcode_transform_order", + "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", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 099a592509..b511898ae8 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -104,14 +104,8 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n "gcode_remap_x", "gcode_remap_y", "gcode_remap_z", - // Machine-frame transforms (only affect G-code output, not slicing). - "gcode_shear_x", "gcode_shear_x_angle", "gcode_shear_x_from", - "gcode_shear_y", "gcode_shear_y_angle", "gcode_shear_y_from", - "gcode_shear_z", "gcode_shear_z_angle", "gcode_shear_z_from", - "gcode_scale_x", "gcode_scale_x_angle", - "gcode_scale_y", "gcode_scale_y_angle", - "gcode_scale_z", "gcode_scale_z_angle", - "belt_gcode_transform_order", + // Machine-frame transform (derived from belt tilt; only affects G-code output). + "belt_frame_tilt_decouple", "belt_frame_tilt_angle", //BBS "additional_cooling_fan_speed", "reduce_crossing_wall", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index cdb2ec1d5d..723725a7f2 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -312,22 +312,6 @@ static t_config_enum_values s_keys_map_SlicingMode { }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(SlicingMode) -static t_config_enum_values s_keys_map_BeltShearMode { - { "none", int(BeltShearMode::None) }, - { "pos_cot", int(BeltShearMode::PosCot) }, - { "neg_cot", int(BeltShearMode::NegCot) }, - { "pos_tan", int(BeltShearMode::PosTan) }, - { "neg_tan", int(BeltShearMode::NegTan) }, -}; -CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltShearMode) - -static t_config_enum_values s_keys_map_BeltAxis { - { "x", int(BeltAxis::X) }, - { "y", int(BeltAxis::Y) }, - { "z", int(BeltAxis::Z) }, -}; -CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltAxis) - static t_config_enum_values s_keys_map_BeltRotationAxis { { "none", int(BeltRotationAxis::None) }, { "x", int(BeltRotationAxis::X) }, @@ -336,21 +320,6 @@ static t_config_enum_values s_keys_map_BeltRotationAxis { }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltRotationAxis) -static t_config_enum_values s_keys_map_BeltTransformOrder { - { "scale_then_shear", int(BeltTransformOrder::ScaleThenShear) }, - { "shear_then_scale", int(BeltTransformOrder::ShearThenScale) }, -}; -CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltTransformOrder) - -static t_config_enum_values s_keys_map_BeltScaleMode { - { "none", int(BeltScaleMode::None) }, - { "inv_sin", int(BeltScaleMode::InvSin) }, - { "inv_cos", int(BeltScaleMode::InvCos) }, - { "sin", int(BeltScaleMode::Sin) }, - { "cos", int(BeltScaleMode::Cos) }, -}; -CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltScaleMode) - static t_config_enum_values s_keys_map_RemapAxis { { "pos_x", int(RemapAxis::PosX) }, { "pos_y", int(RemapAxis::PosY) }, @@ -6566,17 +6535,28 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionBool(true)); - auto add_belt_transform_order = [this](const char *key, const char *label, const char *tooltip) { - auto def = this->add(key, coEnum); - def->label = L(label); - def->category = L("Printable space"); - def->tooltip = L(tooltip); - def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); - def->enum_values = {"scale_then_shear", "shear_then_scale"}; - def->enum_labels = {L("Scale, then shear"), L("Shear, then scale")}; - def->mode = comExpert; - def->set_default_value(new ConfigOptionEnum(BeltTransformOrder::ShearThenScale)); - }; + def = this->add("belt_frame_tilt_decouple", coBool); + def->label = L("Decouple machine-frame tilt"); + def->category = L("Printable space"); + def->tooltip = L("Expert override: set the machine-frame (g-code shear/scale) tilt angle " + "independently of the pre-slice rotation angle. When disabled, the " + "machine-frame transform is derived from the belt tilt angle, so a single " + "angle drives both stages. Enable only to compensate for a machine whose " + "physical gantry tilt differs from the slicing rotation."); + def->mode = comExpert; + def->set_default_value(new ConfigOptionBool(false)); + + def = this->add("belt_frame_tilt_angle", coFloat); + def->label = L("Machine-frame tilt angle"); + def->category = L("Printable space"); + def->tooltip = L("Tilt angle (degrees) used to derive the machine-frame shear (tan) and " + "scale (1/cos) applied to G-code. Only used when 'Decouple machine-frame " + "tilt' is enabled; otherwise the belt tilt angle is used."); + def->sidetext = L("°"); + def->min = -89.9; + def->max = 89.9; + def->mode = comExpert; + def->set_default_value(new ConfigOptionFloat(45.)); // G-code axis remap with sign auto add_belt_remap = [this](const char *key, const char *label, const char *tooltip, @@ -6626,96 +6606,9 @@ void PrintConfigDef::init_fff_params() 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, comExpert); 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, comExpert); - // Machine-frame G-code transforms: applied AFTER back-transform and gcode_remap, - // before per-axis origin snap. Maps Cartesian G-code to the printer's physical machine frame. - // These shear/scale transforms act on the G-code coordinates (not the mesh) — they are - // the belt printer's only shear/scale stage. - auto add_belt_shear_mode = [this](const char *key, const char *label, BeltShearMode default_mode, - ConfigOptionMode mode = comAdvanced) { - auto def = this->add(key, coEnum); - def->label = L(label); - def->category = L("Printable space"); - def->tooltip = L("Shear function applied to this axis in belt printer mode."); - def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); - def->enum_values = {"none", "pos_cot", "neg_cot", "pos_tan", "neg_tan"}; - def->enum_labels = {L("None"), L("+cot(α)"), L("-cot(α)"), L("+tan(α)"), L("-tan(α)")}; - def->mode = mode; - def->set_default_value(new ConfigOptionEnum(default_mode)); - }; - auto add_belt_shear_angle = [this](const char *key, const char *label, - ConfigOptionMode mode = comAdvanced) { - auto def = this->add(key, coFloat); - def->label = L(label); - def->category = L("Printable space"); - def->tooltip = L("Angle (degrees) for the shear function on this axis."); - def->sidetext = L("°"); - def->min = 0.1; - def->max = 89.9; - def->mode = mode; - def->set_default_value(new ConfigOptionFloat(45)); - }; - auto add_belt_axis_enum = [this](const char *key, const char *label, const char *tooltip, - BeltAxis default_axis, ConfigOptionMode mode = comAdvanced) { - auto def = this->add(key, coEnum); - def->label = L(label); - def->category = L("Printable space"); - def->tooltip = L(tooltip); - def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); - def->enum_values = {"x", "y", "z"}; - def->enum_labels = {L("X"), L("Y"), L("Z")}; - def->mode = mode; - def->set_default_value(new ConfigOptionEnum(default_axis)); - }; - auto add_belt_scale_mode = [this](const char *key, const char *label, BeltScaleMode default_mode, - ConfigOptionMode mode = comAdvanced) { - auto def = this->add(key, coEnum); - def->label = L(label); - def->category = L("Printable space"); - def->tooltip = L("Scale factor applied to this axis in belt printer mode."); - def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); - def->enum_values = {"none", "inv_sin", "inv_cos", "sin", "cos"}; - def->enum_labels = {L("None"), L("1/sin(α)"), L("1/cos(α)"), L("sin(α)"), L("cos(α)")}; - def->mode = mode; - def->set_default_value(new ConfigOptionEnum(default_mode)); - }; - auto add_belt_scale_angle = [this](const char *key, const char *label, - ConfigOptionMode mode = comAdvanced) { - auto def = this->add(key, coFloat); - def->label = L(label); - def->category = L("Printable space"); - def->tooltip = L("Angle (degrees) for the scale function on this axis."); - def->sidetext = L("°"); - def->min = 0.1; - def->max = 89.9; - def->mode = mode; - def->set_default_value(new ConfigOptionFloat(45)); - }; - - add_belt_shear_mode ("gcode_shear_x", "Function", BeltShearMode::None, comExpert); - add_belt_shear_angle("gcode_shear_x_angle", "Angle", comExpert); - add_belt_axis_enum ("gcode_shear_x_from", "From", "Source axis for X shear in the machine-frame stage.", BeltAxis::Z, comExpert); - - add_belt_shear_mode ("gcode_shear_y", "Function", BeltShearMode::None, comExpert); - add_belt_shear_angle("gcode_shear_y_angle", "Angle", comExpert); - add_belt_axis_enum ("gcode_shear_y_from", "From", "Source axis for Y shear in the machine-frame stage.", BeltAxis::Z, comExpert); - - add_belt_shear_mode ("gcode_shear_z", "Function", BeltShearMode::None); - add_belt_shear_angle("gcode_shear_z_angle", "Angle"); - add_belt_axis_enum ("gcode_shear_z_from", "From", "Source axis for Z shear in the machine-frame stage.", BeltAxis::Y); - - add_belt_scale_mode ("gcode_scale_x", "Function", BeltScaleMode::None, comExpert); - add_belt_scale_angle("gcode_scale_x_angle", "Angle", comExpert); - - add_belt_scale_mode ("gcode_scale_y", "Function", BeltScaleMode::None); - add_belt_scale_angle("gcode_scale_y_angle", "Angle"); - - add_belt_scale_mode ("gcode_scale_z", "Function", BeltScaleMode::None, comExpert); - add_belt_scale_angle("gcode_scale_z_angle", "Angle", comExpert); - - add_belt_transform_order("belt_gcode_transform_order", "G-code transform order", - "Order in which the machine-frame shear and scale matrices are composed when " - "applied to G-code coordinates. 'Scale, then shear' applies scale first and then " - "shear (current default). 'Shear, then scale' applies shear first and then scale."); + // 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"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index a31179cd9b..47da8f1b55 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -169,34 +169,10 @@ enum class SlicingMode CloseHoles, }; -enum class BeltShearMode -{ - None, // No shear on this axis - PosCot, // += cot(α) - NegCot, // -= cot(α) - PosTan, // += tan(α) - NegTan, // -= tan(α) -}; - -enum class BeltScaleMode -{ - None, // No scaling (factor = 1) - InvSin, // 1/sin(α) - InvCos, // 1/cos(α) - Sin, // sin(α) - Cos, // cos(α) -}; - -enum class BeltAxis -{ - X = 0, - Y = 1, - Z = 2, -}; - // Axis around which the mesh is rotated before slicing, when -// `belt_slice_rotation` is set. None disables the rotation stage. -// Distinct from BeltAxis because BeltAxis carries no "None" semantics. +// `belt_slice_rotation` is set. None disables the rotation stage. This is the +// single "belt tilt" axis: it drives both the pre-slice mesh rotation and the +// post-slice machine-frame transform (shear + scale derived from the tilt angle). enum class BeltRotationAxis { None = 0, @@ -205,15 +181,6 @@ enum class BeltRotationAxis Z = 3, }; -// Order in which the belt shear and scale matrices are composed. -// ScaleThenShear: applied to a point p, the result is shear(scale(p)). -// ShearThenScale: applied to a point p, the result is scale(shear(p)). -enum class BeltTransformOrder -{ - ScaleThenShear = 0, - ShearThenScale = 1, -}; - enum class RemapAxis { PosX = 0, PosY = 1, PosZ = 2, @@ -616,11 +583,7 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(NoiseType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(InfillPattern) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(IroningType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SlicingMode) -CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltShearMode) -CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltScaleMode) -CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltAxis) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltRotationAxis) -CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltTransformOrder) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(RemapAxis) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltSupportFloorMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltSupportZOffsetMode) @@ -1582,15 +1545,19 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( // Belt printer settings (printer-level). ((ConfigOptionBool, belt_printer)) ((ConfigOptionBool, belt_printer_infinite_y)) - // Mesh rotation applied before slicing — the sole mesh-side belt transform and - // the single source of truth for the physical belt tilt (its angle + axis drive - // bed rendering, support gravity tilt, and the bed-exclusion projection). - // Isometric (no distortion); the g-code back-transform inverts it before the - // machine-frame remap. (Shear & scale are applied to the g-code, not the - // mesh — see gcode_shear_* / gcode_scale_* below.) + // Mesh rotation applied before slicing — the single source of truth for the + // physical belt tilt. Its angle + axis drive bed rendering, support gravity + // tilt, the bed-exclusion projection, AND the post-slice machine-frame + // transform (shear + scale, derived from the tilt angle; see + // MachineFrameTransform). Isometric (no distortion) on the mesh side; the + // g-code back-transform inverts the rotation before the machine-frame stage. ((ConfigOptionEnum, 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, preslice_remap_x)) ((ConfigOptionEnum, preslice_remap_y)) ((ConfigOptionEnum, preslice_remap_z)) @@ -1598,22 +1565,6 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionEnum, gcode_remap_x)) ((ConfigOptionEnum, gcode_remap_y)) ((ConfigOptionEnum, gcode_remap_z)) - ((ConfigOptionEnum, gcode_shear_x)) - ((ConfigOptionFloat, gcode_shear_x_angle)) - ((ConfigOptionEnum, gcode_shear_x_from)) - ((ConfigOptionEnum, gcode_shear_y)) - ((ConfigOptionFloat, gcode_shear_y_angle)) - ((ConfigOptionEnum, gcode_shear_y_from)) - ((ConfigOptionEnum, gcode_shear_z)) - ((ConfigOptionFloat, gcode_shear_z_angle)) - ((ConfigOptionEnum, gcode_shear_z_from)) - ((ConfigOptionEnum, gcode_scale_x)) - ((ConfigOptionFloat, gcode_scale_x_angle)) - ((ConfigOptionEnum, gcode_scale_y)) - ((ConfigOptionFloat, gcode_scale_y_angle)) - ((ConfigOptionEnum, gcode_scale_z)) - ((ConfigOptionFloat, gcode_scale_z_angle)) - ((ConfigOptionEnum, belt_gcode_transform_order)) ((ConfigOptionBool, gcode_back_transform)) ((ConfigOptionBool, belt_preslice_global)) ((ConfigOptionEnum, first_layer_plane)) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 4027a671e7..ae9223ad7f 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4541,9 +4541,10 @@ void TabPrinter::build_fff() belt_og->append_single_option_line("belt_support_z_offset_mode"); belt_og->append_single_option_line("belt_support_floor_mode"); - // Machine-frame transforms: applied to G-code after back-transform and - // gcode_remap, before per-axis origin snap. Maps Cartesian G-code into - // the printer's physical machine frame. + // Machine-frame transform: the shear (tan) + scale (1/cos) 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. { auto mf = page->new_optgroup(L("Machine frame transforms"), L"param_advanced"); { @@ -4554,45 +4555,15 @@ void TabPrinter::build_fff() mf->append_line(line); } { - Line line = { L("G-code shear X"), L("Shear applied to the X axis in the machine-frame stage (after back-transform and gcode_remap).") }; - line.append_option(mf->get_option("gcode_shear_x")); - line.append_option(mf->get_option("gcode_shear_x_angle")); - line.append_option(mf->get_option("gcode_shear_x_from")); + 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.append_option(mf->get_option("belt_frame_tilt_decouple")); + line.append_option(mf->get_option("belt_frame_tilt_angle")); mf->append_line(line); } - { - Line line = { L("G-code shear Y"), L("Shear applied to the Y axis in the machine-frame stage (after back-transform and gcode_remap).") }; - line.append_option(mf->get_option("gcode_shear_y")); - line.append_option(mf->get_option("gcode_shear_y_angle")); - line.append_option(mf->get_option("gcode_shear_y_from")); - mf->append_line(line); - } - { - Line line = { L("G-code shear Z"), L("Shear applied to the Z axis in the machine-frame stage (after back-transform and gcode_remap).") }; - line.append_option(mf->get_option("gcode_shear_z")); - line.append_option(mf->get_option("gcode_shear_z_angle")); - line.append_option(mf->get_option("gcode_shear_z_from")); - mf->append_line(line); - } - { - Line line = { L("G-code scale X"), L("Scale applied to the X axis in the machine-frame stage.") }; - line.append_option(mf->get_option("gcode_scale_x")); - line.append_option(mf->get_option("gcode_scale_x_angle")); - mf->append_line(line); - } - { - Line line = { L("G-code scale Y"), L("Scale applied to the Y axis in the machine-frame stage.") }; - line.append_option(mf->get_option("gcode_scale_y")); - line.append_option(mf->get_option("gcode_scale_y_angle")); - mf->append_line(line); - } - { - Line line = { L("G-code scale Z"), L("Scale applied to the Z axis in the machine-frame stage.") }; - line.append_option(mf->get_option("gcode_scale_z")); - line.append_option(mf->get_option("gcode_scale_z_angle")); - mf->append_line(line); - } - mf->append_single_option_line("belt_gcode_transform_order"); } option = optgroup->get_option("thumbnails"); @@ -5592,36 +5563,11 @@ void TabPrinter::toggle_options() 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 transforms: shown only in belt mode. - // Mirror the Advanced/Expert split used for mesh shear/scale. - toggle_line("gcode_shear_x", is_belt && expert_or_above); - toggle_line("gcode_shear_y", is_belt && expert_or_above); - toggle_line("gcode_shear_z", is_belt); - toggle_line("gcode_scale_x", is_belt && expert_or_above); - toggle_line("gcode_scale_y", is_belt); - toggle_line("gcode_scale_z", is_belt && expert_or_above); - toggle_line("belt_gcode_transform_order", is_belt); - - auto gsx = m_config->option>("gcode_shear_x")->value; - toggle_option("gcode_shear_x_angle", is_belt && gsx != BeltShearMode::None); - toggle_option("gcode_shear_x_from", is_belt && gsx != BeltShearMode::None); - - auto gsy = m_config->option>("gcode_shear_y")->value; - toggle_option("gcode_shear_y_angle", is_belt && gsy != BeltShearMode::None); - toggle_option("gcode_shear_y_from", is_belt && gsy != BeltShearMode::None); - - auto gsz = m_config->option>("gcode_shear_z")->value; - toggle_option("gcode_shear_z_angle", is_belt && gsz != BeltShearMode::None); - toggle_option("gcode_shear_z_from", is_belt && gsz != BeltShearMode::None); - - auto gscx = m_config->option>("gcode_scale_x")->value; - toggle_option("gcode_scale_x_angle", is_belt && gscx != BeltScaleMode::None); - - auto gscy = m_config->option>("gcode_scale_y")->value; - toggle_option("gcode_scale_y_angle", is_belt && gscy != BeltScaleMode::None); - - auto gscz = m_config->option>("gcode_scale_z")->value; - toggle_option("gcode_scale_z_angle", is_belt && gscz != BeltScaleMode::None); + // Machine-frame transform: derived from the belt tilt. Only the expert + // decouple override is exposed; its angle is enabled 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")); // First-layer plane: visible alongside the rest of belt-printer settings. toggle_line("first_layer_plane", is_belt); From 5b243eec9245703510b283a9465675c691228dbe Mon Sep 17 00:00:00 2001 From: Joseph Robertson <32232925+HarrierPigeon@users.noreply.github.com> Date: Mon, 8 Jun 2026 06:58:26 -0500 Subject: [PATCH 041/224] Relocate Pre-Slice remap logic --- src/libslic3r/BeltSliceStrategy.cpp | 212 ++++++++++++++-------------- src/libslic3r/BeltSliceStrategy.hpp | 43 +++--- src/libslic3r/PrintObjectSlice.cpp | 35 +---- 3 files changed, 132 insertions(+), 158 deletions(-) diff --git a/src/libslic3r/BeltSliceStrategy.cpp b/src/libslic3r/BeltSliceStrategy.cpp index fd1bfcc191..cdb6ba428f 100644 --- a/src/libslic3r/BeltSliceStrategy.cpp +++ b/src/libslic3r/BeltSliceStrategy.cpp @@ -1,6 +1,8 @@ #include "BeltSliceStrategy.hpp" #include "Model.hpp" +#include + #include #ifdef SLIC3R_BELT_DIAGNOSTIC_LOG #include @@ -10,130 +12,132 @@ namespace Slic3r { -std::unique_ptr BeltSliceStrategy::create(const PrintConfig &config) +void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo, + const PrintConfig &config, + const ModelVolumePtrs &model_volumes, + double *out_belt_min_z) { - if (!config.belt_printer.value) - return nullptr; - return std::unique_ptr(new BeltSliceStrategy(config)); -} + // 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; -BeltSliceStrategy::BeltSliceStrategy(const PrintConfig &config) -{ - m_rotation = BeltTransformPipeline::build_rotation_matrix(config, &m_has_rotation); -} - -void BeltSliceStrategy::apply_to_trafo(Transform3d &trafo, - const ModelVolumePtrs &model_volumes, - bool has_remap, - double *out_belt_min_z) const -{ - // Rotation is the only mesh-side belt transform (matching build_forward_transform). - if (m_has_rotation) { - Transform3d belt_xform = Transform3d::Identity(); - belt_xform.linear() = m_rotation; - trafo = belt_xform * trafo; + // 2. Belt rotation — the sole mesh-side belt transform (matching + // BeltTransformPipeline::build_forward_transform). Only active in + // belt-printer mode. + bool has_rotation = false; + if (config.belt_printer.value) { + const Matrix3d rot = BeltTransformPipeline::build_rotation_matrix(config, &has_rotation); + if (has_rotation) { + Transform3d belt_xform = Transform3d::Identity(); + belt_xform.linear() = rot; + trafo = belt_xform * trafo; + } } - // Z-shift — detect if mesh clips below build plate after transforms. - // 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 - // step on assemblies (where per-volume get_matrix() positions each volume - // within the object) causes min_z to be computed against mesh-local vertex - // coordinates rather than object-space coordinates, so volumes translated - // along the slicer's Z axis are silently excluded from the bound check. - if (has_remap || m_has_rotation) { + if (!has_remap && !has_rotation) + return; + + // 3. 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 + // each volume within the object) would compute min_z against mesh-local vertex + // coordinates rather than object-space coordinates, so volumes translated along + // the slicer's Z axis would be silently excluded from the bound check. #ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - // Capture the incoming trafo for diagnostic logging. - // This is the slicer-frame transform AFTER belt_xform but BEFORE z_shift. - const Transform3d trafo_pre_shift = trafo; - auto log_mat = [](const Matrix3d &m) { - std::ostringstream ss; - ss << std::fixed << std::setprecision(4); - ss << "[[" << m(0,0) << "," << m(0,1) << "," << m(0,2) << "]," - << "[" << m(1,0) << "," << m(1,1) << "," << m(1,2) << "]," - << "[" << m(2,0) << "," << m(2,1) << "," << m(2,2) << "]]"; - return ss.str(); - }; - auto log_vec3 = [](const Vec3d &v) { - std::ostringstream ss; - ss << std::fixed << std::setprecision(4); - ss << "(" << v.x() << "," << v.y() << "," << v.z() << ")"; - return ss.str(); - }; - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] apply_to_trafo enter" - << " has_rotation=" << m_has_rotation - << " has_remap=" << has_remap - << " trafo.linear=" << log_mat(trafo_pre_shift.linear()) - << " trafo.translation=" << log_vec3(trafo_pre_shift.translation()) - << " volumes=" << model_volumes.size(); + // Capture the incoming trafo for diagnostic logging. + // This is the slicer-frame transform AFTER remap + rotation but BEFORE z_shift. + const Transform3d trafo_pre_shift = trafo; + auto log_mat = [](const Matrix3d &m) { + std::ostringstream ss; + ss << std::fixed << std::setprecision(4); + ss << "[[" << m(0,0) << "," << m(0,1) << "," << m(0,2) << "]," + << "[" << m(1,0) << "," << m(1,1) << "," << m(1,2) << "]," + << "[" << m(2,0) << "," << m(2,1) << "," << m(2,2) << "]]"; + return ss.str(); + }; + auto log_vec3 = [](const Vec3d &v) { + std::ostringstream ss; + ss << std::fixed << std::setprecision(4); + ss << "(" << v.x() << "," << v.y() << "," << v.z() << ")"; + return ss.str(); + }; + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] apply_preslice_transforms enter" + << " has_rotation=" << has_rotation + << " has_remap=" << has_remap + << " trafo.linear=" << log_mat(trafo_pre_shift.linear()) + << " trafo.translation=" << log_vec3(trafo_pre_shift.translation()) + << " volumes=" << model_volumes.size(); #endif - double min_z = std::numeric_limits::max(); + double min_z = std::numeric_limits::max(); #ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - int vol_idx = 0; + int vol_idx = 0; #endif - for (const ModelVolume *mv : model_volumes) { + for (const ModelVolume *mv : model_volumes) { #ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - if (!mv->is_model_part()) { ++vol_idx; continue; } + if (!mv->is_model_part()) { ++vol_idx; continue; } #else - if (!mv->is_model_part()) continue; + if (!mv->is_model_part()) continue; #endif - Transform3d vol_trafo = trafo * mv->get_matrix(); - const auto &its = mv->mesh().its; + Transform3d vol_trafo = trafo * mv->get_matrix(); + const auto &its = mv->mesh().its; #ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - // Per-volume bbox in mesh-frame and post-trafo slicer-frame. - Vec3d mesh_min(std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::max()); - Vec3d mesh_max(std::numeric_limits::lowest(), std::numeric_limits::lowest(), std::numeric_limits::lowest()); - Vec3d slicer_min(std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::max()); - Vec3d slicer_max(std::numeric_limits::lowest(), std::numeric_limits::lowest(), std::numeric_limits::lowest()); - double vol_min_z = std::numeric_limits::max(); + // Per-volume bbox in mesh-frame and post-trafo slicer-frame. + Vec3d mesh_min(std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::max()); + Vec3d mesh_max(std::numeric_limits::lowest(), std::numeric_limits::lowest(), std::numeric_limits::lowest()); + Vec3d slicer_min(std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::max()); + Vec3d slicer_max(std::numeric_limits::lowest(), std::numeric_limits::lowest(), std::numeric_limits::lowest()); + double vol_min_z = std::numeric_limits::max(); #endif - for (const stl_vertex &v : its.vertices) { - Vec3d vm = v.cast(); - Vec3d pt = vol_trafo * vm; - min_z = std::min(min_z, pt.z()); + for (const stl_vertex &v : its.vertices) { + Vec3d vm = v.cast(); + Vec3d pt = vol_trafo * vm; + min_z = std::min(min_z, pt.z()); #ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - mesh_min = mesh_min.cwiseMin(vm); - mesh_max = mesh_max.cwiseMax(vm); - slicer_min = slicer_min.cwiseMin(pt); - slicer_max = slicer_max.cwiseMax(pt); - vol_min_z = std::min(vol_min_z, pt.z()); -#endif - } -#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] vol[" << vol_idx - << "] id=" << mv->id().id << " name='" << mv->name << "'" - << " mesh_bbox_min=" << log_vec3(mesh_min) << " mesh_bbox_max=" << log_vec3(mesh_max) - << " get_matrix.translation=" << log_vec3(mv->get_matrix().translation()) - << " slicer_bbox_min=" << log_vec3(slicer_min) << " slicer_bbox_max=" << log_vec3(slicer_max) - << " vol_min_z=" << vol_min_z; - ++vol_idx; + mesh_min = mesh_min.cwiseMin(vm); + mesh_max = mesh_max.cwiseMax(vm); + slicer_min = slicer_min.cwiseMin(pt); + slicer_max = slicer_max.cwiseMax(pt); + vol_min_z = std::min(vol_min_z, pt.z()); #endif } - double belt_z_shift_val = (min_z < 0. && min_z != std::numeric_limits::max()) ? -min_z : 0.; #ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] combined min_z=" << min_z - << " z_shift_val=" << belt_z_shift_val; -#endif - if (belt_z_shift_val > 0.) { - Transform3d z_shift = Transform3d::Identity(); - z_shift.matrix()(2, 3) = belt_z_shift_val; - trafo = z_shift * trafo; - } - if (out_belt_min_z) { - double new_val = (min_z != std::numeric_limits::max()) ? min_z : 0.; -#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] write m_belt_min_z tid=" << std::this_thread::get_id() - << " target=" << out_belt_min_z << " old=" << *out_belt_min_z << " new=" << new_val; -#endif - *out_belt_min_z = new_val; - } -#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] apply_to_trafo exit" - << " final_trafo.linear=" << log_mat(trafo.linear()) - << " final_trafo.translation=" << log_vec3(trafo.translation()); + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] vol[" << vol_idx + << "] id=" << mv->id().id << " name='" << mv->name << "'" + << " mesh_bbox_min=" << log_vec3(mesh_min) << " mesh_bbox_max=" << log_vec3(mesh_max) + << " get_matrix.translation=" << log_vec3(mv->get_matrix().translation()) + << " slicer_bbox_min=" << log_vec3(slicer_min) << " slicer_bbox_max=" << log_vec3(slicer_max) + << " vol_min_z=" << vol_min_z; + ++vol_idx; #endif } + const double z_shift_val = (min_z < 0. && min_z != std::numeric_limits::max()) ? -min_z : 0.; +#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] combined min_z=" << min_z + << " z_shift_val=" << z_shift_val; +#endif + if (z_shift_val > 0.) { + Transform3d z_shift = Transform3d::Identity(); + z_shift.matrix()(2, 3) = z_shift_val; + trafo = z_shift * trafo; + } + // out_belt_min_z is only meaningful in belt mode; the standalone-remap path + // never reported it. + if (out_belt_min_z && config.belt_printer.value) { + const double new_val = (min_z != std::numeric_limits::max()) ? min_z : 0.; +#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] write m_belt_min_z tid=" << std::this_thread::get_id() + << " target=" << out_belt_min_z << " old=" << *out_belt_min_z << " new=" << new_val; +#endif + *out_belt_min_z = new_val; + } +#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] apply_preslice_transforms exit" + << " final_trafo.linear=" << log_mat(trafo.linear()) + << " final_trafo.translation=" << log_vec3(trafo.translation()); +#endif } } // namespace Slic3r diff --git a/src/libslic3r/BeltSliceStrategy.hpp b/src/libslic3r/BeltSliceStrategy.hpp index ff56f6fece..33a7ed4979 100644 --- a/src/libslic3r/BeltSliceStrategy.hpp +++ b/src/libslic3r/BeltSliceStrategy.hpp @@ -6,36 +6,31 @@ #include "PrintConfig.hpp" #include "Model.hpp" -#include -#include - namespace Slic3r { -// Belt printer pre-slice transform strategy. +// Belt printer / pre-slice transform strategy. // -// Encapsulates the pre-remap, rotation, and Z-shift transforms that are -// applied to model geometry before slicing on belt printers. (Shear & scale -// are applied to the g-code, not the mesh.) Used by PrintObjectSlice.cpp to -// isolate belt-specific logic from the slicing pipeline. +// Composes, in order, the pre-slice mesh transforms applied before slicing: +// 1. Pre-slice axis remap (standalone — works without belt mode) +// 2. Belt rotation (the sole mesh-side belt transform; shear & scale are a +// g-code-side stage, see MachineFrameTransform) +// 3. Per-object Z-shift that lifts the mesh above the build plate +// +// Isolates this belt/remap-specific logic from the generic slicing pipeline in +// PrintObjectSlice.cpp. class BeltSliceStrategy { public: - // Create a strategy if belt_printer is enabled; returns nullptr otherwise. - static std::unique_ptr create(const PrintConfig &config); - - // Apply belt-specific transforms (rotation + z-shift) to the slicing trafo. - // Pre-slice remap is handled separately (standalone feature). - // has_remap: whether pre-slice remap was already applied (affects z-shift detection). - void apply_to_trafo(Transform3d &trafo, - const ModelVolumePtrs &model_volumes, - bool has_remap, - double *out_belt_min_z) const; - -private: - explicit BeltSliceStrategy(const PrintConfig &config); - - bool m_has_rotation = false; - Matrix3d m_rotation = Matrix3d::Identity(); + // Apply the pre-slice remap + belt rotation + Z-shift to `trafo` in place. + // No-op when neither a remap nor a belt rotation is configured. + // + // out_belt_min_z (if non-null) receives the minimum mesh Z after the + // transforms, but only in belt-printer mode — the standalone-remap path + // never reported it. + static void apply_preslice_transforms(Transform3d &trafo, + const PrintConfig &config, + const ModelVolumePtrs &model_volumes, + double *out_belt_min_z = nullptr); }; } // namespace Slic3r diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 448b39fe0b..7e5203d624 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -173,35 +173,10 @@ static std::vector slice_volumes_inner( params_base.closing_radius = print_object_config.slice_closing_radius.value; params_base.extra_offset = 0; params_base.trafo = object_trafo; - // Standalone pre-slice axis remap (works without belt mode). - bool has_remap = BeltTransformPipeline::has_preslice_remap(print_config); - if (has_remap) - params_base.trafo = BeltTransformPipeline::build_preslice_remap(print_config) * params_base.trafo; - { - // Belt-specific transforms (shear, scale, z-shift) via strategy. - auto belt_strategy = BeltSliceStrategy::create(print_config); - if (belt_strategy) - belt_strategy->apply_to_trafo(params_base.trafo, model_volumes, has_remap, out_belt_min_z); - } - // If only remap (no belt), still need z-shift detection. - // Compose per-volume mv->get_matrix() so assembly volumes contribute their - // actual object-space positions — see the matching note in BeltSliceStrategy. - if (has_remap && !print_config.belt_printer.value) { - double min_z = std::numeric_limits::max(); - for (const ModelVolume *mv : model_volumes) { - if (!mv->is_model_part()) continue; - Transform3d vol_trafo = params_base.trafo * mv->get_matrix(); - for (const stl_vertex &v : mv->mesh().its.vertices) { - Vec3d pt = vol_trafo * v.cast(); - min_z = std::min(min_z, pt.z()); - } - } - if (min_z < 0. && min_z != std::numeric_limits::max()) { - Transform3d z_shift = Transform3d::Identity(); - z_shift.matrix()(2, 3) = -min_z; - params_base.trafo = z_shift * params_base.trafo; - } - } + // Pre-slice mesh transforms: axis remap (standalone — works without belt + // mode), belt rotation, and the per-object Z-shift. Owned by BeltSliceStrategy + // so this belt/remap-specific logic stays out of the generic slicing pipeline. + BeltSliceStrategy::apply_preslice_transforms(params_base.trafo, print_config, model_volumes, out_belt_min_z); //BBS: 0.0025mm is safe enough to simplify the data to speed slicing up for high-resolution model. //Also has on influence on arc fitting which has default resolution 0.0125mm. params_base.resolution = print_config.resolution <= 0.001 ? 0.0f : 0.0025; @@ -981,7 +956,7 @@ void PrintObject::slice() // Per-object Z-shift compensation, applied regardless of global mode. // - // BeltSliceStrategy::apply_to_trafo lifts the mesh by max(0, -m_belt_min_z) + // BeltSliceStrategy::apply_preslice_transforms lifts the mesh by max(0, -m_belt_min_z) // so the slicer can slice with slicer_z >= 0. BeltBackTransform inverts // build_forward_transform() which DOES NOT include this per-object // Z-shift (it's not known until vertex scan time). Result: G-code From e29a82c6723b7088d1eef92c14a9d89c1af5fee4 Mon Sep 17 00:00:00 2001 From: Joseph Robertson <32232925+HarrierPigeon@users.noreply.github.com> Date: Mon, 8 Jun 2026 07:18:55 -0500 Subject: [PATCH 042/224] add attribution and design notes --- src/libslic3r/BeltTransform.hpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/libslic3r/BeltTransform.hpp b/src/libslic3r/BeltTransform.hpp index b67ac2007d..ad5b6d6ffd 100644 --- a/src/libslic3r/BeltTransform.hpp +++ b/src/libslic3r/BeltTransform.hpp @@ -23,6 +23,24 @@ class ModelObject; // object-dependent (computed from mesh vertex bounds) and is NOT included in // build_forward_transform(). The machine-frame shear/scale is derived directly // from the tilt angle in MachineFrameTransform and no longer lives here. +// +// Design note: this mesh-rotation approach replaced an earlier pre-shear +// method (now removed). While that initial pre-shear method was instrumental +// in getting belt printer slicing off the ground in the first place, its place is +// in the past. A big thank you goes to the Unlayered3D team, who recommended +// switching to a pre-slice rotation stage instead. Doing so keeps the slicing +// operation isometric — no distortion of the sliced geometry — while the +// non-orthogonal machine-axis compensation is confined to a g-code-side shear/scale +// derived from the same tilt angle. +// +// This fixed a number of issues, including several issues noticed by hotcubcar +// regarding adaptive infills not working, gyroid becoming anisotropic, and more +// that were all mostly resolved as a result of the switch. +// +// This also means that the pre-slice rotation transform methodology can be used +// more cleanly on non-belt printers. +// - HarrierPigeon (Joseph Robertson) + class BeltTransformPipeline { public: From 2bcb775b904973d0250bc85539a3d6a0e3fca567 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 10 Jun 2026 04:49:31 -0500 Subject: [PATCH 043/224] update IdeaFormer profiles to new generic belt printer config --- .../machine/IdeaFormer IR3 V2 0.4 nozzle.json | 11 ----------- .../profiles/IdeaFormer/machine/fdm_belt_common.json | 2 -- 2 files changed, 13 deletions(-) diff --git a/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json b/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json index b1a2cc2c3b..bb987e712d 100644 --- a/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json +++ b/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json @@ -17,18 +17,7 @@ "0x2000" ], "printable_height": "250", - "belt_printer": "1", "belt_printer_infinite_y": "1", - "belt_slice_rotation": "x", - "belt_slice_rotation_angle": "45", - "belt_slice_rotation_global": "1", - "build_plate_tilt_x": "45", - "build_plate_tilt_y": "0", - "gcode_remap_x": "rev_x", - "gcode_remap_y": "pos_z", - "gcode_remap_z": "pos_y", - "gcode_shear_z": "pos_tan", - "gcode_scale_y": "inv_cos", "thumbnails": [ "48x48/PNG", "300x300/PNG" diff --git a/resources/profiles/IdeaFormer/machine/fdm_belt_common.json b/resources/profiles/IdeaFormer/machine/fdm_belt_common.json index 0530999f15..b010742d78 100644 --- a/resources/profiles/IdeaFormer/machine/fdm_belt_common.json +++ b/resources/profiles/IdeaFormer/machine/fdm_belt_common.json @@ -93,8 +93,6 @@ "belt_slice_rotation_angle": "45", "belt_slice_rotation_global": "1", "build_plate_tilt_x": "45", - "gcode_shear_z": "pos_tan", - "gcode_scale_y": "inv_cos", "purge_in_prime_tower": "0", "scan_first_layer": "0", "auxiliary_fan": "0" From f682ab5cd3939ed65c5ff043dba780b8da29d828 Mon Sep 17 00:00:00 2001 From: Tommaso Bianchi Date: Wed, 10 Jun 2026 21:17:47 +0200 Subject: [PATCH 044/224] belt: replace height-check skip with a belt-correct vertical-clearance check The original PR skipped the max-print-height check entirely on belt printers because the sliced (virtual) Z is belt travel, not build height. As the reviewer noted, that removed the only working height guard. Restore a correct guard: - Print::validate: on belt printers, compare the upright object height (max over instances of the scene-space bbox) against printable_height directly. printable_height is the usable VERTICAL clearance above the belt: the gantry travels up the tilted plane (reach = height/cos(tilt)) and its axis range is sized for that (IR3 V2: ~354 mm gantry travel = 250 mm vertical at 45deg, and printable_height = 250). Hardware-confirmed 250 mm vertical clearance, so no cos(tilt) factor is applied. - BuildVolume::set_belt_printer: drop the diagonal Z scaling; the build-volume Z already equals printable_height, keeping the live 'outside build volume' highlight in agreement with validate(). Co-Authored-By: Claude Opus 4.8 --- src/libslic3r/BuildVolume.cpp | 11 +++++------ src/libslic3r/Print.cpp | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/libslic3r/BuildVolume.cpp b/src/libslic3r/BuildVolume.cpp index 2e194bdd7e..c9e779f9e7 100644 --- a/src/libslic3r/BuildVolume.cpp +++ b/src/libslic3r/BuildVolume.cpp @@ -193,12 +193,11 @@ void BuildVolume::set_belt_printer(bool enabled, double angle_deg, bool infinite // Extend the Y bound to a very large value for infinite belt. m_bboxf.max.y() = 100000.; } - // Adjust max print height to diagonal reach: printable_height / sin(belt_angle). - if (angle_deg > 0. && angle_deg < 90.) { - double sin_a = std::sin(angle_deg * M_PI / 180.0); - if (sin_a > 0.) - m_bboxf.max.z() = m_max_print_height / sin_a; - } + // Belt printer: the Z extent already equals printable_height (set above), which + // is the usable vertical clearance above the belt. The gantry's axis range is + // sized to reach height/cos(tilt), so no diagonal scaling is applied here — this + // keeps the live "outside build volume" highlight in agreement with Print::validate(). + (void) angle_deg; } } diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index b511898ae8..257aa19f1d 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1406,6 +1406,28 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* // Checks that the print does not exceed the max print height for (size_t print_object_idx = 0; print_object_idx < m_objects.size(); ++ print_object_idx) { const PrintObject &print_object = *m_objects[print_object_idx]; + // Belt printer: the sliced (virtual) Z is belt travel along the conveyor, not + // build height, so the loop below (which measures the sliced layer stack) is + // meaningless here. The real ceiling is the usable vertical clearance above the + // belt, which printable_height holds directly: the gantry travels up the tilted + // plane, so a part of height z needs gantry reach z / cos(tilt), and the machine's + // gantry-axis range is sized to accommodate that (e.g. IdeaFormer IR3 V2: ~354 mm + // of gantry travel = 250 mm vertical at 45°, and printable_height = 250). So compare + // the upright object height against printable_height directly. + if (m_config.belt_printer.value) { + const double max_h = m_config.printable_height.value; + double obj_top = 0.0; + const ModelObject *mo = print_object.model_object(); + for (const ModelInstance *mi : mo->instances) + obj_top = std::max(obj_top, mo->instance_bounding_box(*mi).max.z()); + if (obj_top > max_h + EPSILON) + return StringObjectException{ + Slic3r::format(_u8L("The object %1% exceeds the maximum printable height " + "above the belt (%2% mm)."), + print_object.model_object()->name, max_h), + print_object.model_object(), "" }; + continue; + } //FIXME It is quite expensive to generate object layers just to get the print height! if (auto layers = generate_object_layers(print_object.slicing_parameters(), layer_height_profile(print_object_idx), print_object.config().precise_z_height.value); !layers.empty()) { From 4f3a6080090a5eb944d9ea5ca584b9500e872f93 Mon Sep 17 00:00:00 2001 From: Tommaso Bianchi <163908227+tommasobbianchi@users.noreply.github.com> Date: Thu, 11 Jun 2026 06:54:02 +0200 Subject: [PATCH 045/224] belt: don't flag the lead-in as an empty-layer error on belt printers (#47) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit collect_layers_to_print() warns (CRITICAL) when an extrusion layer sits above the previous one with an empty gap below — the fixed-bed assumption that material with nothing under it is floating and unprintable. On a belt printer a *leading* empty range (the gap starts at Z=0, no prior extrusion layer) is not floating: it is the conveyor lead-in, and the part rests on the advancing belt as the first material is laid down well above Z=0. A part not designed for a belt (e.g. a flat test model tilted into the belt frame) then trips this as a false "Object can't be printed for empty layer between 0 and N" error. Suppress only the leading case (belt_printer && last_extrusion_layer == null); genuine internal gaps are still flagged, since on a belt those can be an over-angle overhang printing into air. Non-belt output is unchanged. --- src/libslic3r/GCode.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index c25a657953..3f21616bbb 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -1814,8 +1814,20 @@ std::vector GCode::collect_layers_to_print(const PrintObjec + std::max(0., extra_gap); // Negative support_contact_z is not taken into account, it can result in false positives in cases - if (has_extrusions && layer_to_print.print_z() > maximal_print_z + 2. * EPSILON) - warning_ranges.emplace_back(std::make_pair((last_extrusion_layer ? last_extrusion_layer->print_z() : 0.), layers_to_print.back().print_z())); + if (has_extrusions && layer_to_print.print_z() > maximal_print_z + 2. * EPSILON) { + // Belt printers: a *leading* empty range (no prior extrusion layer, so the + // gap starts at Z=0) is not a floating object — it is just the belt lead-in. + // The part rests on the conveyor as it advances, so the first material can + // legitimately appear well above Z=0. This empty-layer check assumes a fixed + // bed, where material with nothing below it is unprintable; that assumption + // does not hold on a belt for the lead-in. Suppress only this leading case, + // and keep flagging genuine *internal* gaps (which on a belt may still be an + // over-angle overhang that would print into air). + const bool belt_leading_gap = object.print()->config().belt_printer.value + && last_extrusion_layer == nullptr; + if (!belt_leading_gap) + warning_ranges.emplace_back(std::make_pair((last_extrusion_layer ? last_extrusion_layer->print_z() : 0.), layers_to_print.back().print_z())); + } } // Remember last layer with extrusions. if (has_extrusions) From b7bda9912b29f2caa8b3ae540655cb1954e81808 Mon Sep 17 00:00:00 2001 From: Tommaso Bianchi <163908227+tommasobbianchi@users.noreply.github.com> Date: Thu, 11 Jun 2026 16:30:35 +0200 Subject: [PATCH 046/224] belt: fix IR3 V2 end G-code reversing the belt into the part (#56) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The IdeaFormer IR3 V2 End G-code ran `G28 ; home all`, which homes the Z (belt) and Y (gantry) axes. On a belt printer Z is the conveyor, so homing it runs the belt all the way back to origin, dragging the finished part back under the gantry that G28 has just lowered — the head knocks the print (reported by an IR3 V2 user; the `G1 Y50` lift came after the G28, too late). Replace the end sequence with a belt-safe one: switch to relative mode (G91), lift the gantry for clearance, advance the belt forward one full machine-depth (Z676, the 676 mm product depth) to eject the part and cycle the belt surface clean, then home X only — never the Z/belt axis. --- .../IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json b/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json index bb987e712d..b6f128ca86 100644 --- a/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json +++ b/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json @@ -88,7 +88,7 @@ "300" ], "machine_start_gcode": "; === IdeaFormer IR3 V2 Belt Printer Start ===\n; Axes: X=lateral, Y=gantry height (probe), Z=belt\nG90 ; absolute positioning\nM82 ; absolute extruder\nG21 ; millimeters\nG28 ; home all axes\nG1 Y20 F500 ; lift nozzle 20mm from belt\n; Bed + hotend temps come from the active filament profile. Belt PLA requires 75 C bed — use Generic/eSun PLA @IdeaFormer IR3 V2 filament presets to get it automatically.\nM140 S[hot_plate_temp_initial_layer] ; set bed temp\nM104 S[nozzle_temperature_initial_layer] ; hotend temp\nM109 S[nozzle_temperature_initial_layer] ; wait hotend\nM190 S[hot_plate_temp_initial_layer] ; wait bed\n; --- Purge blob ---\nG92 E0 ; zero extruder\nG1 Y.1 ; nozzle 0.1mm above belt\nG1 E15 F1000 ; purge 15mm blob\nG1 Z20 E25 F800 ; belt advance 20mm + extrude\nG1 E23 ; retract 2mm\nG28 Y ; re-probe belt surface\nG1 E25 ; de-retract\n; --- Prime lines (full 250mm bed width) ---\nFMS_on ; filament motion sensor\nG1 X250 E50 F2000 ; prime line 1\nG92 Z0 ; reset belt origin\nG1 Z.4 ; belt advance 0.4mm\nG1 X0 E75 ; prime line 2\nG1 F1000 ; default feedrate\nG92 E0 Z0 ; zero extruder + belt = print origin\n", - "machine_end_gcode": "; === IdeaFormer IR3 V2 Belt Printer End ===\nM400 ; wait for moves to finish\nG28 X ; home X axis\nM104 S0 ; heater off\nM140 S0 ; bed off\nG92 E0 ; zero extruder\nG1 E-5 F300 ; retract 5mm\nG4 P7000 ; wait 7s for ooze\nG28 ; home all\nG1 Y50 F1000 ; lift nozzle 50mm\nFMS_off ; filament motion sensor off\nBED_MESH_CLEAR\nM84 ; disable motors\n", + "machine_end_gcode": "; === IdeaFormer IR3 V2 Belt Printer End ===\nM400 ; wait for moves to finish\nM104 S0 ; heater off\nM140 S0 ; bed off\nG92 E0 ; zero extruder\nG1 E-5 F300 ; retract 5mm\nG4 P5000 ; wait for ooze\nG91 ; relative mode - keep every end move relative on a belt\nG1 Y20 F1000 ; raise gantry 20mm for clearance over the part\nG1 Z676 F3000 ; advance belt one full machine-depth to eject the part and clean the belt\nG90 ; back to absolute\nG28 X ; home X only - NEVER 'G28' all: that homes Z/belt and reverses the whole print back into the gantry\nFMS_off ; filament motion sensor off\nBED_MESH_CLEAR\nM84 ; disable motors\n", "machine_pause_gcode": "PAUSE", "layer_change_gcode": "G92 E0 ; belt: reset extruder at layer change (relative E)" } From 0da24cd38b99dc149b02194457b84c2ee35d6a27 Mon Sep 17 00:00:00 2001 From: Joseph Robertson <32232925+HarrierPigeon@users.noreply.github.com> Date: Fri, 12 Jun 2026 03:14:12 -0500 Subject: [PATCH 047/224] Belt/Standard calibrations (#54) Enables supported printing of standard Orcaslicer calibration profiles. * Build 2 Checkpoint * fix support generation wedge, ghost layers * flip cornering tests 180 deg to waste less supports * fix row spacing on the flow ratio calibrations * more testing, this didn't fix anything * switched rotation tools, same issue * fixed Z-offset issues * add rest of PA features, may look a bit weird on a belt * make temp towers work * re-enable spiral on calibrations that want it * Final cleanup pre-PR and community testing --- src/libslic3r/BeltGCodeWriter.cpp | 6 +- src/libslic3r/BeltGCodeWriter.hpp | 9 + src/libslic3r/GCode.cpp | 97 ++++++-- src/libslic3r/PrintObjectSlice.cpp | 9 +- src/libslic3r/Support/TreeSupport.cpp | 64 +++++- src/libslic3r/calib.cpp | 82 ++++--- src/libslic3r/calib.hpp | 5 +- src/slic3r/GUI/Plater.cpp | 304 +++++++++++++++++++++++++- src/slic3r/GUI/Plater.hpp | 2 + src/slic3r/GUI/calib_dlg.cpp | 35 +++ 10 files changed, 554 insertions(+), 59 deletions(-) diff --git a/src/libslic3r/BeltGCodeWriter.cpp b/src/libslic3r/BeltGCodeWriter.cpp index a2322c9243..7c645edf43 100644 --- a/src/libslic3r/BeltGCodeWriter.cpp +++ b/src/libslic3r/BeltGCodeWriter.cpp @@ -38,7 +38,11 @@ void BeltGCodeWriter::set_machine_frame_transform(const PrintConfig &config) Vec3d BeltGCodeWriter::to_machine_coords(const Vec3d &pos) const { // Step 1+2: To Cartesian (back_transform + axis_remap). - Vec3d after_back = m_belt_back_transform.apply(pos); + // In world-coordinates mode (PA line / PA pattern calibration) the input + // already describes a point relative to the belt surface, so the + // slicer->world back-transform is skipped and only the machine kinematics + // (axis remap + frame shear/scale) are applied. + Vec3d after_back = m_world_coordinates ? pos : m_belt_back_transform.apply(pos); Vec3d result = apply_axis_remap(after_back); Vec3d after_remap = result; // Step 3: Machine-frame transform (belt frame tilt) applied LAST so it acts diff --git a/src/libslic3r/BeltGCodeWriter.hpp b/src/libslic3r/BeltGCodeWriter.hpp index 30159f4f33..ff7428d062 100644 --- a/src/libslic3r/BeltGCodeWriter.hpp +++ b/src/libslic3r/BeltGCodeWriter.hpp @@ -23,6 +23,14 @@ public: void set_machine_frame_transform(const PrintConfig &config); Vec3d to_machine_coords(const Vec3d &pos) const; + // World-coordinates mode: incoming coordinates are treated as points + // relative to the physical belt surface (X across, Y along the belt, + // Z height above it) instead of slicing-frame coordinates — the + // slicer->world back-transform is skipped. Used by the PA line / PA + // pattern calibration generators, whose logical bed coordinates describe + // first-layer drawings on the build surface. + void set_world_coordinates(bool enable) { m_world_coordinates = enable; } + // First-layer plane: when set to a non-null active evaluator, travel // speed selection consults the plane per-move and uses // initial_layer_travel_speed for points within first_layer_height_mm @@ -47,6 +55,7 @@ protected: private: BeltBackTransform m_belt_back_transform; MachineFrameTransform m_machine_frame_transform; + bool m_world_coordinates = false; // Borrowed pointer; lifetime owned by GCode. null = inactive. const FirstLayerPlane *m_first_layer_plane = nullptr; double m_first_layer_thickness_mm = 0.; diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 3f21616bbb..af59428c89 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -1834,6 +1834,12 @@ std::vector GCode::collect_layers_to_print(const PrintObjec last_extrusion_layer = &layers_to_print.back(); } + // ORCA-Belt: objects print at their position along the belt, so the first + // extrusions legitimately start far above Z=0. Drop the spurious + // "empty layers from the bed" range while keeping genuine mid-print gaps. + if (skip_empty_first_layer && !warning_ranges.empty() && warning_ranges.front().first == 0.) + warning_ranges.erase(warning_ranges.begin()); + if (! warning_ranges.empty()) { std::string warning; size_t i = 0; @@ -3307,7 +3313,16 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato pa_test.set_speed(fast_speed, slow_speed); pa_test.draw_numbers() = print.calib_params().print_numbers; + + // ORCA-Belt: the PA line test draws directly on the build surface in + // logical bed coordinates — on a belt printer that surface is the + // belt plane, not the slicing plane. + BeltGCodeWriter* belt_writer = dynamic_cast(m_writer.get()); + if (belt_writer != nullptr) + belt_writer->set_world_coordinates(true); gcode += pa_test.generate_test(params.start, params.step, std::llround(std::ceil((params.end - params.start) / params.step)) + 1); + if (belt_writer != nullptr) + belt_writer->set_world_coordinates(false); file.write(gcode); } else { @@ -4653,31 +4668,57 @@ LayerResult GCode::process_layer( gcode += ";_SET_FAN_SPEED_CHANGING_LAYER\n"; //Calibration Layer-specific GCode + // ORCA-Belt: on belt printers the calibration object is counter-rotated to + // stand upright in slicing space on top of a support wedge, so its first + // layer starts above Z=0 (at its position along the belt) with support-only + // layers below it. Reference the per-height calibration bands to the bottom + // of the object so they keep their designed meaning; on regular printers + // the object base is at Z=0 and calib_z == print_z. + double calib_z = print_z; + if (m_config.belt_printer.value && print.calib_mode() != CalibMode::Calib_None) { + // Skip empty ghost layers the grid may produce below the object. + for (const Layer* l : layer.object()->layers()) + if (!l->lslices.empty()) { + calib_z = print_z - (l->print_z - l->height); + break; + } + } switch (print.calib_mode()) { case CalibMode::Calib_PA_Tower: { - gcode += writer().set_pressure_advance(print.calib_params().start + static_cast(print_z) * print.calib_params().step); + gcode += writer().set_pressure_advance(print.calib_params().start + static_cast(std::max(0.0, calib_z)) * print.calib_params().step); break; } case CalibMode::Calib_Temp_Tower: { - gcode += writer().set_temperature(this->interpolate_value_across_layers(static_cast(print.calib_params().start), static_cast(print.calib_params().end), 5.0f)); + // ORCA-Belt: the sectioned variant prints each temperature as its + // own object in native belt orientation, with the temperature + // encoded in the object name ("temp_230") — step per object + // instead of ramping per layer band. + int sectioned_temp = 0; + if (m_config.belt_printer.value && + sscanf(layer.object()->model_object()->name.c_str(), "temp_%d", §ioned_temp) == 1 && + sectioned_temp > 0) { + gcode += writer().set_temperature(static_cast(sectioned_temp)); + } else { + gcode += writer().set_temperature(this->interpolate_value_across_layers(static_cast(print.calib_params().start), static_cast(print.calib_params().end), 5.0f)); + } break; } case CalibMode::Calib_VFA_Tower: { - auto _speed = print.calib_params().start + std::floor(print_z / 5.0) * print.calib_params().step; + auto _speed = print.calib_params().start + std::floor(std::max(0.0, calib_z) / 5.0) * print.calib_params().step; m_calib_config.set_key_value("outer_wall_speed", new ConfigOptionFloat(std::round(_speed))); break; } case CalibMode::Calib_Vol_speed_Tower: { - auto _speed = print.calib_params().start + print_z * print.calib_params().step; + auto _speed = print.calib_params().start + std::max(0.0, calib_z) * print.calib_params().step; m_calib_config.set_key_value("outer_wall_speed", new ConfigOptionFloat(std::round(_speed))); break; } case CalibMode::Calib_Retraction_tower: { - auto _length = print.calib_params().start + std::floor(std::max(0.0,print_z-0.4)) * print.calib_params().step; + auto _length = print.calib_params().start + std::floor(std::max(0.0,calib_z-0.4)) * print.calib_params().step; DynamicConfig _cfg; _cfg.set_key_value("retraction_length", new ConfigOptionFloats{_length}); writer().config.apply(_cfg); - sprintf(buf, "; Calib_Retraction_tower: Z_HEIGHT: %g, length:%g\n", print_z, _length); + sprintf(buf, "; Calib_Retraction_tower: Z_HEIGHT: %g, length:%g\n", calib_z, _length); gcode += buf; break; } @@ -7415,25 +7456,39 @@ std::string GCode::extrusion_role_to_string_for_parser(const ExtrusionRole & rol // Step = 0 means gradual interpolation finishing at last value. float GCode::interpolate_value_across_layers(float start_value, float end_value, float step) const { - if (m_layer_index <= 1) { + float ratio; + // ORCA-Belt: counter-rotated calibration objects stand on a support wedge, + // so support-only layers below the object would stretch a layer-index + // interpolation. Use the object's own Z span instead, so the value ramps + // across the test geometry only. + if (m_config.belt_printer.value && m_layer != nullptr && !m_layer->object()->layers().empty()) { + const auto& layers = m_layer->object()->layers(); + // Skip empty ghost layers the grid may produce below the object. + double z_min = layers.front()->print_z; + for (const Layer* l : layers) + if (!l->lslices.empty()) { z_min = l->print_z; break; } + const double z_max = layers.back()->print_z; + if (m_layer->print_z <= z_min + EPSILON || z_max - z_min <= EPSILON) + return start_value; + ratio = float(std::min(1.0, (m_layer->print_z - z_min) / (z_max - z_min))); + } else if (m_layer_index <= 1) { return start_value; + } else { + ratio = m_layer_index / (m_layer_count - 1.f); } - else { - bool use_steps = step > 0.f; - if (use_steps) { - if (start_value > end_value) { - start_value += step; - } else { - end_value += step; - } + bool use_steps = step > 0.f; + if (use_steps) { + if (start_value > end_value) { + start_value += step; + } else { + end_value += step; } - float ratio = m_layer_index / (m_layer_count - 1.f); - float value = start_value + ratio * (end_value - start_value); - if (use_steps) { - value = trunc(value / step) * step; - } - return value; } + float value = start_value + ratio * (end_value - start_value); + if (use_steps) { + value = trunc(value / step) * step; + } + return value; } std::string encodeBase64(uint64_t value) diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 7e5203d624..1212100eac 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -966,7 +966,14 @@ void PrintObject::slice() // 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.; - double belt_z_shift = m_belt_min_z - belt_surface_z; + // 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 + // asymmetric geometry whose centered-frame minimum lands positive) + // no lift was applied, and an unclamped m_belt_min_z here would + // leak straight into the layer Z values, floating the whole object + // off the belt by exactly that amount. + double belt_z_shift = std::min(m_belt_min_z, 0.) - belt_surface_z; double global_z_offset = belt_z_shift; // Centering correction: trafo_centered pretranslates by diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index e8009cf5c5..8c0fe205a4 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -1801,8 +1801,7 @@ void TreeSupport::generate() { BeltFloorContext ctx; if (ctx.init(m_slicing_params, *m_print_config) - && m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly - && m_object->support_layer_count() > 0) { + && 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; @@ -1830,16 +1829,61 @@ void TreeSupport::generate() } } } + // 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)); + BOOST_LOG_TRIVIAL(debug) << "[BELT-CALIB] wedge seed: obj=" << m_object->model_object()->name + << " bottom_z=" << first_layer->bottom_z() << " floating=" << floating.size(); + 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()); - double first_z = m_object->get_support_layer(0)->print_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; @@ -1853,8 +1897,15 @@ void TreeSupport::generate() sl->base_areas = clipped; // Populate area_groups — generate_toolpaths() iterates these, // not base_areas directly. - for (auto &expoly : sl->base_areas) - sl->area_groups.emplace_back(&expoly, SupportLayer::BaseType, 0); + // 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) @@ -1865,6 +1916,9 @@ void TreeSupport::generate() 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()); + BOOST_LOG_TRIVIAL(debug) << "[BELT-CALIB] wedge ext layers=" << belt_ext_layers.size() + << " z=" << belt_ext_layers.front()->print_z << ".." << belt_ext_layers.back()->print_z + << " seeded=" << seeded; } } } diff --git a/src/libslic3r/calib.cpp b/src/libslic3r/calib.cpp index 4b3e05df8d..3509b32257 100644 --- a/src/libslic3r/calib.cpp +++ b/src/libslic3r/calib.cpp @@ -1,4 +1,5 @@ #include "calib.hpp" +#include "BeltGCodeWriter.hpp" #include "BoundingBox.hpp" #include "Config.hpp" #include "Model.hpp" @@ -587,21 +588,21 @@ CustomGCode::Info CalibPressureAdvancePattern::generate_custom_gcodes(const Dyna refresh_setup(config, is_bbl_machine, object, origin); - gcode << move_to(Vec2d(m_starting_point.x(), m_starting_point.y()), m_writer, "Move to start XY position"); - gcode << m_writer.travel_to_z(height_first_layer() + height_z_offset(), "Move to start Z position"); - gcode << m_writer.set_pressure_advance(m_params.start); + gcode << move_to(Vec2d(m_starting_point.x(), m_starting_point.y()), *m_writer, "Move to start XY position"); + gcode << m_writer->travel_to_z(height_first_layer() + height_z_offset(), "Move to start Z position"); + gcode << m_writer->set_pressure_advance(m_params.start); const DrawBoxOptArgs default_box_opt_args(wall_count(), height_first_layer(), line_width_first_layer(), speed_adjust(speed_first_layer())); // create anchoring frame - gcode << draw_box(m_writer, m_starting_point.x(), m_starting_point.y(), print_size_x(), frame_size_y(), default_box_opt_args); + gcode << draw_box(*m_writer, m_starting_point.x(), m_starting_point.y(), print_size_x(), frame_size_y(), default_box_opt_args); // create tab for numbers DrawBoxOptArgs draw_box_opt_args = default_box_opt_args; draw_box_opt_args.is_filled = true; draw_box_opt_args.num_perimeters = wall_count(); - gcode << draw_box(m_writer, m_starting_point.x(), m_starting_point.y() + frame_size_y() + line_spacing_first_layer(), + gcode << draw_box(*m_writer, m_starting_point.x(), m_starting_point.y() + frame_size_y() + line_spacing_first_layer(), print_size_x(), max_numbering_height() + line_spacing_first_layer() + m_glyph_padding_vertical * 2, draw_box_opt_args); @@ -627,15 +628,15 @@ CustomGCode::Info CalibPressureAdvancePattern::generate_custom_gcodes(const Dyna gcode = std::stringstream(); // reset for next layer contents gcode << "; start pressure advance pattern for layer\n"; - gcode << m_writer.travel_to_z(layer_height, "Move to layer height"); - gcode << m_writer.reset_e(); + gcode << m_writer->travel_to_z(layer_height, "Move to layer height"); + gcode << m_writer->reset_e(); } // line numbering if (i == 1) { m_number_len = max_numbering_length(); - gcode << m_writer.set_pressure_advance(m_params.start); + gcode << m_writer->set_pressure_advance(m_params.start); double number_e_per_mm = e_per_mm(line_width(), height_layer(), m_config.option("nozzle_diameter")->get_at(0), @@ -646,20 +647,20 @@ CustomGCode::Info CalibPressureAdvancePattern::generate_custom_gcodes(const Dyna for (int j = 0; j < num_patterns; j += 2) { gcode << draw_number(glyph_start_x(j), m_starting_point.y() + frame_size_y() + m_glyph_padding_vertical + line_width(), m_params.start + (j * m_params.step), m_draw_digit_mode, line_width(), number_e_per_mm, - speed_first_layer(), m_writer); + speed_first_layer(), *m_writer); } // flow value int line_num = num_patterns + 2; gcode << draw_number(glyph_start_x(line_num), m_starting_point.y() + frame_size_y() + m_glyph_padding_vertical + line_width(), flow_val(), m_draw_digit_mode, line_width(), number_e_per_mm, - speed_first_layer(), m_writer); + speed_first_layer(), *m_writer); // acceleration line_num = num_patterns + 4; gcode << draw_number(glyph_start_x(line_num), m_starting_point.y() + frame_size_y() + m_glyph_padding_vertical + line_width(), accel, m_draw_digit_mode, line_width(), number_e_per_mm, - speed_first_layer(), m_writer); + speed_first_layer(), *m_writer); } @@ -678,20 +679,20 @@ CustomGCode::Info CalibPressureAdvancePattern::generate_custom_gcodes(const Dyna /* Draw a line at slightly slower accel and speed in order to trick gcode writer to force update acceleration and speed. * We do this since several tests may be generated by their own gcode writers which are * not aware about their neighbours updating acceleration/speed */ - gcode << m_writer.set_print_acceleration(std::max(1, accel - 1)); - gcode << move_to(Vec2d(m_starting_point.x(), m_starting_point.y()), m_writer, "Move to starting point", zhop_height, layer_height); - gcode << draw_line(m_writer, Vec2d(m_starting_point.x(), m_starting_point.y() + frame_size_y()), line_width(), height_layer(), speed_adjust(std::max(1, speed_perimeter() - 1)), "Accel/flow trick line"); - gcode << m_writer.set_print_acceleration(accel); + gcode << m_writer->set_print_acceleration(std::max(1, accel - 1)); + gcode << move_to(Vec2d(m_starting_point.x(), m_starting_point.y()), *m_writer, "Move to starting point", zhop_height, layer_height); + gcode << draw_line(*m_writer, Vec2d(m_starting_point.x(), m_starting_point.y() + frame_size_y()), line_width(), height_layer(), speed_adjust(std::max(1, speed_perimeter() - 1)), "Accel/flow trick line"); + gcode << m_writer->set_print_acceleration(accel); } double initial_x = to_x; double initial_y = to_y; - gcode << move_to(Vec2d(to_x, to_y), m_writer, "Move to pattern start",zhop_height,layer_height); + gcode << move_to(Vec2d(to_x, to_y), *m_writer, "Move to pattern start",zhop_height,layer_height); for (int j = 0; j < num_patterns; ++j) { // increment pressure advance - gcode << m_writer.set_pressure_advance(m_params.start + (j * m_params.step)); + gcode << m_writer->set_pressure_advance(m_params.start + (j * m_params.step)); for (int k = 0; k < wall_count(); ++k) { to_x += std::cos(to_radians(m_corner_angle) / 2) * side_length; @@ -701,27 +702,27 @@ CustomGCode::Info CalibPressureAdvancePattern::generate_custom_gcodes(const Dyna auto draw_line_arg_line_width = line_width(); // don't use line_width_first_layer so results are consistent across all layers auto draw_line_arg_speed = i == 0 ? speed_adjust(speed_first_layer()) : speed_adjust(speed_perimeter()); auto draw_line_arg_comment = "Print pattern wall"; - gcode << draw_line(m_writer, Vec2d(to_x, to_y), draw_line_arg_line_width, draw_line_arg_height, draw_line_arg_speed, draw_line_arg_comment); + gcode << draw_line(*m_writer, Vec2d(to_x, to_y), draw_line_arg_line_width, draw_line_arg_height, draw_line_arg_speed, draw_line_arg_comment); to_x -= std::cos(to_radians(m_corner_angle) / 2) * side_length; to_y += std::sin(to_radians(m_corner_angle) / 2) * side_length; - gcode << draw_line(m_writer, Vec2d(to_x, to_y), draw_line_arg_line_width, draw_line_arg_height, draw_line_arg_speed, draw_line_arg_comment); + gcode << draw_line(*m_writer, Vec2d(to_x, to_y), draw_line_arg_line_width, draw_line_arg_height, draw_line_arg_speed, draw_line_arg_comment); to_y = initial_y; if (k != wall_count() - 1) { // perimeters not done yet. move to next perimeter to_x += line_spacing_angle(); - gcode << move_to(Vec2d(to_x, to_y), m_writer, "Move to start next pattern wall", zhop_height, layer_height); // Call move to command with XY as well as z hop and layer height to invoke and undo z lift + gcode << move_to(Vec2d(to_x, to_y), *m_writer, "Move to start next pattern wall", zhop_height, layer_height); // Call move to command with XY as well as z hop and layer height to invoke and undo z lift } else if (j != num_patterns - 1) { // patterns not done yet. move to next pattern to_x += m_pattern_spacing + line_width(); - gcode << move_to(Vec2d(to_x, to_y), m_writer, "Move to next pattern", zhop_height, layer_height); // Call move to command with XY as well as z hop and layer height to invoke and undo z lift + gcode << move_to(Vec2d(to_x, to_y), *m_writer, "Move to next pattern", zhop_height, layer_height); // Call move to command with XY as well as z hop and layer height to invoke and undo z lift } else if (i != m_num_layers - 1) { // layers not done yet. move back to start to_x = initial_x; - gcode << move_to(Vec2d(to_x, to_y), m_writer, "Move back to start position", zhop_height, layer_height); // Call move to command with XY as well as z hop and layer height to invoke and undo z lift - gcode << m_writer.reset_e(); // reset extruder before printing placeholder cube to avoid over extrusion + gcode << move_to(Vec2d(to_x, to_y), *m_writer, "Move back to start position", zhop_height, layer_height); // Call move to command with XY as well as z hop and layer height to invoke and undo z lift + gcode << m_writer->reset_e(); // reset extruder before printing placeholder cube to avoid over extrusion } else { // everything done } @@ -729,7 +730,7 @@ CustomGCode::Info CalibPressureAdvancePattern::generate_custom_gcodes(const Dyna } } - gcode << m_writer.set_pressure_advance(m_params.start); + gcode << m_writer->set_pressure_advance(m_params.start); gcode << "; end pressure advance pattern for layer\n"; CustomGCode::Item item; @@ -796,13 +797,36 @@ void CalibPressureAdvancePattern::_refresh_writer(bool is_bbl_machine, const Mod PrintConfig print_config; print_config.apply(m_config, true); - m_writer.apply_print_config(print_config); - m_writer.set_xy_offset(origin(0), origin(1)); - m_writer.set_is_bbl_machine(is_bbl_machine); + // ORCA-Belt: the pattern is drawn in logical bed coordinates directly on + // the build surface — on a belt printer that means the belt plane, which + // needs the machine kinematics (axis remap + frame shear/scale) with the + // coordinates interpreted as world points (see set_world_coordinates). + if (print_config.belt_printer.value) { + auto belt_writer = std::make_shared(); + belt_writer->set_belt_back_transform(print_config); + belt_writer->set_machine_frame_transform(print_config); + belt_writer->set_world_coordinates(true); + const int rx = int(print_config.gcode_remap_x.value); + const int ry = int(print_config.gcode_remap_y.value); + const int rz = int(print_config.gcode_remap_z.value); + if (rx != 0 || ry != 1 || rz != 2) { + belt_writer->set_axis_remap(rx, ry, rz); + BoundingBoxf bbox_bed(print_config.printable_area.values); + belt_writer->set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(), + print_config.printable_height.value)); + } + m_writer = std::move(belt_writer); + } else if (dynamic_cast(m_writer.get()) != nullptr) { + m_writer = std::make_shared(); + } + + m_writer->apply_print_config(print_config); + m_writer->set_xy_offset(origin(0), origin(1)); + m_writer->set_is_bbl_machine(is_bbl_machine); const unsigned int extruder_id = object.volumes.front()->extruder_id(); - m_writer.set_extruders({extruder_id}); - m_writer.set_extruder(extruder_id); + m_writer->set_extruders({extruder_id}); + m_writer->set_extruder(extruder_id); } double CalibPressureAdvancePattern::object_size_x() const diff --git a/src/libslic3r/calib.hpp b/src/libslic3r/calib.hpp index cbac1d3f86..b36c75db62 100644 --- a/src/libslic3r/calib.hpp +++ b/src/libslic3r/calib.hpp @@ -358,7 +358,10 @@ private: const Calib_Params &m_params; - GCodeWriter m_writer; + // Polymorphic so belt printers get a BeltGCodeWriter in world-coordinates + // mode (_refresh_writer); shared_ptr keeps the class copyable — the writer + // is rebuilt by refresh_setup() before every use anyway. + std::shared_ptr m_writer{std::make_shared()}; Vec3d m_starting_point; bool m_is_start_point_fixed = false; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 823deb0aea..6bc6a30f0f 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -12594,8 +12594,181 @@ void Plater::add_model(bool imperial_units, std::string fname) } } +// ORCA-Belt: belt-printer handling for the desktop calibration tests. +// +// Belt slicing applies a global pre-slice rotation R(angle, axis) to every +// mesh (see BeltTransform.hpp) so the slicing planes match the tilted gantry. +// Calibration models are designed for upright slicing: their per-height test +// bands and XY-plane quality features assume slicer Z is the model's own Z +// axis. Counter-rotating each calibration object by the inverse rotation in +// world space cancels the global rotation, so in slicing space the object +// stands upright exactly as on a flat-bed printer and every test keeps its +// designed meaning. Physically the object then leans over the belt with its +// bottom face overhanging, so per-object supports fill the wedge between the +// bottom face and the belt. The wedge prints entirely below the object's base +// plane and leaves the test geometry untouched. Manual tree support is used +// so the deliberate bridge/overhang features of the test models stay +// unsupported; the wedge under the floating bottom face is built by the +// belt-floor extension in TreeSupport::generate(), which stacks the floating +// first-layer footprint down to the belt surface. +static bool belt_calib_rotation_params(double& angle_rad, Vec3d& axis) +{ + const auto& printer_config = wxGetApp().preset_bundle->printers.get_edited_preset().config; + const auto* belt_opt = printer_config.option("belt_printer"); + if (belt_opt == nullptr || !belt_opt->value) + return false; + const auto* axis_opt = printer_config.option>("belt_slice_rotation"); + const auto* angle_opt = printer_config.option("belt_slice_rotation_angle"); + if (axis_opt == nullptr || angle_opt == nullptr) + return false; + switch (axis_opt->value) { + case BeltRotationAxis::X: axis = Vec3d::UnitX(); break; + case BeltRotationAxis::Y: axis = Vec3d::UnitY(); break; + // Z rotation is an in-plane spin and None means no tilt; objects already + // slice upright in those cases and need no special handling. + default: return false; + } + angle_rad = -Geometry::deg2rad(angle_opt->value); + return std::abs(angle_rad) > EPSILON; +} + +// ORCA-Belt: flip the ringing tower 180° about Z before the belt +// counter-rotation — its sloped face then leans over the belt and the +// support wedge gets much smaller. +static void belt_calib_flip_ringing_tower(Model &model) +{ + double angle_rad = 0.; + Vec3d axis = Vec3d::UnitX(); + if (belt_calib_rotation_params(angle_rad, axis) && !model.objects.empty()) + model.objects.front()->rotate(M_PI, Vec3d::UnitZ()); +} + +void Plater::_calib_apply_belt_mode() +{ + double angle_rad = 0.; + Vec3d axis = Vec3d::UnitX(); + if (!belt_calib_rotation_params(angle_rad, axis)) + return; + + auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + // Spiral vase stays enabled where the tests request it: the support wedge + // lies strictly below the object, support layers never spiralize + // (spiral_vase_enable requires an object layer), and the spiral/support + // exclusivity check only applies to globally enabled supports — the wedge + // uses per-object supports. + // A skirt would be drawn in the first slicing plane, which lies mostly + // above the belt surface. + print_config->set_key_value("skirt_loops", new ConfigOptionInt(0)); + + const Matrix3d cancel_rotation = Eigen::AngleAxisd(angle_rad, axis).toRotationMatrix(); + std::vector obj_idxs; + for (size_t i = 0; i < model().objects.size(); ++i) { + ModelObject* obj = model().objects[i]; + if (obj->instances.size() != 1) + continue; + obj_idxs.emplace_back(i); + + // Manual tree support: only the floating bottom face gets a support + // wedge (via the belt-floor extension in TreeSupport::generate()), + // leaving the test features untouched. The style is pinned to hybrid + // because the default style resolves to organic, which bypasses the + // non-organic generator that hosts the belt-floor extension. + obj->config.set_key_value("enable_support", new ConfigOptionBool(true)); + obj->config.set_key_value("support_type", new ConfigOptionEnum(stTree)); + obj->config.set_key_value("support_style", new ConfigOptionEnum(smsTreeHybrid)); + obj->config.set_key_value("support_on_build_plate_only", new ConfigOptionBool(false)); + // With the default base pattern, tree support base areas print as + // hollow outlines (no infill) — the wedge needs a real pattern. + obj->config.set_key_value("support_base_pattern", new ConfigOptionEnum(smpRectilinear)); + + // Counter-rotate exactly the way the rotate gizmo would: rotation on + // the instance, then a plain drop to the bed. This leaves the object + // in the same state shape as any manually rotated object, which the + // belt pipeline is known to handle. + ModelInstance* inst = obj->instances.front(); + inst->rotate(cancel_rotation); + obj->invalidate_bounding_box(); + obj->ensure_on_bed(); + + { + const BoundingBoxf3 rb = obj->raw_bounding_box(); + const Vec3d io = inst->get_offset(); + const Vec3d ir = inst->get_rotation(); + BOOST_LOG_TRIVIAL(debug) << "[BELT-CALIB] helper exit: obj=" << obj->name + << " inst_offset=(" << io.x() << "," << io.y() << "," << io.z() << ")" + << " inst_rot=(" << ir.x() << "," << ir.y() << "," << ir.z() << ")" + << " vol0_offset=(" << obj->volumes.front()->get_offset().x() << "," + << obj->volumes.front()->get_offset().y() << "," << obj->volumes.front()->get_offset().z() << ")" + << " raw_bbox=(" << rb.min.x() << "," << rb.min.y() << "," << rb.min.z() + << ")..(" << rb.max.x() << "," << rb.max.y() << "," << rb.max.z() << ")" + << " min_z=" << obj->min_z(); + } + } + + // Each object's support wedge extends upstream of it by roughly its own + // depth (at 45°), so the tight flat-bed layouts of the multi-part tests + // leave wedges intersecting the neighbouring parts. Keep the grid rows of + // the test layouts together and open up the space between rows just + // enough for the wedge shadow. + if (obj_idxs.size() > 1) { + std::vector sorted_objs; + sorted_objs.reserve(obj_idxs.size()); + for (size_t i : obj_idxs) + sorted_objs.emplace_back(model().objects[i]); + std::sort(sorted_objs.begin(), sorted_objs.end(), [](const ModelObject* a, const ModelObject* b) { + return a->instances.front()->get_offset(Y) < b->instances.front()->get_offset(Y); + }); + std::vector> rows; + double row_y = std::numeric_limits::quiet_NaN(); + for (ModelObject* o : sorted_objs) { + const double oy = o->instances.front()->get_offset(Y); + if (rows.empty() || oy - row_y > 1.) + rows.emplace_back(); + rows.back().emplace_back(o); + row_y = oy; + } + const double wedge_factor = std::abs(std::tan(angle_rad)); + double cursor = std::numeric_limits::quiet_NaN(); + for (std::vector& row : rows) { + double rmin = std::numeric_limits::max(); + double rmax = std::numeric_limits::lowest(); + for (ModelObject* o : row) { + const BoundingBoxf3 bb = o->instance_bounding_box(0); + rmin = std::min(rmin, bb.min.y()); + rmax = std::max(rmax, bb.max.y()); + } + if (std::isnan(cursor)) + cursor = rmin; // the first row anchors the layout + const double shift = cursor - rmin; + for (ModelObject* o : row) { + ModelInstance* inst = o->instances.front(); + inst->set_offset(Y, inst->get_offset(Y) + shift); + o->invalidate_bounding_box(); + } + cursor += (rmax - rmin) * (1. + wedge_factor) + 5.; + } + } + + wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty(); + wxGetApp().get_tab(Preset::TYPE_PRINT)->reload_config(); + changed_objects(obj_idxs); +} + void Plater::calib_pa(const Calib_Params& params) { + // ORCA-Belt: PA Line / PA Pattern have the belt plumbing in place + // (BeltGCodeWriter::set_world_coordinates draws them on the belt surface) + // but are not validated yet — keep them gated to the PA Tower for now. + { + double angle_rad = 0.; + Vec3d axis = Vec3d::UnitX(); + if (belt_calib_rotation_params(angle_rad, axis) && params.mode != CalibMode::Calib_PA_Tower) { + MessageDialog msg_dlg(nullptr, _L("PA Line and PA Pattern tests are not enabled yet on belt printers.\nPlease use the PA Tower method instead."), + wxEmptyString, wxICON_WARNING | wxOK); + msg_dlg.ShowModal(); + return; + } + } const auto calib_pa_name = wxString::Format(L"Pressure Advance Test"); new_project(false, false, calib_pa_name); wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); @@ -12927,6 +13100,7 @@ void Plater::_calib_pa_tower(const Calib_Params& params) { cut_horizontal(0, 0, new_height, ModelObjectCutAttribute::KeepLower); } + _calib_apply_belt_mode(); _calib_pa_select_added_objects(); } @@ -13104,6 +13278,8 @@ void Plater::calib_flowrate(bool is_linear, int pass, InfillPattern pattern) { auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); + _calib_apply_belt_mode(); + // Refresh object after scaling const std::vector object_idx(boost::counting_iterator(0), boost::counting_iterator(model().objects.size())); changed_objects(object_idx); @@ -13120,7 +13296,19 @@ void Plater::calib_temp(const Calib_Params& params) { wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); if (params.mode != CalibMode::Calib_Temp_Tower) return; - + + // ORCA-Belt: a counter-rotated tower would cantilever every block off a + // support wedge — print the temperature blocks as separate objects in + // native belt orientation instead. + { + double belt_angle_rad = 0.; + Vec3d belt_axis = Vec3d::UnitX(); + if (belt_calib_rotation_params(belt_angle_rad, belt_axis)) { + _calib_temp_belt_sectioned(params, std::abs(belt_angle_rad)); + return; + } + } + add_model(false, Slic3r::resources_dir() + "/calib/temperature_tower/temperature_tower.drc"); auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; @@ -13190,6 +13378,107 @@ void Plater::calib_temp(const Calib_Params& params) { p->background_process.fff_print()->set_calib_params(params); } +// ORCA-Belt: sectioned temperature test. Each temperature gets its own block +// cut out of the temperature tower model, printed in native belt orientation +// (no counter-rotation, no support wedge) and spaced along the belt so the +// blocks' layer ranges are disjoint — they print strictly one after another, +// starting with the start temperature closest to the gantry. The temperature +// is encoded in the object name ("temp_230") and applied per object by the +// Calib_Temp_Tower handler at G-code time, replacing the per-layer-band ramp +// that only makes sense for a monolithic upright tower. +void Plater::_calib_temp_belt_sectioned(const Calib_Params& params, double belt_angle_rad) +{ + constexpr double base_temp_tower_nozzle_diameter = 0.4; + constexpr double base_temp_tower_block_height = 10.0; + constexpr int base_temp_tower_temp_step = 5; + + auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; + auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; + auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + + const long start_temp = lround(params.start); + const long end_temp = lround(params.end); + const int n_blocks = std::max(1, int((start_temp - end_temp) / base_temp_tower_temp_step) + 1); + + const ConfigOptionFloats* nozzle_diameter_config = printer_config->option("nozzle_diameter"); + size_t nozzle_id = static_cast(std::max(params.extruder_id, 0)); + double nozzle_diameter = base_temp_tower_nozzle_diameter; + if (nozzle_diameter_config && !nozzle_diameter_config->values.empty()) { + nozzle_id = std::min(nozzle_id, nozzle_diameter_config->values.size() - 1); + nozzle_diameter = nozzle_diameter_config->values[nozzle_id]; + } + if (nozzle_diameter <= 0.0) + nozzle_diameter = base_temp_tower_nozzle_diameter; + const double nozzle_scale = nozzle_diameter / base_temp_tower_nozzle_diameter; + + std::vector obj_idxs; + for (int i = 0; i < n_blocks; ++i) { + const long temp = start_temp - long(i) * base_temp_tower_temp_step; + const size_t count_before = model().objects.size(); + add_model(false, Slic3r::resources_dir() + "/calib/temperature_tower/temperature_tower.drc"); + if (model().objects.size() <= count_before) + break; // model failed to load — don't index into an empty list + // The cut replaces the object at the END of the list, so re-acquire + // the index after every operation. + size_t obj_idx = model().objects.size() - 1; + + // Isolate this temperature's block (full-tower coordinates, the same + // 500-down-to-temp indexing the monolithic flow cuts with). + const double block_bottom = double(lround(double(500 - temp) / base_temp_tower_temp_step)) * base_temp_tower_block_height; + auto obj_bb = model().objects[obj_idx]->bounding_box_exact(); + if (block_bottom + base_temp_tower_block_height < obj_bb.size().z()) { + cut_horizontal(obj_idx, 0, block_bottom + base_temp_tower_block_height - EPSILON, ModelObjectCutAttribute::KeepLower); + obj_idx = model().objects.size() - 1; + } + if (block_bottom > 0) { + cut_horizontal(obj_idx, 0, block_bottom + EPSILON, ModelObjectCutAttribute::KeepUpper); + obj_idx = model().objects.size() - 1; + } + + ModelObject* obj = model().objects[obj_idx]; + if (std::abs(nozzle_scale - 1.0) > EPSILON) + obj->scale(nozzle_scale, nozzle_scale, nozzle_scale); + obj->name = std::string("temp_") + std::to_string(temp); + obj->config.set_key_value("layer_height", new ConfigOptionFloat(nozzle_diameter / 2)); + obj->config.set_key_value("alternate_extra_wall", new ConfigOptionBool(false)); + obj->config.set_key_value("seam_slope_type", new ConfigOptionEnum(SeamScarfType::None)); + obj->config.set_key_value("overhang_reverse", new ConfigOptionBool(false)); + obj->config.set_key_value("precise_z_height", new ConfigOptionBool(false)); + obj->ensure_on_bed(); + obj_idxs.emplace_back(obj_idx); + } + + // Space the blocks along the belt with strictly increasing layer ranges: + // each block must start past the previous block's highest slicing plane, + // which trails its far edge by height / tan(angle). Anchor the row near + // the gantry so the whole test stays in the plate area. + const double cot_a = 1. / std::max(0.1, std::tan(belt_angle_rad)); + double cursor = 20.; + for (size_t idx : obj_idxs) { + ModelObject* obj = model().objects[idx]; + ModelInstance* inst = obj->instances.front(); + const BoundingBoxf3 bb = obj->instance_bounding_box(0); + inst->set_offset(Y, inst->get_offset(Y) + (cursor - bb.min.y())); + obj->invalidate_bounding_box(); + cursor += bb.size().y() + bb.size().z() * cot_a + 5.; + } + + printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); + filament_config->set_key_value("nozzle_temperature_initial_layer", new ConfigOptionInts(1, (int)start_temp)); + filament_config->set_key_value("nozzle_temperature", new ConfigOptionInts(1, (int)start_temp)); + print_config->set_key_value("enable_wrapping_detection", new ConfigOptionBool(false)); + print_config->set_key_value("initial_layer_print_height", new ConfigOptionFloat(nozzle_diameter / 2)); + print_config->set_key_value("skirt_loops", new ConfigOptionInt(0)); + + changed_objects(obj_idxs); + wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty(); + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_dirty(); + wxGetApp().get_tab(Preset::TYPE_PRINT)->reload_config(); + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->reload_config(); + + p->background_process.fff_print()->set_calib_params(params); +} + void Plater::calib_max_vol_speed(const Calib_Params& params) { const auto calib_vol_speed_name = wxString::Format(L"Max volumetric speed test"); @@ -13256,6 +13545,8 @@ void Plater::calib_max_vol_speed(const Calib_Params& params) cut_horizontal(0, 0, height, ModelObjectCutAttribute::KeepLower); } + _calib_apply_belt_mode(); + auto new_params = params; auto mm3_per_mm = Flow(line_width, layer_height, nozzle_diameter).mm3_per_mm() * filament_config->option("filament_flow_ratio")->get_at(0); new_params.end = params.end / mm3_per_mm; @@ -13320,6 +13611,7 @@ void Plater::calib_retraction(const Calib_Params& params) cut_horizontal(0, 0, height, ModelObjectCutAttribute::KeepLower); } + _calib_apply_belt_mode(); p->background_process.fff_print()->set_calib_params(params); } @@ -13365,6 +13657,7 @@ void Plater::calib_VFA(const Calib_Params& params) cut_horizontal(0, 0, height, ModelObjectCutAttribute::KeepLower); } + _calib_apply_belt_mode(); p->background_process.fff_print()->set_calib_params(params); } @@ -13377,6 +13670,8 @@ void Plater::calib_input_shaping_freq(const Calib_Params& params) return; add_model(false, Slic3r::resources_dir() + (params.test_model < 1 ? "/calib/input_shaping/ringing_tower.drc" : "/calib/input_shaping/fast_tower_test.drc")); + if (params.test_model < 1) + belt_calib_flip_ringing_tower(model()); auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; @@ -13428,6 +13723,7 @@ void Plater::calib_input_shaping_freq(const Calib_Params& params) wxGetApp().get_tab(Preset::TYPE_PRINT)->update_ui_from_settings(); wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_ui_from_settings(); + _calib_apply_belt_mode(); p->background_process.fff_print()->set_calib_params(params); } @@ -13440,6 +13736,8 @@ void Plater::calib_input_shaping_damp(const Calib_Params& params) return; add_model(false, Slic3r::resources_dir() + (params.test_model < 1 ? "/calib/input_shaping/ringing_tower.drc" : "/calib/input_shaping/fast_tower_test.drc")); + if (params.test_model < 1) + belt_calib_flip_ringing_tower(model()); auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; @@ -13490,6 +13788,7 @@ void Plater::calib_input_shaping_damp(const Calib_Params& params) wxGetApp().get_tab(Preset::TYPE_PRINT)->update_ui_from_settings(); wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_ui_from_settings(); + _calib_apply_belt_mode(); p->background_process.fff_print()->set_calib_params(params); } @@ -13505,6 +13804,8 @@ void Plater::Calib_Cornering(const Calib_Params& params) ? "/calib/input_shaping/ringing_tower.drc" : (params.test_model == 1 ? "/calib/input_shaping/fast_tower_test.drc" : "/calib/cornering/SCV-V2.drc"); add_model(false, Slic3r::resources_dir() + cornering_model_path); + if (params.test_model == 0) + belt_calib_flip_ringing_tower(model()); auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; @@ -13555,6 +13856,7 @@ void Plater::Calib_Cornering(const Calib_Params& params) wxGetApp().get_tab(Preset::TYPE_PRINT)->update_ui_from_settings(); wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_ui_from_settings(); + _calib_apply_belt_mode(); p->background_process.fff_print()->set_calib_params(params); } diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index f2089c1c4f..a718d39464 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -951,6 +951,8 @@ private: void _calib_pa_pattern_gen_gcode(); void _calib_pa_tower(const Calib_Params& params); void _calib_pa_select_added_objects(); + void _calib_apply_belt_mode(); + void _calib_temp_belt_sectioned(const Calib_Params& params, double belt_angle_rad); void cut_horizontal(size_t obj_idx, size_t instance_idx, double z, ModelObjectCutAttributes attributes); diff --git a/src/slic3r/GUI/calib_dlg.cpp b/src/slic3r/GUI/calib_dlg.cpp index 5a06b24427..870b078aa1 100644 --- a/src/slic3r/GUI/calib_dlg.cpp +++ b/src/slic3r/GUI/calib_dlg.cpp @@ -78,6 +78,19 @@ std::vector make_shaper_type_labels() return labels; } +// ORCA-Belt: PA Line / PA Pattern have belt plumbing in place (drawn on the +// belt surface via BeltGCodeWriter world-coordinates mode) but are not +// validated yet — belt printers are restricted to the PA Tower for now. +bool is_belt_printer_selected() +{ + if (auto* preset_bundle = wxGetApp().preset_bundle) { + const auto& cfg = preset_bundle->printers.get_edited_preset().config; + const auto* opt = cfg.option("belt_printer"); + return opt != nullptr && opt->value; + } + return false; +} + } PA_Calibration_Dlg::PA_Calibration_Dlg(wxWindow* parent, wxWindowID id, Plater* plater) @@ -288,6 +301,14 @@ void PA_Calibration_Dlg::on_start(wxCommandEvent& event) { m_params.mode = CalibMode::Calib_PA_Tower; } + // ORCA-Belt: backstop in case the selection slipped past the UI guards. + if (is_belt_printer_selected() && m_params.mode != CalibMode::Calib_PA_Tower) { + MessageDialog msg_dlg(nullptr, _L("PA Line and PA Pattern tests are not enabled yet on belt printers.\nPlease use the PA Tower method instead."), + wxEmptyString, wxICON_WARNING | wxOK); + msg_dlg.ShowModal(); + return; + } + m_params.print_numbers = m_cbPrintNum->GetValue(); ParseStringValues(m_tiBMAccels->GetTextCtrl()->GetValue().ToStdString(), m_params.accelerations); ParseStringValues(m_tiBMSpeeds->GetTextCtrl()->GetValue().ToStdString(), m_params.speeds); @@ -314,6 +335,9 @@ void PA_Calibration_Dlg::on_extruder_type_changed(wxCommandEvent& event) { event.Skip(); } void PA_Calibration_Dlg::on_method_changed(wxCommandEvent& event) { + // ORCA-Belt: only the PA Tower method is enabled on belt printers so far. + if (is_belt_printer_selected() && m_rbMethod->GetSelection() != 0) + m_rbMethod->SetSelection(0, true); PA_Calibration_Dlg::reset_params(); event.Skip(); } @@ -324,6 +348,17 @@ void PA_Calibration_Dlg::on_dpi_changed(const wxRect& suggested_rect) { } void PA_Calibration_Dlg::on_show(wxShowEvent& event) { + // ORCA-Belt: the dialog is cached across printer switches, so refresh the + // belt restriction on every show. + if (is_belt_printer_selected()) { + m_rbMethod->SetSelection(0); + const wxString tip = _L("Not enabled yet on belt printers — use the PA Tower method instead."); + m_rbMethod->SetRadioTooltip(1, tip); + m_rbMethod->SetRadioTooltip(2, tip); + } else { + m_rbMethod->SetRadioTooltip(1, wxEmptyString); + m_rbMethod->SetRadioTooltip(2, wxEmptyString); + } PA_Calibration_Dlg::reset_params(); } From 85fd613cf724e3c3da10bf573486fe05fd66e85f Mon Sep 17 00:00:00 2001 From: Tommaso Bianchi <163908227+tommasobbianchi@users.noreply.github.com> Date: Fri, 12 Jun 2026 10:17:23 +0200 Subject: [PATCH 048/224] feat(belt/calib): add Overhang temperature-tower model (selectable) (#48) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Belt printers can't slice a tall vertical temperature tower. This adds a belt-specific temperature-tower model — a row of discrete, individually engraved provini laid along the belt, each printed at one temperature via custom per-layer M104. Each provino is an inverted-L overhang that stresses print quality, so the operator reads the best temperature off overhang quality rather than a continuous ramp. It is offered as a "Test model" choice in the temperature calibration dialog (mirroring the Cornering test's selector), so users keep Joe's counter-rotated sectioned tower as "Standard" and can pick this one as "Overhang": - Calib_Params::test_model (existing field) carries the choice. - Temp_Calibration_Dlg gets a Standard/Overhang radio. - Plater::calib_temp belt branch: test_model 0 -> _calib_temp_belt_sectioned (unchanged Standard path), 1 -> the discrete-provini Overhang path. Assets: belt_temp_provino_unit.stl + belt_temp_tower__.stl (6 ranges) + gen_belt_temp_tower.py (manifold engraving). Based on belt/generic-calibrations. The Overhang path is HW-validated on the IdeaFormer IR3 V2 (discrete M104 + engraved numbers); not re-validated since the rebase. --- .../belt_temp_provino_unit.stl | Bin 0 -> 7684 bytes .../belt_temp_tower_230_190.stl | Bin 0 -> 286084 bytes .../belt_temp_tower_240_210.stl | Bin 0 -> 223584 bytes .../belt_temp_tower_250_230.stl | Bin 0 -> 163784 bytes .../belt_temp_tower_270_230.stl | Bin 0 -> 296684 bytes .../belt_temp_tower_280_240.stl | Bin 0 -> 287684 bytes .../belt_temp_tower_320_280.stl | Bin 0 -> 327884 bytes .../temperature_tower/gen_belt_temp_tower.py | 79 ++++++++++++++ src/slic3r/GUI/Plater.cpp | 96 ++++++++++++++++-- src/slic3r/GUI/calib_dlg.cpp | 10 ++ src/slic3r/GUI/calib_dlg.hpp | 1 + 11 files changed, 179 insertions(+), 7 deletions(-) create mode 100644 resources/calib/temperature_tower/belt_temp_provino_unit.stl create mode 100644 resources/calib/temperature_tower/belt_temp_tower_230_190.stl create mode 100644 resources/calib/temperature_tower/belt_temp_tower_240_210.stl create mode 100644 resources/calib/temperature_tower/belt_temp_tower_250_230.stl create mode 100644 resources/calib/temperature_tower/belt_temp_tower_270_230.stl create mode 100644 resources/calib/temperature_tower/belt_temp_tower_280_240.stl create mode 100644 resources/calib/temperature_tower/belt_temp_tower_320_280.stl create mode 100644 resources/calib/temperature_tower/gen_belt_temp_tower.py diff --git a/resources/calib/temperature_tower/belt_temp_provino_unit.stl b/resources/calib/temperature_tower/belt_temp_provino_unit.stl new file mode 100644 index 0000000000000000000000000000000000000000..556cf313d241412d00faf8f961adfa11519906c0 GIT binary patch literal 7684 zcmb`MZ-`Yz7{=dVHO;jGH6oP8=&t>G1Zrso;eX*P6P*pRYd|l&mne`O}k8e7xl*52Er>VI;<4v~j_C#fhJ!vKV|jP(kruuI2vY ziJlwlTolR0lvIS4$BywI(O0PU_xLLBf%O`T>d=l*rE3E?k#XGv`Wi^0`?d zDDlO$qdn`Zu4nJG{v!a_yZBws5>)W`Bk#-QJ+NLAd^2x3=IkLA!Y{t&#`|(Q<_peF zx9gXG9mXBiTy|Y0W;-gHzj)BbCwBBx`*xJ~z}St&n&g=C9jQP+qEoI7LkA>JE$y3R zJ8P5qi)t>X?TCGJ{=z$`UN&}uO`dsAmG{8dT~C9z-VUx;Du_Eu&%w~jb!y<1yaWT@%%WOtVFJ_!iwO%{F=dig%qkC7Hb|@FP4dh)Q@TZ zwkrzri|A*kt(+J?+b3>JgDLlRh7}IWM4CcV6CGapbMfuLi-HH&g&xMP zwGY_!-8)0?%4d*QWTa5-8XIdKSTB}|woltz+|%_|h>Gt>k0;LFZ|jP?lBhfxryt^| zh`3EeTvIw8Wfe%Fhg(aOsF1>PXfmHR@fjqCMao@Em*_bnBEK*q3J&;;Dx{997=DZe3o2AMJzDJ<(ANX?q_PgGj3=vedG{Pc7!Vaiyfu&jF^_4o3t zQMR@dJ+^6cK3fx+k{+ngJ&nS4j1)_&rMU-Kl3T z=3FjX$Z}DpjAe=l%X&MI>RNI-O7;fl|Gl1bId!2g&8eA^9;~R;4y5>e=A%OcYPL-r zQy6$E*|9K%r#LPTDxK(5@<59B=hc&j*5FAIQ__PKeh{>%v zUVcBwV~$a|-%I0BNx`wBx{`wHm7}Rgh3(+^R%s?Ob*mnz;OA!0Pp8EbRh}}If3MVX zu2-%rMB2)rtrB`)p@P>jKhoL?$CQjp_do@&$KJ#3Sjw){OvzPS_do@|SLQ9;kkj5c zOv$&8?tuz^Kh9ki=kU#0o-&qyuQcXdugo7v%>cA)KyL>sIA3}5{Rz1vN+!dUj7s-F z1?OGw46M#!MpmA3=hu+K=^k9K%u7klEVV3C_do^b!G7*)88TBcD%}GWoX-aj$n?uo z#`5p!yz)YK$(5SxHJ0}!p3`kCar0pQJ z-K6e;3f}Mfc4#|cresvQ2P$~q?me^}JyUXbuX~_^&mX*pb~3?~Jf+Y*P{HRY-a|WK zVM?C1=pLxRQzIfg9mKOYJ{@$=N9jDV|MS78oGuqBo%P+Sg6oy1k+L1=fl={!>GETz xx8O-?dFoa@xL$dl?D9nAL|8o?_E}$g$^QbNg3qr#s-0{zC8N^&3Ke`>{~wWbJ;ne4 literal 0 HcmV?d00001 diff --git a/resources/calib/temperature_tower/belt_temp_tower_230_190.stl b/resources/calib/temperature_tower/belt_temp_tower_230_190.stl new file mode 100644 index 0000000000000000000000000000000000000000..0496fd1522daeb8370694134502bcf14e68f567d GIT binary patch literal 286084 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 literal 0 HcmV?d00001 diff --git a/resources/calib/temperature_tower/belt_temp_tower_240_210.stl b/resources/calib/temperature_tower/belt_temp_tower_240_210.stl new file mode 100644 index 0000000000000000000000000000000000000000..31801c17447296d9a50d20dc9367bda74b2dd72d GIT binary patch literal 223584 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 F_zxDBl4}3} literal 0 HcmV?d00001 diff --git a/resources/calib/temperature_tower/belt_temp_tower_270_230.stl b/resources/calib/temperature_tower/belt_temp_tower_270_230.stl new file mode 100644 index 0000000000000000000000000000000000000000..7a0bfbd1da917567c95556c81c668f837e0ef3f7 GIT binary patch literal 296684 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 z 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 owrF+EZwQjJRv|cB|PxS@z@A=7ih1 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

jZ>>6U~ik_Hc!FKwP;3EI;Myn@7kOBJRBY25~< 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 z zlTaQW={;v)u>0A!lKm_=_d(x%`YgOo<00m!B^o8L;0%ewsziCVWc$*~tgO@w3Cm50 zlWV)VpSn0Kz=E?P_F5R$TS-;Bg2?zXW0Hh?(;{s z_*w7@dtaRU%3L|I*c6HOgc#T?$-S*-ZWjytWxVwBe0gH;G>JYM(RZT|oykVFqg(vt zNibg2(rpYN8+$1JOC(63J*0(La=d|iO|8Q&7Sv)tkSPxq%9Yn=Xep#rtt{&5L2J7# zM`5iP*L3-J+wVV7yp2dQXjI#r`bYS6(7pl$s+kCi3oA`eq=#esPlpwa^15o;RY~z;#i~2cK#3 zW6Nc7*IOAUFWOHmXdShXCYSlN_PUqWU5`4}UXj3cSRsZIafq~8P>aLr3>KZhI|@^U zrGPO}4N_U9Q8}^T6)YdLM_vjZ5nQ@RpbeyjI8$qX><=_=FO)sjjdv~fkL*+SlxMa) z7{^a{Gk-r2yMu@l4g$6CP7`9)FTKsL>Na+52Y(n#mx+{#T!;lfUzRGDYgC8hUWB`IR{?;UEbT99Hq_-dmYCL)t7zpcdMr zH;S*UPoE&u`eZ=f$n(%EJ$!z`3^=} zw}IAC3tNnC1GStsl-FA!DtD}B);{!0fCUK-D=pmy5_rW)s1vA#XhfrPrv}OXLXOdy z*JpMVnk&jWc5cXr05#Z!M?RF5MC}44-=cfpPK5 zX3YjmWHm`64%7{Oz0=g=4m`XRV8JU$aM<>xcaexYq`f@^p|miCdVOH3_<1>J89Kl8 zd1@t)Li!pvV_nom>!L1=fD&a|`4rBC04=mf^Q%QyTn|$tO%}9{rzq(;=bWguK8%Z3 zyk>v1On$m?o+BTV$i^*-U-)zzNT5CaSq-1CIIL#H`m@?wfx%`qy2n}YiG{;zPR&m* zb}M<2Rwj-c<=rfp4=hpL+w=k=wvzTK2Z36=GSQ*PXY###v+0g{K*Tqs&4O2)gz|1J z#Ot({@p;qoZWdmFP%Q)fMf2}=ou8xilC7k@gamCk2^EtNJ&5p=HVYCQRw+T+(RQJ| zB8|1F6R5>u^=?}y);ww@+*0rghn1g9y_I}j??}R=95E&f5_mcn(n366|4716(w^lY zFjYtk@mqr<31di`1+_S=zS-6Zygyh=wFX{WRQCtQ6uzvm{*e~q_a~3|i{*?lSuig2 zFN!Io*=wF7{wGLVItYvxX(9UNI^wTL+AOHWVO1YG@%Dz^?yU!!xLA0lNR2EURwDr3 z!9pN`_c_u!fu)NC-`i?DN1FWN{?;m1jcTaHYcgtd)`>@}gDzSfbYUIfeTR7pt`1rR z#)Y&_U_OxGwLUdQAx%43w;6G2R6_#ufi%sp8hK1_uFHY?B&fwx4a!+hiuGu*f_lT zO_}&nicxG`Ux_sB8U3SZ+LvS_)?wr7o`$kj!xZC#?0qHDG}ab1eB^P+WKUC=s+Jp? z$|}`TjF={g5^365-08awwBy&Loxd8fJ-LO<7oTFBtww&;fTot{8ynx9^3m5p-2R}I zJX$%$;5TJRb2@_c;l)&Ocwgz=!4s(Mx@pqm_pj9aN+Yy75MsfMn;+t4PDn$3HSpJbC0Y$upNb-&gHq zYNJ%6NTDI}^p;Mtf9q6(y=EOP^^yE>XSy+}8GAPB((AnMDiQ2!d*ae=a`co`~g((rtWL`&+9Q<+u7spbeyj z*tBz2$W%of_(WcMFWoq?e!P~#jJlu`z$~wIm~*kIjU0P0-FScer!sk9lVDhg5+C=E zKS>06&C<$BOFPg(8*cK9XY#-7a>sTRY1nP-$pu$@he@0E{b;4zsL?0#V0x;--iAhQ z=qtbeAWZ}F4iE%i{&On_4Laz zyc3C1=Eiu}z1?4qc{a^p?;S`}3#}dH%SXg9B5HS?B8PQPHHJSNB9k@_4-$EA50jnS z`ixWG45xgEhq-<6l3@Jz7lDjNV`5ZRrv!zt>_MuW#^~27^g?g(d==) zGwvw%AdsA6fxI((i&5ZhIxz`S$iJ)9vVQoU0q(;Cn_T0jkjd`PN@caMD(_8EI(+LY7D9|goxu^Ei#bUIOz(>!KsFeThA_szHnwX*xflmh`E6kmK6%^G2i2n#mH&N4LRgSKp+H zx^08Rx=Kyt3oEx6YfjIz`YRO^orX~OjkI!9R-37;(8l+>Ys<4AZ85$(HdnL97VMq@ z2~obj^wGWYbi;WPY4S}~wp~1})S9$ntJ#=Vp`mOzb&Jt=BCX>CEyUEp8?Bz2zpX_Z zm=BE>Ihwy2cbsfQaD70|i?41WPy4qR?41B<+BrEpcRcsGhK8ZF$-TKm*jFPQd-7kkQ6A8avv-_LdAyswe7a;VmR`t6~#hh>WqdvUJBKA6tC z9+@NUO=^jA9CvMUzGX6XrQgVVc9C5AtSR69H{F=xa(+ja{ObVuu)s#6((H+{{a^j% z$E!CQ=YF3gkM8X$i+r%rV1HrsrRmyHI*=+D0Qu0Cf zS<5&YHO=fqpjOm?J~G$ZO-A!u(;P&HAG62PlZ55N6m4L<*p7u5G2BdJuXSt#<2`yJ zQ8ur;+29?lNYi{FOWj!Y{DC!vC7L{Bn7sM%W`lQxl3(Y~yfNHgJwZ0`oHp#D(Hsatt!C`f;aGmS(5_|puGY--!kbjTiJ;*(rlfWx#ykf1gdA>J#yu3IB zfme{|o^P+QzTwgk1YSX+Zs8w{hMpB62)u&CvlaFkZN#b&1YSYn#W(jG4M(jBLEsf6 zcJw%4cn_@$LEsf6YR)@oOgg_l1c6tOxW4z0Q8Dl(5q}rl?<-1ecaNjaaR$KQh={pQy&k`ui0af5xo#DKTHBk}m(g?l zM%GnK^le@wErwF{r6X0SRc!Rzvh5nb;s29~y6%0xVnpn65U90uRTG(Qliy%(G4vH{ zYfl_Mb6Vp$qzbiqj($%T-Rn0#@597}UNIX&jX0>4e7~*if5>m}dM|x7`A;B@W|fc6 zMM0~xr?cF!*>Bvdz(oJI{`FO&eE1ysK&|9{J><`y`;EmHIp@wh%C1|xq**I__QOW? z<|!cvyrSxciJjGt7+Za*Aqc#JL>KQ-B?BzMJBq{m)|?L3!G9xePdXp$Ki+J8?orS1s9kv}N*2vVzbzMneH7AGK3JPY zBifz1YfS35+Nw3Abpo#-QTX)5Fuf>VL89!#OU7e8H782^`OKAIua8-Dj>*wx_3 zf^YC$^;EEv9Aqg+rKmxTY@4aE{ufVg{ z%<8=F)ic4~0Na>egVPu)#NndEW@5ZL>hLOweX}^ zykgHEoCKZ}YirROL~v#iY1m=Q2AmX&SM2%2V+5QOYirTdesE?HY1m=5fhWb{6?^{R zB=DqITT2Lg{t#*4@A^rxww4g~{DDrNf_aI4Qmm~-&nCf{MWkW>-nl^S=ujgkj%4}v zC(5B8_>F`;Jj?jJU}yrh)=f^556k(DO1BmevAobTv7u(KsD<-jA^NmfsI8u>wu`f3 z4wsGd8$UhIsY*zR2|4#ct%Fl%$@zW##@gaMI!C33CQz&Pt@+xVT3>HhsX{G$V&yiX zpTld{0gn2S29ERkIlOqq9^0J+p2KTv(OP_P)Q>dqcl{h*TZ_IP3Xb}bh8^a7;5oc_ z#U9(81fIidYvI@)Y2ferIlRspfI44~OI^(XEFyRgFJ7_7@JIuHw@wH4k8Lo->aT1q z@}Cl%Cqx={_)*S-=kVecdkl9Hq3$SrKCAr~?cjN|uE7@cQ+09Nw#VH_1AiCdyUOPS zBUZJu@?mRH=>|vSNCSV@PrS89%Sgix)6*S{&cchQ>e?fxP9SadiNTY5?UZ1=k%neN zJ5|>%QJgJB8u+{L;;Fj!=tXZyV19*R{Zw6>5W*hGA`SfAx@!aN^Y^k!*Vdxdm*6Zc z(!k&8XaG-oz;~>hMc6e(CuRo;q^Ziicn+^UPlz;dmVsg6#dCP=IRl-s4RaI>>*w&= z1btNyoLNK~_`7u~yLo?q9V;Jpozwo4;0!6!z~A+Acw&V)%@iZ8=s<}yeS6-vNGg?;Nh@m_k@Vp(dFYo^gU`;m4K%H{S6lC^ zOi!t)4uaR-)EhJXE!o^%wc_@X4gM}1Q-xY+FQX=Cjp5N7cRS}tSF8`(Kzp=9`qJ1` z_3jRBFrnV1AuYs*C!2nyR@IQGeSC}jd{&CFxmJIPv=I4*-;E{zj3)VM)bbJ4XRCan zON!CrH+okHH09&YtyDUl!u-uqR(t>0F6+ISV(dAaD3K<=YOjAEUxsX$4jU)h?UXOP zkz%~FBvB$wKDJW#_)3wDuN^jOzx%C>shVOu`DUU-I-_)H6(__ydVDXBEuR@4armtz zmI=;9IIK#V&Rt5KpQ7GoAc5t>;hWUPa%>n%v!!=y+)PnxOHM**;c6u99$4{R9JO@I zgFwQwTjl+ksYcXduYBq6ZF2CyRHI_FS1xY6mCl0LXnZk?*Yuam*Mh6(sFnQDc6q8p zs`2qQdOsY$T_$@r8ol4*r=kyw{T8dvAB;HpjeMQn12#H7Skpq9{K@>-BuiOum`bfO8fm)b$oyd9S9qW4r?%^;WwI6Pi7os;Bho4Gz;`Yr>Bs;v&nAC{yV-S>NC+nonwt`ZxKrr&Y9O3YK@EA>_e;~I5gmps*D zqmhzrlGIamc}}Y|l`14KRY(hQ@MyL;zCZZR#Z+;4bwhd|HJs`L@;d?~c9& z(8iA%jen3c>ZBNv`{>IvpoRE;+?Z5)OKo{Vz*Ob0bVy$AkYa4x)>k4;o^-A>udH&8 zMC}Pb$&xu!jmet_N~DDtf7=LpBSE6k&=YcI4xcgoy`xIc%2?-oYna zSE56uiU~C%LwiD8PCRUl21IEi4)%jM`Uv5v@@<;>LMYJZ8+oGjCS&c^sj~L8o${C2 zTa4u^=_?xwUvIq0xHD-g)%ndLBhu&%fJtx4+(;ZP_O0C4ag&jFR;Ou&{@1hC`&v3( z#ohk%U2D z^Z7k8*RV}S(>gkVG0|=g?})UI$wm^{(1;y-WS!xgjFt^_n%;BHE*!`0if4VbLF=Sv zzbxBslX3KbGv_S}W=(5LHbxSGTFFZe$$f8bGWzs(65LLxoGkB2nif5oVtuHv5bN`O zo1#*c{@hX7<&TZVrH7Mg4~|V>OuW;(dJNAKxs_mB5{FI(`x3of4SZs)))H0gyFK=V zyu32qIDMAR@B*679`JiCBIBKx-CmEb`$>NA`9@>g`AJfv#f>M9Sofflz&s%>#Hm_U z<9MD(?}fE|V9t@|vJHO^+H0hCYMBVPl8S4gc-Jz4E2;LnC_P~Z-_at?MCg@NTnk0o zvH>fp_PS`Kfni)>rBsDnNyW8Lq}c|pq}uDBxE6{u6S$IUuYclNDAG)PGjN})d+yFA zhwb%GTnj~-wdjf7rJm^RmAgm-!*)zel$*22rB)44%U;Dpnu$FV#<*&3O0-gCuh8LY zU1n>2_R3wPfnhr)wvn8_g6n>@ftC%_vRCntW&+px>=im(t;=k!&tAEUG%#$(#5U^3 zefN5`s~PKXsKxiZTI;iae?$b=`s|gvNCU$*%>=DLhFI&v)jFh^pqvNS`s`IFT!BKG ziBM~O`ud*pJ`cB26mHc>Umdj9^l*JIv(-U+9ggZF?CK!hgCSP#aJ3F;&Uv`iL3?#b zUn$gAAnjE^)Ixh~qhNswf!mMTuDzy*YkgQZOoUqH+!I||2LwxEx-IKJ9%7wwoH zCr}GVPCJE88>nTEeVN%nEgU(++dwV*?P6v&Pzy)V@HS8jN73*EYT097W~o9g923La zKrI|O!xN}wkA0b?3bk-d3~vLqaO4b6pq4%MWtJ+`vd6y62&a}BIUh@vmv0ol*KoA7 z-)4od5hOS+6))1^ZD71OiathAqWBN3V}Gvq)cSpH$EEkX_9*JSYmu zdzrwI3{z!~B~HQ~i!fe{$(br7LXSRR!^#KNInE#K*+6E5t)-9W!P0%q2F@SsnL*a* zS+2W>%eiYDDd*-rI5T$By1KuIwj)#DYdJk;mg^=Ff0FQ?%1bj+jRnQKTZE!@f|3$k zxkG|?@YLPnGnUuwr_toEZ&zGga3>O^%1JB{X-1E(1Dxw{K;XV2eU~YeuGg9KV7#y^ ziT3JW|26Q}yb|VX^GleR65NTzF$HOjz$;Ed?R*kqd+aRNYAV?~j?%>v)pxSqy4}t- ztb0ZGeiAHQ)Y5mdUU=BfRgJWJlkkpJ@d>Ta-tDUGchz^Yp19Y}HJY^BI|#I=+vqxe zPAu0T?TpuK@V={D<9)`=t+bOBTaUO{=R_*MSL2;8cm)Y8b=n(#?nzg+s?lyY3FZUS z&iiYBevjVo)apvD}#EM8r7=)Z%x-{hFj2h4XMeb~Xvb@u_dLj;z_> zU0aPl^cfe1^^tn2uBCMIXYs|DZAkD661cC7)^}GQ^Qm=FEM26rZpd%#y>C)~q#ERX zELc96cC<%d9n`Ot5KpbB#w$G&rJkypCw@yvI2Uckkl+<0^gVGk&))IJQHtD-RAEZ^jr;Twsm8+z z-KYjP{8&9s`3lqW0a|E}o>0%+@fV`_JF*SXa@tVe1d#8Y9~Q;&PS&5Ol_0@;ZF~Rh zGw#0D-`el2N`bzl_+WmZtSo2tCc%3RcTQ@3mss`D{D4`#ocRP1*cv#jc7P%+M8C)? zfx%B#Hoqr90(VX#OMS!{eiSqR9;oO&ho+14TiPSMS^!xsuI;px8@UB zT-~NdyJwJS1l5O%SKGHQUdWflHJ-GaI|$Tbp98~>Y&7C0j-lS?<)bfKJ72z|I!7Ct z7Nn5;JUpGnRh8l&LxNgP8_FvJ?Z+v2!BuU{i|(N$NN`wbp;rVtNtuW$q@6^N_nCl3GkXB@vDq@6-;)Z(3ih2Gn2OjtxG31~Jx=zJ#7`MnbE4J6o8 zb6EA~`uA5WI-LouBW)HWP>a96x}4u^5LTtc{x>J-I*%&O&R%(dUOxop?eVH;;`}>q2Gf8HrI4F zj6WMlB|*ZODn-+qI3nDneKYQC0JYE`1lpsPSUSnQ>Ww^sBoZXpYeZCRx^XbqFo|>q zv4XTak)Rg(nb3*I<8{m%D^I#skYHJHSe1|db=!?B=dJJIp5vJelrC%adEhhdHy&W6 zO4W@Jx2G(P<26y90bo9mz|_)SWJ*;tO3`LVsxT!;lRuxzN$%P+o^VBwU|j4~quup% zWAm*M5=+7A5`~LW#hwt5z<7DjuqtUG7WXNbI*VH3Taw1EWLLz=!p zr9Ls5`ojT^J`wvtOkqaORiB79cz<<1-$vuZJYyu<(+Rx7JIfcvZ!%UtGtSzvu6k73 zd2(+<;QLKc=5w2)Ozh!sFFOBj?{WX;MStLBIyt*H2@26}eQ(ecB>3UUXeI0kfxp;7g8dI5p~4vif9pd}+!}!? z;jdd&YAwP`JFv|nWQVm>T<8Vp@l=H)LeXpkobuCeNw6Yq1pw{aen zk&iUWM+M4vlg_YHek1lvwt)m*aT3a(5Ut|;k}aN|QY}w~Sc6y! zSWiM^-L^HAJqV;y4}@CmJK@L80b|fh^Q<g{~T1PExF+z-^c6F1qmyw_rhn3$4omlXUbp5fjlDU`!3H0cIGNu8)rC?g*xDNT6p5r0L|W-~Ub+LfR`r z5Gqwj)7y-HkGM)x@7C1O!{G>ky`2!RCN}f8*b(E7AiCo*h&?y$kM(n5PebU1%0 zVJc~#Awlb?g`TZw_dwpCTr2*3$$g!KJ^u_H;WsMQoT+)R(meuYMyTBCHa5)}>f@bZ z@s4(drGRZrh_*33ed~y*LIi5D&ypgy0>;c-^QBI7I(H{-3lT3n2rLDp>7t(OZMp}`)BV?%1n)ZzD{of%J&4b0IB#=4t0`}0^nKTMT16cf;f^04 z;YNbPN=x@H){%(5q+QKHpcaRVR^M#QkKw&CWtz7(yI))HiX_1+NT8Q1@;dZxP4}7e zX9Kk^oDE>k(R&t_LgvJ(A2u6@x=oTu)7+<5=Q!G>XVO>LTFzNZ5onu;+)3_F&gKnl zAVDqkcUbgC720i#uedjs6rm*;c6LBdy*7W)SqlGks%+8up0^L1C(yB?4@Z78pGNLx=6!4+YQ3$@sr+4N7h7~amjo1@4RWn!yRs@77f zkU%XgH~MaH$jrD+lyh1~cHrd68q=%8wh3 z1|@2`Q{*|$ezk)QK;`$=BcG9kGoO7WTju|?!=a1h~KcjrS;m8MjzgW~DV65sk zM`BF$EzQl}0-Fn#Fi(-77W&ITZyU6aJ;#zbby5@N1Iq_v5~9uM0p>U7@&_)Epca>| z@>IvaD}RkNv&>r4%-Z){AdUp%Li)cGDqhSJz0DXl((*e<{kt|gqdn}8Y4_QmzXdi@ zIhA~_gw|7I`CzY2dta$^w>~Oej0i)o1sS|j`NvM*h z@6M?{c2QkKoX;OX!dV}7Yfpc-PN43G@3fcvZrWTndtpds#+IC8@zroo1QI(W{;oof=`0UK_;;M?WfomtUJCn+)29;B9M_FO{U<&DsTq3HF_9zm(;v|%ZXj+Gh zX&=lv66_tO>F$6r`u%y1oEK_a*R_UnUfhv$B(N07pFRRNVIy3sRL(Jlx(&=BhcV}NKKu!%gL{#A zk9)mMsmA(hJtXg5U$D_>Z!_%P(C1Z{|puOAF{rOqk7kDu-Aa0V|_;Jlnfsb%AOD{w@vf$Tr?}? z1JjO#o{yuE_Y>+Yjdsr>!F*s!bRQ70QTG$Pq+N-GcPSrkk!oahchx*a=)0FA%iT|C zN7@Mv8yGLfMBa_ZZS`%We7s8ez*M0Ydlop^#b?ym$KTvtzSTHR?Yu_0$WVxglexylMhqhvA`Ws(yX4Pq{&y57W!(?iG~H{1s=37XV&RZ&cyU` zSoxViFAOx(e|BC#l6D6YtXtGVd*oG~Qgx3~6&WH`>K@dIe;(Or(kmg@{M-*i<7Wg}-|} zy{dQIdCEC^zd#$Ph4yHL{>vu=FZiom`0%g3ieF# zO+f063@{SbAo>RIm--x`qL^;D5)vo#5}3{E@xuTiuRJy$<;&CmUUd4L3S zh+bTfCT~_`qbSASG=vS+N_67OaUb~01Y+E0{4v^nj(%!5wNqzoGWt#A-e=>z{H~Pe zqTSgFMZ56|hn4Rgwo(5R`dZ`x_w7jF6(^xQ-Uv~w>A$Yi=Zm_3B;lGGkbRSpzwTJ8 zo>jcMcbN0<{Oh_y+6fnmTAp;27TZ(t>O{X)4_)`DWRE#Y7h49FG|gmIK6Dj|c)@(1 z1hqJMaf~}pys)e(MjjCnSjH0WOs0Npnd^wi=Uy&y$ zHxj6Yw#lR1Z_}Ksn_70M5&nBZW6I^g+iDL=Q8R>+)6&HNU`yOhs$D{{~Hye># zlBG^$Te>B-1kD+C(mkjVFfOr=q{~Y;8%Mt78FFCTfH<|6496(EqjchWq1fQLGI$SS z8$(*3>0{2FGyMaP{OZvue7mN&{`&Y;a|H>`&(X7$?*HodV&}fX)u`agCak16*Or3* z+XY_Xuv#7Bum$?IA$#7PgLlTRW`@RuB!*--H^tr5X>A7|;RTI1hUNt2hfF(3HL)9m%6WA@5-m%58^ zCQIHBdwk%(M%oEPVb6eC{JWaT(z~Kvg#%5mS2mZE;Qh>DX#EHkoIv0fm(X1$_;us;ZM^3j0EH5u=2i#G=10iUE#oI)Iu7QAb~9d zX(2xPws0Vkw5K}=tbe5Wjv}u^bpO#;#csTF@J?kfH+V-0q5R@F?^>nBgu1&d6TzNZ zBS=f#cltf}C{@2>Bq^%6`#e}x2vm6kIfI)N!cLgmRyRj{nI z(skHVG3lw|e|_1#fF~m!I=l1kiyn2}c8~659{oPy_DXJWzUrTyg(|OxSljptlZGFEG!DrDS zvC!2>*7>ERr^m)biL?;6&JPNis-6|zmA8H@>4~Egx;0v~dG5I{)fZc6BjR*R`Nn~g z9zJgyY1-$td4BwGDyt72WmTa?J2{O~MfaOTn$E3QGAEWiO+E6IZr4Y|+uda2OC>$I z$$vZ2oQ`0-@?xquTzyXk&#!goSYw}E|3m8c2zot5La%4?O_;T+=UDxG!4unAH^UaT zE4wYRk5<**M!itONBhW3dbZW-qY<6`I;VRn&qz8W8S_N@wwrhJ@!Bi<1He=<(d}YM z&n`M68EMv`Z>ou4-_9IX)=!`RLL(H-I>Jly#Gp@dJMDOXL>hKkF~a^u$#b^5W&f6P zL$gwzTXcGE{wl5I3on-Ow4~Fsm;Tcz7^a;_v-0~M%@Z}^cD0UjdWTXTK56{wk`Lwf z!eu-ot&_%g^<5TEek?4%=Sxd;l`jk}&%@X-_$Zl&dc*wp*z;3 zJ6E%T@nQ;v_}|b8aq9akj4NVnCt3HIGM+7THaXJt9YFnwY3jWi5|}Ea$xGp&KJjX% zkNMy)+I~1!+H;)FCP$jf*4mNF^7&-wClh7c6Qw+SZn#dYzC14eOfKquvc$UkAMYir z=c0a)PC>x-s<*@f^H-;Brc_zqctH(fyjTi!epke6RcNQIbtWsOWc$9pa=t9>X-X%J zBTZ*0H2Wf!&S)?to_uk0DOL5TbTzHJ!-A>OiSOG#<-0-z?G@IDgyoau#I0pLztY*{BZ1~x&N$zh z=2#jrr^ftX&ww=RhwtIAkFs0I?klnJTpzDI>PwUs_HZ0-v7(g6ZJka2Ox*wC)m_UL zkx*LGTfZR>O)cfg@gkSik(t-yn~;sE4jYZWt}iEaDdky4reM@;f8K;@&m>!4y zuF3~$VG4!l^ydq4IMylxd$pv89y#w+Sx?4k=7*ntar z<|!**^{F=|NTAk<>docB7NtD@TBn&;pSCvWZ5j#G!v2`%4E4JCRBJ~9wJ=Zgw(r(Q z!7n-Z%4^HYc}A@f!D6 zVB^=?wd9`5Wj%a0IohMM;^&TxQ!_Fo@~^2UQ~oaNd5X>^N1FByA@aq2$DoJU7Bm){kP^6=T@NDFcFT3H|U zJSO>dawCzPt&@CuLm3aBG+yJ44`t~BQJ%%t+2l=gSBh6XHELZu+eNnAS;o_7?G))+ z_OWdGu&gJl51&om{C4g%u5;SIq}e#W?LFDFP?V<}^(mM_Aug3KpGMx9Oj=pf2uul& zoSh3tc}zN|TnKm0|5f4oc-}#v7TTlr#CO)luOotYL0~?Pe%?`*$QR`qNaw1fJ@W4} zCU1Hw5wxpHBQUo}3-L?6>Q-6t*E?7~#TRy$buN|lq|s^SNK;G9Un}ky*?5u&yg#l^ z^2+q9Wj!%;nz`QZZhGZ+Ukxf<+8M7AbLuaZF%zOZdFV9ry0?a#bTlJU6j540ebeeh6C;epdr)4~6=`{1J^?Jw)9m{x@TBn)!eR^K})vPpr z5K+9aS3dc_GM>wHnmG~}6P+=+c2lZ)K1bqe^TY^dD!Oj%D7ol{OH z{SDrjrcNM6B5CkoS@7F39zHQ0?a?~*qWiIea!%*IXsN<@k){#tRMEJ|HoIIr-LWR!xf+3^CDJs%T7Juuo_V?rB_-Q+r zgJAkF^(Fb=o+pu}wTx_)(v}e6Ap+Y~^xp1r+K**C&(dk;=~sKnU2DsFzOYU+UzYFB z6h9HXg9o(|?)H-J=PT=JL8qDP#B1(2YmB0uznTry!gfsG`~3EG{1&po<2lBA^}Rka z$NObH3+Xg-q=jgdwX?4q5&emn6BsHFbuH`3NvD=$9q48C$@lwX)!74hPav%mZ;WXT zUjFSl1Ktx6iCg49lImv43~Gtj>y{5mpjPd_OgS`bl&9|k?%|wsAA62t+heV>vZ5#S z`R8~=jqOZa^H=nQKK~rAAn}yDk|*@}=XeE)hdU~HLZ5$*SCIJNt;(LqPSy2tdhrSp zmrqpogg*ZquORVSk7!Tm^Uv`L68E#mctW3lj#rR4upq{hasK(tCL6S;ZKZ{yK8M$I zi}D<@&Ogtxojx!tsl}pqu%EYN8gTG>17+sU#3NQ>RG3m_u40_(9G4O8LValwdUk+DjUv;@?2}O znA+~qUaQl_la2QrHc)G1_7<|kf+){Q>-_Wb+06L5WMiJg25Lq5-j`JuMR{UIu#JMf zVk+YrfGR80S~{SOoUu5{Q`9;!{aVC|v}8(EGe@dW%e$?k+%P@LlW}5tqX)+;bA6B< zE$67U^maG-4ULIQLY|mzw+6d5xt>~9i}i#)F&(d{JTdY5u~<*&6Vveu5>NJv^MpPz z9j_qK=lOU~=o8cN3KF@t#Ct-Yn2uMFXy3XD;$z?gI!7#9-1T&<>;m6r^#1n*i7SD*M=unztk(L}g{JtB#BBVnSUbcY*YJFH+K0XdIAiC_3!7&xJkk(W6)N4j?y%p(@ zgqLj~fm)v|@OVO>n2xP4fBDz{kFxK8uB!OH9#pCg5tSkmY6u`TB(dZ@ZU&ViML<9W zQNTtGNkk!(&_obK>BSHbq>F&0Sn?i-Qp5^U^am(KkRlco8{)Um4fF1vOTzkpS!-~7 z_v|^-?#$dXGiOIe1MXvX3o_!j9d}1Y97Y$7;NHh*rCN?}6nyLH0>*S+mv}3Vs3b2g zr3HILaNRy9EOCC~tvLLMb{SQI>n^gmdQzyj$XX;S$%{+6t-buhk! zPD~`+f5{OR+v_KhH;_#7*DjmAAxy z>=>i>xs2)KON?>tCGvglM;NL7J7+t~Q)6@iV|wD)6Or#IMzB=J*v!Z`uE^Ls4JKDC z#b3n*i0ruw1H&$IFMDmkvwl<(&+|5m5 zEJ~gfxauRbepHedmy%R_|L??q%Z+q(@Dp!t_ahF!HY;(`MQ(CWis$yIB>vrIQSz?< z3}=JXx>*6ZhzN@kKk?>pFbP-)K-Zio|05pI-QeV3(n_1WU=el|wm)OaBfH+Jiv zyB<24`E;;UOv~-ghS@Rt#^$m@zT$>Y;+Md}l2pRh!BTfD+u=N%8KV;i2%_9K*>Nk8 ztGj)1KczKw-1;l>R+RwaN?Qj@wfpRI=jZt``q#%rhx~!bEM%C>c*WwaIHHpH zcbhBTlS03WtVO^RN9!P)smWU>L?wA~dy>kxZ=GPQdAPJuQ-nTcHv)>K#l&`M1ytTMu8mEpBHPFh9QhNx*;aoYS*Ub^suA6ZjJC3$fvIrB%E`9t_d zyf~xf{owCfv$rh>6X$(ykvD$@Yf^7-J1U9e1`Ur{za__cJMMP6W{=+1@rM(-d+s4*?)yDdTbCa;$q|G>E7jy8xAslu9^(5$=LJUU|8a5w1E;S^SwZ#9=gc zM(2K+cL#QO-^30t=Ze$i92V4U;U@?9v;fz%V9XvQmr&xSe(btR+;v-y@rEcC!K_BL z_0uJ>4@|Xd=jy&K^>c9NE{wZ(K}MWw^PY3{9npG6>z6IGRSx(4@OIpJmz~Sx%wG`M zMNVI|Ez)XJx9@eH?p<5Il{vvlZ}^e({>Qa+hkawv9=|7)&in9+X5kton*|?f{Dq@> z)zZgnj&hpj?2bsu%rLn_EuDGoNFb{Icx~>Jm~k@Df*<$X;Vj0^VR`HvS~^HK{4K2; z5z$FB=6X}A=F)WcoQ&L2y})3emS|fOPogruP+3y zYLa&!a+T%I=iVFj?DadG<9llA2UBsIoZc>_KJH&D$M|Pp9g;(HHj2@h>*iKU*n_wB z26tWJ>3AgWb7$1ZO(e(pT9;QhX_q{6VT zoU`X^>Iz>y?=V`a@9(W=$A8TZIyE9tw;)ug&oO?YL9WP$ea5lq2EIIW}qy-sm zwFmykE4jqIHsJU1)SchIEoWq81o9)y{K5K^`fp)w#ES=yAG|jqzngtL+ztO`NsjS> z&Etn3Va9nW2RNRQE4g3GJzUumzL2=&FlYa?qMK`BUk)H#u8InTgRLSzlt9L zMBj`3%Jii}&m4@*K6bxKpII!v3J}|Uh;!4w^+q^C!}sTqPjcjLc)>_L^>k zM7zK%N0xUd|tE;mh$#OE~kT~yczj&2$tfR zy69Z76wl{HBUs9tkuR4kmhxug%OU(yWi$C(lY5Xx@aAo zF3;_kAdo5fW9>Z4*zUatp+ZZ1Jd$`dXLz=dA4LUl{`ZbJlA@8XS5&T z&5)cfr{vESBMP4rNrzhwZgXDAc5uT`%1W2++QMTmxUi3c3;SVkVRJe7POW`g_J%{ZG+Tmx zB`3*QJLig97HjqcAH>P66ghRzI^>qn&@;8PFlI9vcbAAmT5jHMd7_pU?rynLdlPmY zg#TLZ|BN|-9SE>oLkG%n2<7PQD+lMDQ&MXB;p+qCA>9FipZF2x&Mb;lqF3KUd z0l6HE<}$&%7&yHTgVp*ruv@c~#7(+fCfpwcZ}?syqJiKieuS~T!;aB9Hcqx@`S*ggSJ->`2_yJE7W&nxKk_fU1;%u+PIIm#ZgOJZULFVb>{{Q$2LuAAD>qJ` z{Iix8zHRy4+`Cok{X>7`FGBi1|m zH#ZLXo*(?s_sI@}F85bPgS%$u!2HsX zZj8V^k$WGbtrECHLqnEsoX&eA^SA-j{7`GOUB8xTcjF zcWtr2*GRt{0!#UIn4A8%XmpsVb2~2>1~3bCw_$I4`9YG#j?XF zd9;+AW;I50&&OS4;`C-I@mSDgU~Mg&-Zr8#7@_BY4f@6v;6=ho+c^lFmS2bQ?f`08AYOuW>4c*pmSR5&l9Gp*4ut5k({ zH^xl|q7zz>pD==RtCX-#cLC${+X(zbd`n#2C|V2OHkYa39b5q&;M@)}f_2CpRa0+D zb;xWPTMicC-6#hmKO?ju_WPM6U*Cl9{uFTu5a<@tjFPK!qg z+h4|FAXWhJ_TL8sEX8A#jd%-nbsy?USh`sUOWAdG;%v=$oMmu*(6J7=vAkkev=%OG z9x;?E75X{uL6m$NO3pguUh;R-qP6gCb4u{>Tm1Dn^L@@w7{U6Kngzb?0JyftfODI* z^F3<$-MfV?9B!+@wf$>coggDvN^VH^yseHd;M>0X?{%SK&=HNm2+j%XE8t#k46f~N z2;8=OTbgfY7mZ*kt_6Gk;M!$2Sg(u zf~C0Lq;@@vmw1N>4y~ny3!6Q~NSysGMGzT&L_bwq3l}z{3tI4PyiwxIf)R3Koe?Hp zfY^k$sg@7HIgxXgCbvcFU@6w;A~N7(LO626lCIItx9~45M^T8v&;_}=03GlONs{@4)>Qm%^Qy)!Ij`v1qlf%% ze5VwY!<5{QFy8}=#=1S@SmHqNV9!F}H5~iSuo1$y-4)VH3qhEb9;1~Sq4oyOx2zd% z0bWa9tFey*8!;YCm~oJP1cB>>eH<`asUvNsht`92`T~O7=pK+9qlIsqZ+hFFTgHK> z`fo@V15Y(0SV}y%j7OUbT7g^8P`Y@aoM1JM-H{d_w72$K@Lw}PQt*tvzJn{{xy;(=z~ zvKU>!(9H;b;zt;-I^d5Q-z~ow7>*x6U<7-%u|3dK-sCZKb6rV$Y`&?k7KUzaCwSi@ zhHgf1nHY`pyTokGQsNI_b*;KuxUg*mT-613pqErAq?ednagV``!>Uf8=|_#jV-Pqk z_EKOYgrWNgq#y7hI9H6usY&A7=5laNGur0cW-0MfkkGKM7QSs8alCdz*MEuYBZYc{ zeBIx*Dn<)8HKXCv>3S#d3CeI5fzy(>@yTG9W6^JaB@P1D_G|=3u(t(97cg`$hxAAv zf^))Xd~F~3D{(TU|A)X*5;tBPY{dD_O%lJ^7aOh%?qx=>e+EVuuz)wgnA_SnQu7#W zkGt1hd_Ow)-Vc5!xTeAHeEdXWdvI-6+f!i0F>SzJnZ)1HukcJ!zykjC$Zq++Y>o{+ z109@}Ux(*6YQ>Ky68l5P5d=m^+(@xLoYn%TceS%Mg8jhh&Dz=XlkL;#p`9lZJE9De zp@R{eE~8+Cn1cuPPS0en!)9OR#4%!t(n;XtdxTugA$*XW_xJ@(%AHv#ZqA z!UE1I;ZDP?ug8PeDV&Rvb9>00YJ=dE@Ffgx^7+WoZ@yeHf^!J3ju%wuT}U4m1f|RO zwrvD|5Axd3|2=4YdKGYbKLK{`Pr&QV2#Fgh@tAeIYaLy{(0vOyz54>O41u5c5ynH8 zwUSZ(HLmeyWv_QXOS&@$=hVW@(H@*D@gdb9tBw|iZXQ<(IKBS?3wQ&tfb$dfI>jr% z%Oi{qM#G=}8x@cd4IaKPx-Dk2#1Mwsf{6xH2O+lV>fsZIlD_;du85>H%n12I~7!&#qF*Aja+BgDtn z5_t6!j%`N6+v2qzZ#dTvmz;Bpn_-*hW~ZR8#1|6lUR#}M?*wOfHE_kV4vCxI z$7om}f%q7RUl90-AL03+>k97W(x520nFA{fZ3X#ux}Ch-FPN}efPmuq5F`d zF}_@JPFNrMZnsvUb&&osy|HU4GPWK+!pw>A#P;3P9CJd5(-QwBS7BWuoZg&Ue0hP* zydlc43T5FZjNmd!dqhseTe*^3hDHaYv4#T+cuVLQ2Oa!G;^toBB|0*?qVG<{Tg3>z z$;fCoQyIoO$o4(Ib&zeAN*N2J)RnQlFq+8zWNc03ixf%I9RJ}ifARR(rDZXXQ z`jnc0Hh&21y%B-6v-bmT8@n8g;B*;{bA8YD$Ts(Dxg3nP>#E(qN#Xf3Plm3Tbuz^D z&R!8NgP`8$M|i#yuKy@4yrOdFP{~_6hd3>X8!5RheaCOLb<8bevC?M*Kk*}suLg{} z%WjA>p3<$7Q@Y$v@J~HsY_kLCiDjfGQZL~gvTl6QcN`V|_REg>ckk?&&rc+7a$?^N zm$3mWrLde`VuVPUer4xMbQDMWu|6G~E~5*^E2QtP$T%&Q65UM~*3o|rc?Go_Y_%wE z3J?#Yr-EeclRN_4j)zYsjIbk}o)Skr5lCiI;?tocYc>ru|cU}*1=M& z53DfRyYqViF#!mE;zt-yF*wiAYj}<+IV0pAy%}rma+KIIG0>=MgK+(B4Z{3{5$xSV zsrq$Kg-U%A6>PC1%F31KGdZ#SOyuP6$~LnB=SuFxnK_a5VTZT=sZb@P-@&JY(`B?$ z-(|1Mk@p;)Jc9g0ya||@)7CNfqK@{^!3e(&<2wU)Ya8kCFTs<6hnk-ZFoOLINZiyk zYyiDa$C>%uk1$f4LvXUrn-m(3-o71y`zZHt9)q!eK5J5FKcr*uesBv)-1wd8EyDzGc(t6+lm(l zv*KXgm(P`%`#6X2jC}LBY%@o3TC7751`USQ4x@rAR(HxT@n)xdej>g_%#35}5X4#_ zzC~auzc&&y!xhXpW>#Yz;_<@h;FPfQ^T4QJ+w&b0ha>P4L7RDr_1Uu;BUlHc!3TMv zfA|1aeU-54QZ1yik(UVxo@YgnfA^E?fBK~(3Kan_pd6}=`f;KPeOW1s$3ED$+M9I&h)Gd7_ zm-XC^opp5H)zf@z9vy(VuKm>f1_&%AapSp#>kWMuUlSFMycmi@V1%rxjpr6dgSBVp zmJM4`S9128+k;nOoDxpTqCFa+EvER|gWG`7@Yd7%%AEh8<2c%b(_$%E$^MiOqrbUZ z7(?2=Dm6+#=kv_ijxKH!9gV@G=6Y%efM?@%2+Qf~8nrZ3)c0t>RE!rvosY1y-?UGk&5A9lU(eIezbyvrJXe4V;_$~&+5 z%29RUsX&jMsPHDA4o0xPg4y66%mj-Ocnp`g8FB2{0B>!$6yDfq1Rnc%Oq3DTyunH( z&z}@ZOKK3DkHF>N8nyjjEx^~rO}asFk`KXBvi7O^c8q@Hy195iMq_>TE2Jv`#ZUYQ z&tum7trG*OT^j`FBFGxY^bGc3CBJ)Z{{2T21IHje9D#NC5hf+2Uc(x0AEYn%5P~*2 zVKnyDYG*XVT^#o-mFtT2aciP>pO~K04v0DEB`hUzvm0(B&SahnbVM(ii@;ALZv1fA zy=2crdlK&}5gVR`zzFu(%V?ZoX}u@$45Sl$2+kFwmHME~p2V||u8+V{5;yzjHsZ~M z>2ZaXT)tk+`@tzGb;I$I*=DsQPfY*e_GdKS^NM@&`<29wzAsnozl3uL>qnVA`PTu_ z8iBQQx{Su@wyXE#7lU*M1eTJxX%8FGeqCB{>6e`YSMBT^khQ3pStM>|0Dgm&Vg$eE zjJ6T)?YJ^>Tb2>>Zkzd>(KuN%uyUN4)mTc_aApRu5v}n3@$b5mfyV1G?$SHR?;V#Z z^8FFO7mHPLM%#!x(#yq})e>u$uL&~)Fj}emHYLTGS&b1~4n`~0q@xZuEp;+94}qm5 zZq_9>V$<`dLe=x5!p|V^6N#Hyjs4fyIvC;CVbDs=z3q{>n=vxD^9R)~qq)Dqb3Aet ziyWmR@Dt7n_exk&fk=S#(>?_2U^K?Wysfd36&Zb{aF5~s275^nGY-!(j4t?^IDHta z7_4OY2_v|jlsb(smg7Lw0D_yfA*^_ zuBSpdy~Bv`$a|cM?+nr>!G*0JPIBshI7nA;xv;PPbf(2}Y; zdc0)W1mmNTb?kcYerLezgLE};VKW+U)Wew;FS5~bdPfK6(Ck6FH@L7Fjb8G8r`&m1 zy|(q0qx-t9&S#Sc={vw|J#k|vr~33kI!$=7)zUY5<-lvbYv1Kuu@vir6=%z?>{CF1 z4b!TtbG;sQs!SZD>w*iL^(oaS@wJ?YruugXvuk`%`>#!(VEKy`)=iz3$3ej9}IW8oEW6v z1HUh$u?x`ggSa}VtFTXpdU~q!{13_c8TgTAG)mO%pPbbw`CY#DP$OnKM-L6sbKzS% z@z^xym)gm?7rb;UmGi#`$P2C49+nj8!+o`fnz{W7tO!=^KKR%iryNS2j*_!J+zmfl zCLZUa+>?M(qox-77F*zKe0h+52%d+l-uasI_q0L!Jp6jYvR!dn_Gy$u{GKs_r7V5w zjz-tI%%s=@u@LExEsXSC)(0%8;aFg$@^@KBj=U_d-5V*;|?m5sYmpfSB32t&m<2&kuH*ySuby(?A4sm88IwkJUi#j;1b7lHD z)juArL*OR2(CW{pzKSn~a>&TQ2+kFwm0I-p(p*zlT#hndrZ`J82kV32CTFzNEf^e- zKKfNy;#}|ESQ3@w#ar(1xXD>Z$BrpZ<@=NMcyN=CU-z7IW7omDh09HzO5EfuwfEKM zoI7VG>k;54w-IeSXT()U$v^V72TO68Y^HHecg+JsogsgrHNiB_XpAMRXU3Ivix}Ke*Z) zXXcn>{Q$hx51u}sM!2F7iDhRMigr~f<0GDR?CptB^kPmJt<)EF$Hw7AO1L)aiV-4p z*VYt08eX#*Ejo&x)p%a=dSb_lBjT4shuq(`{4 z*rL7jM9pNq0&M0Pk9TqoS5DR=T{iR8FO1Ctr%V{!GMDHu>5e&ckF%;xvM%K^rl&pH zFrhqhWf&xl4wkAqp`}wIFkKON_ZIG<*arw{FN4y$2 z0l_6_DJ~OkdLL=g>}TW(Uq)6rc;tLC;~wYP*{QnAsQKtu3GXyd_!*w-&-rSX>u2zD zU7XQ3VdixC&#N44kYn~gI9Dw7@dpn(tI8$oE7uDnLY2*L<|d=$&-rr2QarliY;9Dv z954fikN6NQ#bp8)_Wg?^btO;A_ejxgal4UAYa=A@Sn;|1=c@JN5yYCtot=%FQ}r6~ zpL0sM$(l1YUQTOuLtXKck3VkdEO|Xu?*RWf>w`CI;y-7EYS7l1vMyDZ1OGXrk)!gr z=RJUu&qB!=F}-02=b?8}_4D9AXEZFrhvMR8T*tb>LU1`OwAw}d=Unots~&b<|14GK zga4e-O1-tSMh@X*dV~g(LH2NY{H^BKd&LK{Jmhjlj86j&1 zy)#vB0_QoSaVGWn*xbD+2kz@z<*@QjXq=8d*xH?IaWg*1<=9-Wuk-gusd^On&n>h% z+~b#cSpkT>oe^AGMx%D8+#CN55Z54ATt8*14R)$8O4aXx|D4fSi#ExNlkeIn-&^ba z@UqkL#T0!?7|b!_?D{K3J1$%E(Dn!8@_~?BTm04@dSJBk>XK9)2gY;@ty*T~#{Le( zuhQoDGvgJfE34&)-%Hg)U4G|J>eq_HtQp4a8JzmvFlXK0gLNzz)1UlpsPoi4Df$=~ z)AxQl*a=6a=%p@Wx~lj^P8%TR15y5e8O{&y4A!T?n9c}J$!1Jv1kby9-;Q?XeVn34 zxQyv9{_wJElaUr=L{pXF{4_E}*8*cY>r+ZE@)*+@!Ray@_wGvH?DDwdE7Dp4@Ep!) zc!6$xYaDDP?(G_u;x@1mRfunb=hr3uS4C!LSq=QrGgbEj4+ZYNUQszG4>}t7`jvQI z7%(|S-vs{iB?F&z7Cn)w*ImMY&QdYMo^|53r0DM8KerLTTu5=t0o#w&cUg-2F~-EB z9{)L~yZ68l=iBnBdJ*`~84XXI-;Rw-M>+aI2iMQkQDbmBGgX%c7rI?nJ2Ok>4n@Bj zfc9Vn_jX2u+v%6ra)&_2VCdjltowSVvwmBu4uWYz)>re<+sC;~8!-bW$M?mWVFcC; zjF4}q=x_h8y!H0+|)YQ~>NuKw{M=BB*vl>IbS*9FrCqhX=HZAlJh&F~;!t_C-I zBa#zFyYFdO{>5L0{Nt^o=ncm2@BcL{!Ut=w$gC%3tp&uEXVWj`md(He`w1aVo=cDL z+S)p-CxqBLe0YRA*G5=R2yu7G5&9AyUN=|P6GD72b3_qbzt$5%l&>=~!bWW8%6dYG z-T5OUjK?;@dP0bmT}EBxMQ+xwSKP&O$kw>aD+`H_ubrlMyBzB`&AYv+jMT>NV@7J0 z+T69Rv*YeGU3{g>y!i3__%t9=(66Kg4T7brO?bdLa!;Dxyitr` zsU<5rI~(p#(`{Xj^@RiK<>J(Gc$p8uQZLTx=G5t!rhjrd)}#KpF5xe%MbG$h#ZrSO zJmNI(lBQR>9P7``J(K$hbX4-`V5ysNNA-qoX*%BJSih}bY-71S;Gfl4YU=sNoq-Rf z>8dWr`dv?k;S?=`FBEHy~?btbh<)BiRUKFGsW53D~|NPDo<)aR3(tQKkdjY2ur zz24xpjnsPVucIQ2$9BK6o>0Fk(=sE%s%#^yCxrNIc1DEX+eTPV2(j#k44XIH?I7}x z^@I?=+%Z}g&arMiA;iL$Mn`zjBe@b4%6dWw>C=W`)kgGcIL1qh5M@V?xinX<4l6A} zSSf{F(To3BX%XV#on!2}$^bw@PZ-fVe!RY9Y?n-_S;NOi<_VT! zwB6>D!{Z}!5u*!5WQYz%u+(kpg-ho_0jTPM7b0^iOEKEcRoc)OB6fU67mCOb9gJY9 z+Vv(B!Le?&dCJ(xXuy5UZb3#I-!wKd;`~AAq7mHt7>%1-FEk1^eX4+Co!2GaiX$q? zi%V(mX%-#q{&fMzIzRE&B7Vf=QB{I1zij2ci=Gs2rbX5w;0uVvMMsIgHwMqNdB8>R z6K}=gN3{K|RIqld0*-Z03a2(BYY}iTMdG64(Vpi6PvjSHtn(9Z#o|w{4Y_i!X^Vz9_76$li^S z+R@D&9zFEXZkQm~9f=J2Q_{@v#D@}zjCj!NQL8++*Q z{+)Q`jRlP9{KT87{Rn1E_oR5Hj!NR+aULgO``ve*9_5zYKc5?(4ry64pDXos)9vry zbrHc&ycszviGPO&z6G=LU%1HQ?nx=-&D3DBqZNQ#@_bWre&Wr@enerbaJy#|yo68fT=S5^U&JTech7tEbPsqQAzwePC+z27BVA)Cxx%%$ov|W#J}Tg#@SNgITyz( zZ|;jq^5WRzqq$aPe9-sC@G}?3L~kCn5sY?6xR4p)yqs{l;LTti!Ymg$e~~xbt5IJ2 zL?!X>HgC8$7eyuUOvZ8djS87J%0npS%}b~&n$_I5He}vfPYS+ABWtRtB>r7xyxSr# zG-lsGUkgShc?d?M$uqu;iw>9AS1_x2vwl<(uNgRQ^M-pXfT$!7ZLdDy!yV(*5R7ut z$2^2m-Z~*FiGRm>XH=E2nK`{S$3AOhy%Uwhzq|WULAg;bI=pmoODVEW1bby94xtQ4 z3nC~8Gg1rFiWg_JNEu72#LfP>AWWS1phd>~6|A$oednkojvF-Izx!k3{)aLY?CN{z zUfX|}^T~!mx)NA*Z5>CSdZn4{(f{L?!$YjN7k0RdXAKw)KH;gBbg{RdcUT|JExmI~UL#n%a@}1rPe;tN8=Z}B4AR|?Jm)YP zC9`x1Kr$23W_RhpvgQMVHCN%if`nQRnhFrm~tW{V0`+VeVe07k1c<~U2 z(cs(OvLn7e5bydBExT-W?#BtCa`z8$7+ugqq|^l^w@3ob?V-D*>{FL!_vWM8!>$1*c|NchBpGi zR%gy3TcGhqoqM|(PQ!=K1Ht`A;;XUCUTWQFU*G+BN4;!wc95l(+!%Ix{4`k447}_^ zd^mjNdfc)IV-F44tQZU0D1iqY^?IAC_p4bZXYVqKZ~;T&?Az)(5$h6EWCBp_xe<;Oc>#g)`~U$UKug5idn)O~CD-4xjx3&;_|Nh*`te&iEtQ+M>ykrIJW{s6L z|8Yf3oH-%HQg=SO)0yie>%m`SIFEhwrIVvl^uybwUmgFkT&|3Xu<-_2`}pJAoN9Ga zbnaEJICeSS&aa*D9ZHT}cMBotdUaEDv726T7_HQhMJ4hIL-&|ume@l70j6uD=- zxURUgj8-asN;uxs6_=b#%V?}5w)~!Jm@v71HrM&e+1)K!cbPQWS65T&-w`>>$xVNOYT{YXy%j+D#=D$liwEh=o1SwRt<(?6HJTQNXgT?7 zr^VSpdis0A9Y!lPywZ0$=2RHhRmaf>oPSf2^{hw6IE=Px7ysksO5$FP^0-pD=fqgg zLT@9?T9ox+-}$$Saqpq7>Y=W9w~yyOnG?;aCotT0UD-tT=;bybkH|7#nSJ?7=cp#= zi5t+bSclBWW>=SY{-orvUjc!4-9m6q7!6+*8~Wy$`Ga#M@oTUjaoy2{?p$kfSfB$d zC3oiBd)rp0(vlQC^yqY_<&2Mi`;ob**G`h}`C8or8;rX^g9Tz!q&9W!~mbK%Vt zJ-4YX#b~=6jMy9ZvGd(qDSAbcjmBxJtu1rW-r@I=tKTZ_aB|jS3~yyiF&g&D7sltc z!dtrlZ>@#+aEH@nU5b9RosGr`)9r1V<)Iw1N8gg9rY_#)RGgKfhospxT~Lk>fT#(C zCH31kR(qJXQEFnV-tlr1>0^{5@0PvJ(@&-76Bk~^ZAlNoDZxidQr-sd?1%S#CSMTd zya)GVeCJP}p8E)LH5s{*_s+aUEX92c>-MG_H((bojM2eDs9gsmBenewp8Fvy$DEU4 z9p@$@XUZk9kkIOlHE5!-&0WzIJ-INYNi0d&RNPs{PoWE(Z_hDGR~% z#%QJXw|XaAW*pc?Ed@1e?%BNA{>|ts%f#f4N1KZ`EACfp>uWT3fz#x{G#wlud%~@5{gd{cy|!g3`O@?D zov%G#CcYis+Bv>{#ZvcdeBIgJDNXXYP8>0_o{sDY0dEcItj5Hb+hfaXDBj zr`K|)SM@Z#(d9qyQ+!PPtH{-ZzFe`C*fNYIUFz1}Y4B{}QAr&4YW?+$^UXY)zE=)T zmq(%k9fjN5*<#N61jF`r&q~e~b4Ck-?d_iR989|ZpZ0dPm@`^*u)W>0lC#B}(Sl%m zyJtO*O5(Vek{}AVw-?m3vA26@L9o5uvz|vKaa_iH&q80gy`3%Qj20azdBooCS$^3H z%xFOrWqb83=C&Qywzqp$a<-UX&fe}>wxg0b?xiHT!gy7PEu8J&j21*u_IA(SYTMpz zo4#jPXDQYvI@sRsS;^UY&NVFv^!AY1UGl8tY%yoFAn-;-?CqYFoGs>z76jhfh`rsj zm~*Q!S`bB9&plhXZAph6V0@$STTd74J9{>7wtw4}blZCFS+=8+IPRq+xnk=%Tg*A` zwWc60S|Gy`=X5=bIqM??W*oMXd*h>w;2v6}3R}bRDooZWX4Q}Ot}#0PQH<>``o?ya z;#G}kuvap?dhu2koI^i?rFdOp){mF!U@329aXB3<#p{xyb+DATUw=6rEX8Y-qIIwo zuThFdu#`7jUM^QG#dB@ZI#|lvgT9;&mf~5oXdNuYGiT8Vmh$$XFPAHp^5(wFAy~?r z`!0v@OPQJTl5%9oiz06)&z9bfav?e*2uaJN%jlwYaJoE;UV=cTH^15=oSiF{;@O$UL>H0a791sKDV{|yK?ndSdw%t#?723Q zD;MF7`kZ%8>0-$tEq}br$}<_Gsje~v!E+zy%A0-s2ygb{bU7t|t{74Hd__9ka&VjT zio;t=Tn^z$*(-oZ?OvjTR~+72;!xb2z(>352eT05Cj7shlXZ#wWXt=3?bqPPW^)4D zApIqR_%Eo9y`lwG2e}A?wh{NYkIsDy--8MG9%O{v?c4ilvR>Nyc?*q`{Wq7+HTFc# zl^=1{&&fJ@fN;xFU1cyr;^vN`#66D=Cx7}b)H$0L90j3eivE^gapa2dE!Sz%A|Uu1y`djcKOW+Z1b*U282=owk=1)I=UwEg zK61t7V6@yH?YJ~qciJhQht)IpC92P2gA!Ou;wD}3uyF>v;gznDTqU>8jp$&6+!=jj zQL;Wh=2^$qabn}Z{6RUf;ltUnVSd60zN3np-upj@_$lIYFq&&xsW$&?$^Hr@7f&Ia zE6zLXvrEoW{*oKdB=~y#_K*Dg);0}FV1&etlfM z6O6`_3ut7v4V!+^Z#S{Fg8qR@C#m1EW)Gh9xk9{c|uHH;ez?9jKF5Go0Z=aBpX{TXOG| zp0BiASCP02F?GfDBR8^5{n&Lis%)`9pXp755-bGtnRIO*DOIj37PueMb9@Mv5+5mN zA5PKnFTaSfWWv+?vbW$pcW+2jIV>s4AA!@)QhpuAiwn-l-*`Om$&1y3 zKO-_-;5xhO`dCzqPy0e;4s=oNcEcCS4PM^>Q}I%SXIAdp)r>H zzR2Cdg8u~fj=O0C)*<)O-^1PTA>-r(#GUQF%QpM9EX6u(#HQ69;})TI?}ZMQk~_nJ zDye$%!j~Nz@mt@+pC65ldc*B-1-eLBLi5vQLy9GZ(9t&&`kc7LvnXDiH6 z`&)&#FFzPKjKBzq8!7QK)BjAeKHo`J`g8x>9Z1AmTM>b^OWgS0vESNZ@7)`mAAdCT z3<4wk&XL1xQX ztgXUWV_Aqp@F%qLEP7(6@vdhhqpv^*{8)td`4F5gqv17TO{-wp^9KVC0*@u^W6AcO z^cG6~BuafB0+*Jh?2_LF9akd#FaJ4cAy^-yagw9L@xb>Jss&5=#zYprGje#=jV&R9%n2QTqi8Wx(oW1nQ^#w{rXHzgY*8G zn&C4W%Y?r`;IuebT&Bw-OuCGQ@0|zI!=n$D4lG2l-XikdW%=7G8bMyU_+3%z<*P=7 zyFa-CBs z0ZdLk?7)d=;S6Ib`Q7tvD4bzG03p0&tiz8mDcPK1j9`6o0zXm<7H1eESc+@G=0xIj z<&=f7El50WZt_J=q#u9Uo%k5|chV3T!KJl5E(#}7GNd2yAy|s_!MpMAyA!)ZS^_`u zBh1%*0VmQc;Cv|s&KE8%=bi5!*qlgQ4n}ZFuBY1|JlzK1={6urFS8nxSmy1k%@??)r zaT(y0$|ue+MsT`vJHns~nB|s%BW^1KBdl~SpMb(Fmjmfp2rR|kfg~m40adBG;0!zP zMaO&ztX<;9KcL_Hp2thZ2$3?SW!(ijt^)VjZl4ZLm(d0Dhv6mTv{*`XU(+r1A}^Wn z8+-;XItkq4ByQT7Yg(!1Kzsse1@=0A;ztnv}154{=AvvBX&in}!D>@OMA^KeG`7 zMjuNY1!)O>g!wYH&}xm^8>rB#W>^A$Te8PA8_}lC^w2*&ZV&E85MMa=)=AT!{yEF? z(Pw+#V{4QjVf^jL@1FleLS*<6U2@a({eug90va^>Zma2`s7G%PN)S(ek0zz*N!w@J zegMfopi-%=r-$l5dath>R^BNkrT+I#pU|h+`;fpo*mI!m6R_g(KB14Wld;!_U@7ql z_-f}gUHcJXiD~(BpU_W0NZ==agz=>auczrZhrYVDX;1(V_)G1Xrkkx79gN^7 zeuVK_sMOK%-SX>v7#ozp2=*Lk`vffizpLWq-9V4GdI{H?w5(}OrLNqxF}pF|sBC;q zu#|XDdhOFRJ)*{IZXfdyx!dl{Z3INF55cv-Xr-!k9~wCOYQwMu*3MGwtqBa>j|>f5 zH?3j#cp(Vm`AJgpdg8I$oIsC{8iyrty6n@*)^X+*Cs2G#<8VVCLVP-nKai$BeEl_t z(Ml!#TpzTh28$kL`AA)nmXq?t6_IKi= zke0ww?9IwXZ2oUgpa;goDBqaKGXRf|uzukEsE2nW+V_4~?*#0n<$I^;J3p6qF!lI} z#20oq4W2|0Ypr?0UN6O;1*x8cSzUk7>vQ4E0*tk2eQymrHQ+%Iwca&q70dgH#J)b^1Rb6aC9$&|5# z+C%)21y82w*`?sAka|AWa5LNGwg+Oi55c)&v{Dyhnk3%xb!=Dyr^Q~t<*d9nSqecx~vnMj5}XO6}TQGhF(mGQm0sjF7mI z60fHluTRyPv2v633=pLtErFl-5ys0Zyy=34uQ<58z6Wm~e;crmPV0L|wGqa`Y8?}! z5%>#&zdvLZ?%liV+vVP1gX2d-{~S9S;<{pAqg-!FT@F#QYpUKg@D+#AIM0xDXU7_NO^#k0x)DJ+t*tSH3#B*&P@~-^3`&IyhafX{9!G_$)gEh$n&I zCyWqpR=t{~={1kfMy}?e1>2zY`XVqw;>Js_=r(?Gm8w_$-r&>D_ksBcjNq}0(KvYq z!~>AN4S}U3ZoEy~h`f6Hb9SP3aX;KDhj`_>Vm5qn4xHz3N;viM%aKsjE!D#kSc?64 zu@^98& zCOGMv4aCZ|WKycj;>t5a0psl*kgkNlQW7^emTd&Mwu0rsvvm%9TZ|Cvpt;G)Xr(?R z7FV95WR{88tSE=*?U`@78;C&SGxPo$1yVS6R|CA`#H6Mxz*C~8nL)? zIT#^jst5nO1uU-MMGpRhQcGY2OIfv`&b06tRykd<&~|Uu0Lv2SX{Xl z3Rqlo!N)4WLQqZw?fDBAE2l8H3X`jaASp|qnv-=ZPzSu35?DL?9kgqAE9{9kfR*$V z*hx7p&WY`lT-Z-nK>C~y!BS$4{IhS`MfTGnuySsN^e_Z|;zt-C=&;EUw=CyMb{!u5 zF3shZ<80B+fn;FXQ_m7Eb)Em&Nptb=pNXq(HFpD;q==9>zhG?A+_$dv>}NZj11 zwR4qw@1Dfhz~(7|5q#^H(FI(lX^>v;LvT(Qt<=Nq_ash$v;>xtxcQE<5iKe#%9(=s z{9(-JB4zfuvotKQz7e-OJ@H~A1O#rklg zEqizVDIoqs;3s~Bx1-ph*YF(ic+wl?V1)R{FnfY_IZAAq7--+MLHOEkw!NJZ>?J@XK)Q)9SAK-ir_^`Z>v9etSEZ3F)*;?O%$~5Ft1XRmxK{I%0SSy?FLDw$H4W~X z-lyZ1K?l6PTRK>Z^l}Ipg-X7$QDEXr(wFK5K zB{#dFTnm_;ZyuK|JrOtjgN$H(j8E4wVPOfpj$=f~8m=u4Y6}2(+BrAlM8+{PpbvAK=Rn;0&aCP|iRusXZak8tFgm)4}PA=g3#% zl6AX1a(m!kAmSk11A(9T5yn#`xKaMd%3X+_cprKq=ZevsL$Eu3(>gF}VB_F21o1+- z9DJhTy#rhymL?~*Kg-J@OipZnmJPqym2K{Avkvhbn6e>R_uNq6Gt=mU2eW=-a$yY) zw~{xC>Vy$?IlicTKC$1DreMKrYL$a?V*AXjA9X(QX-E%85Wizpa2x2#-~h|BrtOdH zwyNh7Qz8AlPY0*VDZw(4RX;KRf3d+Y5%>wGYkM$D?41=4zdNo^a;_^zi?6_HxL0(~ z=W?%T#n5r_V3iBY>1al<6xW-)gOMAD8Qe=GZu~V$-1ud^Ji_#FMl1Dd-oDT_RWmF> zo){_i@Mt4yRD3P8=*jxwtq7c7i5u^f?6nei0m{7=>i$&y@E8Q{4J^g_z+a18eTW?W zP)M%KTWcdyZ+b2CDANB9xndohL)HhsYRlt8gKugQo`b+r?BS8igp;x><3rOR-L(*e zX=g^`zS^Mt(5Cuzf)x>#z|UEOj1+xM-bBmar+C90GA~7Um^2B!%$SI@zsqv7^taq#~T?kQs;)d{1; z^ILk|R6V$ic);AZ{Xq5zAf5q&r8r$1(dnr^aZdm-1_+iCpKzyQQ}xe1;9u0zp}Ka; z6MMVkLx_jB`|nQG|IL}|FdCjnuD?4MURA?or5rR~i9bX0y(9iJz58mT``1r=5F=0D z{r2d?BMzhC2U6EhJPU*be&R=%FC_4WPimV3Pt)Q5VI{-4;+*hEq|}^O`sH`diw)OD z;3tfbc!V!mt;o4LG`myNpajm9#7$0YAB|7t?`pOSbp?)R3nAV+2fm%GZ(0IIXsfQC z+HiH~5Yq4K)4}PASI6gLll6uTGCLnynHF~pI>2;n>0l|=hmm3Z)uH7`|9u3O^6M~u zC*k$<_di14_q;854uKI8H&Ws`@}_agdfXbhF;aco)w%I#51f#;5aQKy=?UEWE;-Eg z_iAcEspwl@h{HS^Uf@G;x{S7S#ZvxUwYcaF6K6lB^~>*p_O4b)o11pFN4U+;l@D~U z+%((_fm@0_x!S$t!(rtE)gavxf$Lr3#?vjMmC9aqAuy+OmGFBAj9^c$jK*05=%|GB zAM)jj(YB6B^Lpi<$d3(P6Ng)1fp!@M#gByiT;fl#-&$x1Q+s z45QEX5PMgr{{l0B$U)$=I43q@)`|;(C(Bd`f9$JW?n_+L*j-wBAutz+_Yqi1;>Me< zjkx094#B0jd>Q%*ff3@X_Q$|reQozqu5Vm}hSgx=>9{KB?b~I%qBqBnFz*V!&!J-p z(ti(ur6g`V_Hqt!TXw+%;cIq%9;ku9IwWr13EK~({_Xp0?7X_na*PnE&#oM-Z(W)0 zux{u$nI5eCxeC=q;3wj-x89n;`dsu_7h!UUyOuyyg0uvd^85Jp5a+L+-%Re9z(=*~ zN7&RT=ZaG*__~XDP^O%S4@=HfLKXDsg)gADfB#d5;5UuG43ui}WkAYdo=A-vkH+%5 z=Xv;_E`Mgrw*gL!xVbV2@nIReJ4N65*krfOO`pbnoUZ=`K1EMo>Fe#>Us)ga?&iNA zXS}h?6O)$s6g^oxRsZxO{I}D)iP;(DNJJT~FQgm>ZR?0Ro*pb;qFA620+*ai%WZ>G zFF=%qv;?kqM%#$ole6P;P**tH7vwe&521f{PSKA%`l8!QOdaCO>+8Ah$+L#O+GPaS zG*0O)>lDsiz9S^TFJ(NE+K3f+Kcet%NbvV|;}4npm{OnD86KW`{I5g_{6yjg!R50N z{KSti9<&SkuKA*89gOB$SL$5j;b9&AGhYHrN!+B%H3Eh@Ant>7Sp@5e4-xVGss^KW zzeUZi_tmbTjg;-ZH6?3PysQA$;;rSiD9nnoJ0ZS*qmxqgo~^Q`UR!Tjwz;pyZw|jvHsb90!*Pb$f!{&yON=hCMy0_L zC4p^75;v!5Y-?19(8zE!PVbaLV2co2ip0)jc4F=C53XJR_lMyKz=^GOX~ErJb`D4o z>!8^UmAKjA;~XlrVe7=e?5+)h$p~V{HT!kUiz~l-J8|ECG%;{Pw+6wEK!|nO>}~lG zCMDcB{BUBRI1qV0grLnn7o*|t6;|iQ>rV#$U5DL1$`$8?%T!=>ZU}?~E;*xZ#2xA7 zTxRQ&=(~&%8?xE2Wi;l&gz0g{r!1FT_9@Ixoc!+XdEIb)WVW$#GJ?}(G&mGW?8$Fd zGBzxM(_$~Q^1Ioa!aA|cp8U3uzQ%{(bQ!HwgX{O?M?qQwOG(`9#My|sw>=VPI7T?{ zTvwcuQe}pn3YE@}3QOQ8?CF;E!RNs9r$Rrx9~I8?Ay@~a?cUDsAh)L7+uz%9WyAvw zBjmj^XC@epyJQ0^$BB*XCtojNgxL4JlLN2iZH?Uugm|v!_nZ-|TdAT(1|AITgPlQy8i76>0T$MsrOo)uf{iKUL~vNCHbq+?@Te5#xrO3avrw zN#G|EH>VTq7Gxca@ar(>f;|@8ZVO4^Tybd`t<*2qC+A*)l7Ekqv;B(gGxGa^uhR5{ z9`IBZq15p3mT9TGPlLM3jjUl`#&ofc@(@Aj|+PM1CV%kLg9 zz_3rI1qML+-a-(@?!#!DtSQ?$@Jxlq;UomM?=VMzjTm-S=RkDD#^EP@2%eD{?OJPH zAFLANa&X=y?)lRnu&aIKHaXX#A3Ye1&{5UpL_BJ@NF63!L0bsd&#OTF?P1!P+XiRV zruE8K_->ZKbFIY9{L0_a;A33YE58J!58{iP*Ci|^zk4h(D@xrIn1^rY6a+@F6n{s< zzi-|WBP_Jzci3z~*^A~1rPBp8jGFEuYD?uGPP1YR35V+5mdy6v_LiQho_ zbDs`I+lW`kG{^%-oXc0l?ZIn9M&rb9kGhGs9Ec6i-ydt`iV@6Qfw}hix`~G%{f-a8 zQd}lj=ws?8{t9UcoUX(T3kIWM@!DP~H0)IqQmIB6k(pQ_vcG;6{#zK{g@gpT8C&{KWL_w z|H@y34fFNJeQTOa{MA7VF8+SRM9`eK>cAwYI(1c9=eTAV;`F+iuD*~Ci z?8ief?lx~QI(I7O!LDd`M(n-4rE^Q;Ouf4P^A4kxs&Z5RgfuX?q`16Go{p1uv~g{FGjE3#?in{UeE$Z5-xg3KVc5yma&(w`O4{bez&IE)4Z@P8}gUxAJ;zS^z#a35zea#bZX#9=gArtXZ~XHfDHzLJ-DV4yRmex|-= z-g6G4CFhZzn8D?c_|VMPBV6cS`;TgyoNeq3jIi4oy9IYF(;lm?NE%(Hm&h+(UxL@$ zNPlaZJv07Al)R^}Z*Vr$Lm*Rmc8uM`65$4UGfEo(b$Q*F+CnTWZ^!(T$Oo$hV$+Hnflc? z(w&yIra4ax%G5p22_IyGp5^0P0+HoIuoUY9BimDrV)0#Yk-NsrRj;hs&N-BPu#}wi z;oR%6=D9Y^PUBIY&Y@UmbB)@JadC*N%aBF zQ_pAWt}kUcKOX4s42fT$?;JkaVKnSlcgE)(Knu$GE7rm3at`su^3}`;e-x)xX5ur> zh2{%%sQeUPu2!zv9&aqdjNn``8Y}%qD{`kJSDle7E{EKr+E{mi{&ww4R+-$owKx>G z-o3FTD#?q#Ixsfg@JF$ZC%;H`{vMsF$JfMdh=xO)Ate{+=EEnVb`NeEnSgVgVQ~Ef zS?culL!3!VGxg8`V||EgOO=ntLIy7?IAp=%fDBHudin&Xg0GdTr(eC;hS6PJCvj?yzqR5b|ec1AfAYmhERo&;^JY(9sdO z;wOwav|tA1YD4|qIYG3?a^#Ao1U)IdK#%%&hGQelOapBhqS1_S66blz>xuu3Dw)s* zxtfn$u@uj05`W>POdZUZ7JPUA6?wCfE7b5M2qQJEXA9>8^y4e531Z!#{Voshzdl6A z5866a2WIL;pN?|I|K8bo^GU2xT+ZT=AOD>2>J{L2DrxKBbek5x&q*GZsrRkQfR4&b zXXmA(C-%EIiki~0)cJ$0ohMT>wZ2ObOZ(-;4@1cZ_)5-FoI}*kj5>KgqvYU5wo1+; zXZi*0EQ`+45007-9UndSX8cI#kP|6fyIk6@n?K|vJ&~#ZTsIPk1|5#aW8D)5vz3Kl zDIP1q7k0X#dnOfYcMHK%oI~)|OxP1|tZMS)xk(Fj%hj(soOc@`dH)BwIt3l&xAlx5 zHow);`7|+0uQ&w129y#`%C=0--HN)30)n3$Zh4;*xIIg6+c)3Rhuh?nqY|*Y8^$iL zg($zejq`SFmM+`jHHXnS71ryS1etMgqufF)f3ux4FCh#2@~=6J#@JrwtsJmHyXD|q zaXA=`^H&Q#X(}ZL&$?CerZEpW0iC7aNyvoXbFUVZ8hzx0Y#Bw7GYc{Gs%M;DuPo4i zm6_%wwdn2iPs-9u>SdzMe~s^!_Zf6lKsi|I$D{q73G)`{j&DwL?tY`6Q#?LP*Lzm1 zYL(j+d#46^`)2e+ZjWy_J>p!|B1_k~C)42^;?&FidAV`O)jD6UI43gKUUyHH-uR&8 zYHp2=3DH1=eF&CfeK?JKXmNaXAmG*7D#zXxPdg_AS$fpj*L)?v{*@!~)qwcOhv3pO z8k~LCzL013ytsae%ZNU;R1cS_;g2RhCTZh z=-{+jDES9!TYNOyBkX$zPrs7kG;5Hh<9?X$Fd7!&;y2{tL|FI@l!J9}y0Z42dq{~ay(%N5OnP1B!s20Xq%uc-_c=#=N3g(DZ}V`pA*PPa*R?(e@qFWoLX zajgz-%RK-cWuSu*OCBBS-1E=^-FPlM-4YtCx>r@m1AlZFJNv<^l`@<|2@CWO(f~RS zrPux}h`WAYl`tB)dIGs(sl-21ol=<3yZt`Hhp73*!FYH!4&!XNrGwMuo)0gA@9Vr5 zfOrfDPWSX9&pO?YEzntgXZv(KSfO!lCVFBkj3rz@^FpJY56&#mC4ZUi*mbq$iPrI` z*)VE2$nOLrI3=aZw>} z?!*d!5%TS%T4m`&Gcp}UgZcf9^xS{&t#%5zV#LyQi<~r!wYBPJx?f%ft&K5k zuKN0Nm7caFk`qSDn{8VSymgelzS^D$HnF}1P~1V3TKXSJ8;4lk@O1i?=jacut`fdyK5~*t=#cLmlHyo6u zzx)GzO|>s|U2a{xQNq3KM;NI|Lt8qNpUu*Xdn^RvyPAo)i9oFIAy{hf?lw-xVOhHP zN~f-KeWYKS*8q-zcv)cfcFS%q!jn?UTX94sd2!LPw)^=&bm&nR z!B4yuhaXWU_E_NmF?Jo`RTSTMvCxZ17tlvhItU4Qq2w)_Rm6q}h$tuu*bopbAkrd5 zs`Mt1M2aGahyr;*$y-PsQX*ZNbZOEQ8^s^xf9?zO_RJdi{`tQB7~bA9_fFYqcjipY zqOK8yD}|k*(Xogxk$;ce)*M(`pj!mtN}(T)jzxTl{5wunC8h^X?dcgoxKdarkB&uf zyL=SA?LfYjbqX|eRvLOgm{sfp>&{N8_JjYVSUlDSJLRA~{z~XM*Lr(S>iep+e9wBd zJI2@9GLqR*?R0QIdQO=^O1`O@$Wp8id+49*OjXzj}DgFK6Qqb^>(UVqQ3Ob@7DS{P1YG!dk`!oUYTV>Q|(gCWCS38e8;+*quhu4 z61msb?^x$IZcpt+tb6gLK;L!!A|>cbVSE?u!*OdUjR26AyXyr?HW?5>xKda+jP~Ka zL^mwy_$=e!go}3%iXixn+f#cHe8;*g#XYqzk$=aUN!*5}jn58_=PA8HU@7`V#qf!$5oGsP@Y3`+SLR7`qaw zuQig;COWE3bo)Ua!RSZ}7g8;pYYE5eOVo5|cdWZP%3~j2BL8kA@g3`KU*t>Vo{Yo# z9qTRv?M$>+^Cj}{`W@@86wVe#$5g&V{@qAID;iR*s9RFLL>Iwm)T<=)YEH+!R;0GO zy}mDz#|#`clK573cLd-|bkX|gBkK8y=x`A@7aJWXpjV^Ohhd=SbX3pjmO1KMbi4!q zAdRA;_ryhXxbdPVijEU~iEda@jy`9fcOSSECC$Af3v`GDlFABGMMi8#A$lZ~^ zK4_6we>vkU*YE60nhD|{98<1yMWh8-#qbayAXh$AzXMt49k8f!~! zruf}G6E33Qn9QhEj7C57*AKDqmN@V>Xesa9bFI6dfzXE0xy0UJ7Sd70gQ(GBLv$Yi zqjSmvK1fp@`5%{o>x1U9nOgexcoS;?UtkR&=H!B?KeI#Uxz^s-lkH!t4z^_V%iYN_ zXn!~VFRSbx>E4bu|JYMrr61m_-(K^3sYP_OVYIPV z2a5wCs~;@IRcxAD~?IzvAzVW;7>JK>cZpSM^~M1j^H

Jb`zMXx{KS>!T?gzhQUOpWVOS+GyrzbG&ZyMzo|~fOZx~v=>Xhx)E7n1}J@twZ94}iKO?l+bVz!s1THKozO;Zjc z>Z7MOj5yLG)04Xj!BR^;*b|k~5&R8uUiG)PM{}C<%`mEDKfgl8J{M50IJGRrx+NVh zQwX=a63)Gqs5kQYw8E&n*p;HSSMLK{4+mCOOVmnSu>?^WJP$92XW+$ zgVC0O(OL=OKW^?yIQK8O|64s8svRj_gc<>`KJ3N0bX%J8F0sdi+d*j`)m{^^3a6@& z@^6t2Ze_Ujkygp|r80uwFhW{B9ih^}R!KXryl?EGi5O*|PDNO`7{RtPItTG45dD!3 zwu&v07TopM3I;v{2fhQ7zinwhRQ~ASIeQeP^-;Dnf@AWO2O(HWzH_C|<5wd@+|rG% zUk$D3P5f$vkgryG#u)AKs}bV!q%E#rjnFx{`#$ojC-RC@F71`d1xDu}q9sT=IJJz% z?wz8${paCnxPb4R-$*N}QmgAo8hMxB_0hYdVj1bFa^|{#wxo9DD4UghQPHo6YPeoY zCN-^?j`5olzR~(9#D&R++}?n`dF)~7Q_Ampmq-Vs)HlIs+zt3boL_cwEJg0JRnmS$ z(?Mv--Dq!su-=yN2tZkzQy!QV<+7g0D}ubEQ82uo=&la>5;@H4jFDMUF6)WBBFJdb z!Rv|c>Yy)?!@SNInV;vfp2#bLj20cdp6IRu@`@m%1;Oiy?kXU!2r^m_Seb~fC%UVE zyducxlJyt*x1r2SdCHtuKbq`Gwm$kO)wX)b8re@{UiOawqLT-~QZm+dJ#pD{S_QkK z?tb8@yDZi6`W$Q9@KpQHmqo|Mdbjx#p<}*Bhs@~IYTcSS3#@y4q}pqC$qM9l-W#Wk8&u%TmL$0;{z8Mrl&d4g-zV)Mn|FzD#)NjXbx@A?C+ix%AVFrDUb ztzs#e8>*a^bl+@Ulvg%67X*<#c4VOWcYPcQyQ`VJvdL(X;&oAXmD88VVK*j0!R)|r!SGi(u%q( zsjw=Sbx~f~WVGl&%A@O|?g|~R)-hTTH(MjS*?L)|^@*;Fx~rMIvgy0Ux~RJX=}Y9W z81(9=+N-<%$x^IObc`=qFLb4G&I-M|n#rr2oYR81 z*}AB^vdL&c;EM{WFUnobN%I}eBKW}atFNRTHDT2GFoLS@LMo9qGA!*MAW##p6V`G`%`l${p$5N(W2vIPvB>SjzP%-%1Bd@tE=E zI#`Owj5kNHlskI4l~pXoW1pMrU@6y2dMh0)<@WlwLa-E%8E$SBOSxXsTj^jax7WWF zf~DME|5gaElo~VqCml)hp~zRu;{?~|nTw7nLgG^KGWzB^I9?t<{09M}3I# z`giWerT0X> zrSwroG_N8Ex4&cCIVP`FjNqQr)ki6h=-^)8mC{E5x>at!ty|^BZ${wzowdzQyK9?rlI_iplI^@1y|nZB`pNq` z4QB-6fEc%{wkapQEtxtqiY39FNO5_VY%4xKz4GEyr$ zPo`9MI9@(Q$7k?vtwUMD`V3>hg~Orb*_EBSvno6CMoDqJI%53qheQ7Y@ny1Zl^3C6 zGK>{>yb`z(xzr=Vm!L{hXF|B}ch^8MNcX_4(_9{0&N^apr?TnM zUEZWa&J2!7OSY%4?xX3$>c`ctflbhHv8Qg87B6u%7)JXY#R7R6$C%X`;l^9i!JG^_ z+rqqWuLiNk;9#vmO9xxUB?}+Rfy^k67weESilx3zw%crf&tf!6u;UMY+;8Z-4IRSV zu&P_KUE`4+5l)6<@FL$+xQ8XD4`(h;woeWbuKUFYiv{W<9k0ae>0sMACV2Hv6$`XO zs?&ksH(rF|oWOlWw@r092Pm%?&1u4og)JXR=(IW3bT-9mIs~EO6`l!|ChVc_^GL#T zke&e)zwsiJ{|l$Ku8#F%Z+qm9T(*SsN4M(FFM4J?orYVQ@bDW(@M%!Pn04l2#>lU6 z1M$)r%_`=|(Ah7B9XOgW>3EED=va)y@yZGL!poEGTD|*3N>K3x7{;n27c;6M{*E3U z9Ir4_oOw6dzBX2PwjWqoF>NqX{u*it=apB7TT2@4y_m58=_vdS(m`=~btpy;d{K>h ztdQAVdyfuAvp(!k{QU>N@Jw711nH17^<6gN6mFA#5tag#3t&R%k_^6)+D>}2+75pQ z5>}^|`OGlt?(4~+%aCr2hrczJ;uaOY?JDu1KZ-YS>fm7=QvT{xH0#6uo>!AY7ZLvg zj}At&KErr5+%w|~)T#L^^m@g0iS@xVI;m&I93ajC!L>xfs%9`6-i)g8p}coDaPsDo z4%L?EI;f8_TG9y|b51ZC970!i2KE4P zzefk7b;Q;qb$C!sEflr_EopY2qK){7mG*2KO5c}~kf~ADT$srVn17RgvKYLb8_@4eKJr7I>vbrY@x2>k%#W^H%7`ABjv0^ zP7|+Ko?_o;kFgkS7;*p1NjnO}?UGki`&dfOrEkOu=|01R>t)syC(7!`QmjKqJhJh$ ze2EWt$F~R*(mQDzl5IaP}ywa zL9mptlo+3YZ>yNxy72v*zl0_LQ34OY@gfwH5>6rf+&_qR#F5s4`wvEQ&BqDp4QGNc zppP0YJq~FXhLt~mOSaE!8ffW=xAs5kO#b3v;N*gX0Y*qzSt8siRWQ=O(nd!5N4{%r z9`GLv1sWX-aa>*|Q%;f|qBUp#<*Qk60NBp-(8s^1r!z0*j$Ot*_zUG+}`%IhR5$+wOFGmN?Iy+Z9 z2#%N0UmeqsE1ALg&7#;h3vxI9hu}Tn+NO%g^vbrHs%Q zan;QW{_0u_$5O(CP+(n(-RbqQ_-c1t|0a|L9Yp%~6z3bhsMo7H)7KPquB|TU@Eb;Onhc{+ zv+XPX!tR~FJSE5$a!k142s)NS$5oFGj+b-6F!DUz-+Av~fxw7E1+=zAzCqP~@VA0J z!mau{y@6OG2>Lp`2*rhE7|C}GbnVXe9SDBoMJVn*!>GS?Raz}rB`5FMDn`rRbJwbGV`itxjuhcxVFXKYnlP`;`rdyT z2w}kFcxBg%n)ge1#24T>G-NHK^68xkWpJ19-n_9ILeR=~of9zj)Xs#GKpgQPSc>%- z#%kP!ood&0E?KyZmQu);@ZNgEC{lD+!Z5_2i93rqF0T%EWnxY}-?FMmc_#A8n{t&} z)(r+I+~eK=_p$cCy{y7)GqG%n{Y%n^8uugDF~isd#2!fJJEkL;iA@mhd4n2pwz&iK zc1Yam0LR5rdfnZ4@RzhN(Hk^DTjI4!)eODNi?4mxdA&m0kgt5(kemhhr+$iEqs&l^ z;Z1&5%x_?>FI+cR3VF3n>J_J)(84pbZgz_OP+0i-s-?#UmG_kqYzgZI8)C*Fv+R`d z3GHzEDI+*u`MvrLj!wfU2gGn7PEH=5z*5Y3qa&)f9_)19-a4bgw$>S}om1;&l*4_E zH%3d05Gj>f){Q<2I!Z!EEsqY4m(e*R8O6}Vaj}%>{xe^yeYwvB$%!@U5rMu$oPkq-#|wk532$}k%DF8xqH4g2jPyVZFFz#|KuQ$=!D)-oKKweWfZ z^M=Xq|5-1|6`@um7>)is=|sY|gE3A6JUoMBHZvX3!jTdvd78i*?M7o$8pv0&`cdRD08< zX&O_V&O!I=!;?d+UZ`WX_aInGIOxg`OtnXEn2x&3D_&k5icdyo{852o5r z^v>bZQ)t}NaP5zv@WX}Ct1*Htk>3^CFm|@@7w9*so)er{&*3-BStGx@I(}+7IrKlI zyc8ae*PC+1-Deo*FLw=HhySA#9!5x5N$Ko+k00t9+6#|NS3JV9*AV+#ALuZ{Vkw<{ z@9(o+LuGnDXcqG5@FG-9xMi$==}@tvG3JaSF`5pJSGe>(ACqb~{8sKzm^h$xs3H)9 zfZ#V?gyIK8@BHht8N0)=rXLR@n4?eU())8xsrb_vAzwkS#(5=-K;LJk+5@Z1j?_xG z1n=?x7Iy*&1Kc}|;Cy2=Zayi$Jm{cRs0JM@C5%8{Z%nnrb7xyRV%@9#0_P{xbM8P* z;ab8{Tw}lq_)5RPZ$R7+1WQR+@gM1k*UDRgL*e>PxgT)T2BnsnjTmhhCGNHY&f5CU zK0J)zbTHa5W;RNQ+lZ8dsaZ=omkp!gUFsf+aM0BSf~ABD>AfwfcIDq^TRLL(OEZGf zQp-vaBV;wQ&&R2D<1I5RM&lbq%6~>0O5@?UB&>Lx^mKf(>vBTlFJqi{7R6`?W@utG z_U`_6IbqYn80UGQSO;6eXzb8?wLwDtJ+aQ3z3>}QYHJbmla8=%+a2hOcDJFYrRFwR zZ+D+~t$oI;S+V9_YxVDgzX{Gad{GBaC0yPaV=lnMZ>$mjYpj<#C*XpDdjl8#s$%x_AXuMn z)!*6Uf+?uG!pg;YC9GV9x24+C^Mgx|>H=0dk6(<#Jk7yb77f8V7>&EOYBfkGfjU** zQ?FQx>ljX}_h^tX4btsU8(B)iYB#2iSZU-5y*MSt@lB4=5E@q)p>bo#=6t~iFvYyquekrr61` z!kYaX5aWOtDhPVxMYwFCtwV#%%Pq%ebil(SeP)Z~*$e#VZ3a7LmDUN*R&JfZ^8ubs z$SB-B6Zh@78NsthA_o1Yvn5(*tF65=<^{i$2k9Efj#D2a!4Fj_~<_}>TSpPv^E zl)}SO!bp1mv#EB?Zc`&xDXvj?s_V4zSGtF=y!%YJ1J2}?5rGXEATN+>;5?1w<(KxaGTqkGK z*`;xZ7+Za#x6 zavS_TQFq_RSIbs0nstNA;ezV|FJS>EQl@SBDrKxewBO7doax2fvZ9I$z7Z zPtGc5BixAi3hsksgsec`1{Tsk%FOWag-ajxD*C8ao<55ECAJW|kk-BHoXyuZusv_v z0KZ{`tU0OE(uUFLvD?$Gqqd(!${8V2t=gvAKR2Bku}bBeVYJ&)D%c5#5oj4W9gN^K z8OD!Ttv!HL7dkdF#BU_5P8M^%8AjJxr<5)8ztHTNn_PcRE z2VX@ACLtYci5H=A8oOTF+h#}Tc;M=(5KBo|on2uIZ=SoHqpTaAhT3sI(2*3Qg$oC~-;8|#S&6YHAy;^DX?tSr%4vs2({m}1v83waPMC99FIPfWEt6crY8 zS>=2eh>wBbH(rEd6EKXAI#%$X#mdCLsEKTqcpWPIm}-B#a<(UT`^`NQh(oFiA@!Vc zMsP0REb^Q)ffyj30)nL^yzB;ZppF=69ZuT|tBPQ~gstKoVvvr-3 zcsPZ^C!%~sI&ai{t?r9aUNT0og^b3|n%ZYV`(f2Mk5z1moGe#!@GrOeh07>fvE2uMTxP8<;{y&W;n$C|0C2gz%y$53jDH?t`|o5%+fVYj!%o zI+z(v!YbbkqhtKmwBFDmUSuyqNwGeh?#~((c&mL~=VecMa2ZIhD_;oC)~+8FI1cGE z?cigfQj@Uq{4g4RXWV~&^xM;+N#DTpLkO`%d3zY0bN_iZ5R-F3sJvn{e0N{JYwkMH zHni$^+YqOa*~N532_SAe**3H?7liUhG8*^Yp*$L+Jl^z_hvc*>XD%CX(_ZWDEQP-o zH(Lj|q*zKAPpU^fiuIF^#wlMaBScDhdpUjBEpolPQwH%r3_V_iikHzCS$1pa&yQLn z=TbQ?mXegKT)^tbuDX8ZePt){o|Z$R9E zho!t+NG?J=Im$E2I@l_X3A^;39&EO|+B%`fmDUOTM$pQ)$@)YCb~_=jgzJS7tb@_` z&NBu%li(BH3NJF}m4sD!FxoK60C63Nm+|l$FT%YoP1|1rmJ8LA3Hycm&Y2|-w=VGh z5<&<&kouw+?b-E02#!~0L)7+{fEPpcWGuy8ik>|zq{E9)F?n{R5JKWrQvJcVq;df_ zubwn+2ciD zs_-+Yv9E;P@vgSVixAv?=-h_d9xp<$6zlWs*dj~32sOU;>rmORzh z>N>R91R=aS)F=}ivOG5+1anwQSmm^5Zs0*EDYnqFn~G{X*KlrwJ-ex>1f`tS41&>~ z`3E7moEhzze^6bL998`XqdoHvLP%;=+sWuPBLquv zPJ3ovRL)+j-15-oAB12XjP}ewC`Y{rcm5&ss24GhYL0mn&weDV{J{D=>Z#BmXC_9k zS7h``vqlN4cxAq$JOUoSGmTz&Tq2_kr4Kyb-@I#f-j4PBZEXW=mCSL~3O!q>BUnnp zYUN)?9A7&sukxfvO_$Np_9}q&k^rZJ^>LaEqc+xL)_;3CuoeiGlCZKwM>OhxcbrjWi$|X?#h?}#BLy1 zO2TR_Tt|E`p+XSe_M8=Zsz01>924%qLGCs|?v6t4@*76zxjU-wnb3Ji7Y^wNmSPJH z+_!ud8Jfs*AX{*gAi8kGz43yTeYFJ?Yy4%bf||%2TO?#r7vgIpc(S2vv^-I=TxO8 zVb#{^s|Iq$q>=Kz5<=$vstg!y7@AL*Rwj53$hvdd_Nc#>+IDWw8LfL?IWCrxd9*4g zkN1_*!BU(ikJpmsuWXf|RnKs91WU0#kB66XR4h@oPtRS=UrQ;Jl^fOW>gmvY!lc8C zQ2IO`UW!)`sxGmG9)B%aCG!E*_pv^Yzm^e{qk>kwkH=rj2$Eub9)B&RgBg1{O&-rE z*)Ho0s>jjOp?OA02P0Tt&R8@$TB7l%j3(9SMIVc5qa_-V$+#^#TGANCHNP{ZgL718 zmDl{vgkUL|d8rYO$L~yUya+W$@r->ik5V<`b7HQSnU`DU+W3leco8Zl&uEDdV!N`0 z(HNJo-B4%Jk^AdbSSGB?eSET zlpxf2M@MKQGD^9uu&D2Y^?62QgphJprN(H_h>WaaE>^bCGa@4d$IEEXh>UC(gengm z!LtdORjZyw!m0=0FIb;V@Jx}>Izk&QQOh7RFV#~sS|2U3l*~DAyRkP)M`$B`Dg*xR zI8C0}1f`ryP)BHU1477JhU%jjZ5Z14iV&O*MtjCrlvxv9prI44cxSjeBt}$FgQFnPv?Lnw< zBBMQHYO1dyr4XENQBQSbyvuR16qmoO$>{e9r(}O;=d9F#$>**$_Ub)Ln#unMWoO;^ zy=n3j@ilSoZWVSe%NvDgQ6C}J<-y7YjoJYes-3{ zXxxG^C?oxE+)MW7jr-_T%8x8=Z5^B(wyzDHZ86#~mM3>f!_EW;I~25Z)HwZ|wJbSo zKUFE&Vl>8Z!(Pg@%)i~&(JG%Bw%7ZUEk?udJhyuKZP0PtqvO@}FIoS78n&Hhk}XDK zU*Dnv@j;}#o+ssvdUv&cfK~k}CtHlh3QLhu!LiWM!K0&f>o=^VPr`P^gR?D0V^%c6 z@h3sYs~#O(ml!Rjtp0N9m&#rF)%_;GFB|=CPJMEJa1yK%7ATQYZ^jRqXgxC}(>_?b zpT#vFtX!!d#<$7`*3;W`M4mO1ti9ti?I%XOZ!y|1etyJCuY$FVEKixwd}E4rU=(z$ z>uWI@d(RJl96u8}p77{cF>9)|@FVDWxv#}&!-)B7lz%$%>M2iNRqHgv>YJ2l7n=p{ z&zR{}mm!(<q^#Pf7m2!P7)@K-%QnxJo6*{ha((%7%=UDSU%(Ul$ z!<5ltxskLT()bP|>2S-L+eBZY+m=-5I4yn_+QcW&CUP5fVaelGp4+qRN_~b}T*nOK zbj2CLuV7VU=wPYVHQQUCJeFmD@%so5Ld;U!n_BEz=XDg*jmVIsTaO*yQo9OQ% zZy1rgQc;Q$cJ1Xh(U<79kB_a(ze3$*$~u;8=wYpRCClzHXOzYI48tn1DQ+s#0oGoV z5hb5^&pJOb%P#xMSc}m(mGV|n8cql~I4`3iUcL0b_1YI%_Q)SUwipej+hx=JxW&xr z?m;Yhd60E&Tb6zPl?fK3;U7Fw!ms)$wo1aPmys`O_<>COb(~bed4s&;(^8@15s!}6 zEyi1Ce#o-dEgx^OKJYDVtPy+zh(R|H9IyJ$bshcpZi%}MWpNyJi>+d*nJXt*MMq@W zm(#|;suG)eMc4Xf#!RvHZOydTwMnvAiqVEqXL?FQrGpViD$lUKdNIrX^}$gZ8mE_E zZP0KH(s92h<*b8q0jFhGmiL2m&;j?L`OccD)>QQL+34vt9b_$TGJ7vGJrO$QLkA;v zpP6QTx-QdhmZYN%<67e0_|nj^6*`von_;~X&a@ND=~9d~j0L;)rlE#7A9xTY=YL{d zUz2Iy{^KAkTz!`HYx^ua^(Wy^9B&UyuL49C5F={Nvd*r~w3jc`r5KHI;$8Rn1rhcj zIzBbqx)^-=nG$1t9s`lGFG8o&AYilyAP zWY?%3aq7N1ZqvF$93!&2eP?+P*_k8LWM`)I z2$EMSwMUL8Ssx{Z?cObCS>xM|x6WeBxU1w$l;FKbC;R1OG45a1bj)n>j&Z6+?U&oid4qDPr_$se-bUy@_*<(19Si@-vKN0k$A!?>eM`}C^lYctW; z&MZE|%CiDJ*|c$1iw?7_)}LhAar?$vXD?5+P9M&))7wr&I`WKvK9~v}k3a`Yb$n!M zRELg8{_cgicp#Pl@yn)UtMHU8d*?@EEk z;bOfDIv61}WA3c5?KhGwMq}>0G(H{dunt&aHAIv8$yPFEEWew{k$SAqSois9M7$aS zunsBnbMwRY;VH=$qopm-+eEkRb6W|K3dXLtleQX*?N0vtNFPg_zfc-y0G4EWiQAD!EYGRx5)}`47*V&&M*G3gw?ORff7M?DbvJ~1C3&r+xF1|O z=@H9V7q;(8lQn=>4o*)05p7BN8>=4*!BVgK!27ZpEo!Y#fw)xgAAcq2kh|hp2TR>C z<|(V=&al0EmLS5v1mo5Nai0glQWt)1ZO!{RY&SnIh!5NL3Sz$LOhj#G9W1r=d^_vd z!La>YtkkOxD_TdmiraZ~uvAQ*=dDt|gzeW}6T~yWtc+U=t15XAES0eG1uN-j*e*Rt z5Jm3jm;N>K%J0c5mTEEdC2PX5uzl}%LHv40vG`+XuWq|BuT{CrQlB4v-MV-Ht1b5n zqWG{t`g73nu}23>ovrnjb^G41-Ex=YmG_HsYlB<Z2P5l>A4{I4I|#Se?&Sw z2uYy9Zx|tAbsuE3O^hHKpLFA5#HsjA|FtTD;J6sUG3h!uE=I(R-lXSM5){Z2{mvzP z;^$4#GWh=?&X?E{ZE=RL+kB%fxlyl@5T!htj;$|liMDEtj-*4R<SA!~fBQiJ7+3z;8tqeAiqX1NPuAZST?J-zu81Vj!3dVR_RBVVYrXOA7bVj2XeGF{ z=H_T?z;#S7L9VZ>`)!W4IE=nIf@>e6(X)N@pc(U8BS);_afv(P@Fluok>Z=J`Hef` z@FKGm$yOi{9n0XDjvC64x$?oAFSTCf7 z?L%LtSUfsM%RFgAl)FvzDFpX@y}x+Rid!4D2Ywq(A=jBrE2tux*x0fsWzn8-s^1F3IN^m$1~Va?`AH;jn#ridgka zwSXThI8G0b4wgDHV1~7FSJ?jio1Fe!zY*HaQSQTiiQH>P?yrsB2+eQYp4yAR9l3$z zb^RhG=t|)%TeJ^HnbQaW^PH0P0_q-KR|-uPzI`_1l`=zR#EFhT+|$zSc8PP)9Ht zyM*Z0Y1a~t*O#d2&~AiwbCkzEzC`}rNa7oz-M+||$UPZ{BVSbXMrap-6J60>4S7X< zV&tn0wJ!Niq&!?Hv>MSdl`oNhHmRhqT-4TKKei_gPJIh)MFQcHSFj(!I#LtgZFH3MMw3VZkgk5`lwaDME)JQh!*q# zw4|cLjn^>TafvU{4MQl&Knoi2{@_3A`;Z$e-7upiUh&aiyU|js6=6ZBu;wunz>UA0 zahB^P^Cfavp$(%?%EPhX_Ho2Aj<;CiLTlKUnfBZ({ViR`8&xj_i(_`TGcvn#EvfPD zVyhZ_Aro69S&Rk;U5QJv?#i}{n16AVwSQ`+9eSjnWgK2=-PIt=zVEe<(Bn9r^TZv2 zRd;%#esI|JhQH*37(QXOwQ_W({ld|{mZr~mvdXw1R%#ur*lH&lmE%IpG+NMrEi^U{rF6leQ@qbi_y5j zrT6Y-s_ya|M$FfIEG=i&^UGRBWt92we->Kj5rfUQ&ED9 z*mUU&Yj3|Sd+E{97NZSg+}+>%--lH_Vb%N=Us@GrAb-}4wHU!Mfj{w`*Zf_P@}WpM z*HH<7eInD&_sxf%@1yDTV*Z^#+Pom^)s} z1?*4EXQm^coO7PMI=gY5^#FE)4ZJkoVsuW+5b14D8|8oQUIX4=z^%kn&+lAO2-5pJ zI(E<981<#HKD3E3<^1J<*aSqq)tjsqYcuUS4F_BMF8yc~+JJ9x7t%p!-21w!ak_(a z2>Ul9VoGnds-c$nz8P#u_&`>ceR8POl5eb!;%|oz+!bfC)bxJat5zS7-ju70>b( zbHU^%)09V%)6x5h9oAy(BU`-&BN;+tiy0ZDc73HKt~LhVggP^FfR- z9E>zHALOfN=SBVJoBGYM{=k07>evs7nRedcei{3~18cHW$+!hk@4JrJ;LMHOyf_L7 zmOA^?!l++fNBpv-y1x#rS}A$OeG$Tk;3X+?>{Cncovj1k1YZMUFc2JX%4MfVjq0Bj6$pz)3Wi;+FncX)1AZmMl)ON8-dG93` zloy%N*mE~;h(8fJ=6iH7T1U+8y{)10hHH63dA0lgZ>-E%c>5+zi?j^U`Jfroc%5~k zK-lhkY_c`~>$O&mX<>W87vNS$c*z}M`{IPjK>T{>Se%SrF>Wxk!t1T+MZ)$+_f63d z920t+=MSb~H0EGbX2!JMXl*ElcoTI5$Aq&0>s$C&K?fN4%%>V=Td{YA?RJmq2#yJB z#=TFazls&eB(zr=;!w6V4CohH>S%cTMsJFf-MG&~$9$3sblPgI${)7(ZPRlBCHPvZ zzaS6?fMBV!p&izV+roCAcfAPtZq@iolPb1P%UzYzhVf39g=rXJI2dV|C1Q44AD_V< zxN8%w`Gnw@P!r!io~CwY^0z&F%vaV&Q&E~JxG_X4^M?mbSuXS1>nIOKXi`*T@STsp zExj2KpTjCfwEpaXbq+gg4j;n~7;3w4>-3sUv~$^Qabh;^wF>^6WjiM(Sd2D|w#$OS zF3=$(eb&Lbz-a9EnLccVyHnQH!Fk1KDUF-AqORA$jY$wU^G&dKf!kd{uy4ZkH24xZ z%sz|A3QI1&3HB~9T6C~)!u2%x5;@F13&Y6WH^JToMvD&iO}HKf_AW455bT?9Jqqky zV6-6EH{p5|*t@{!tdXt#1JRbe;itR{3}=x1fxXw{q51n0CMZsyH!{Sk~71iq-KH^cQous`Bf-VE0t!M+Jbi;kQ5CR|U0?x%?K zoVofYT6BsQ3 zwIa+T8JWGP8HtHGFId6_& zDYx&tl~pXoJ?G7Ju$1fJzm*P_a{InpAy~@o`)-BsN~xanKj}!44@JH~?k!z^eJ(np z2#HI@%jlcy;CQ(g{SN|0$scRy9zbu~_3zw`OK+*&e%Je*Gs2bPe)m=gw|~`JINd6i z;@;V9D{tsXiX&5kBe?%?^-*3$ba3D2 zO6mQeZk5}6=~lThd97ju_k*rJvI^Oy*DD@zxZ{UgAzUea1Q1Qfe{}GO!yQYo4moA| z3OwK$gL-PG?d5lM5)sk5y;_nN@xKyTcHcj+BK)7T`pYn5iNTD85puGW5h@JtYbU?C zI^{~}fjL(~f>3YxRO^3rG(!AXWzlqql(JpUN2>HmIt(K>t5}LHi~y394wmxjQ2H>h zMLN174Rb#IZ#v$1Jj-r!XrRSuis&%lS&FQ3a?05fK`1n4sSn4QUE;5WQvBCK{6@~l z<(Z7rRc{RS=qPs2TPxK0L5|mpaCLOO7Uxu~eKjz=?$rPzB%IrsVVoa?Rg+;=L0HA{ z@~J&N<&XY4&X0TOoRKI&)*)wu^L2;E^>UKMF=5uVBhFm#=GD-B-L7gn_?#c>%PGM! zsEJ#kN7F%PpE*s2@iHAOA>L z^deHu2ua81KV{jyhK}&0{AM~hUJ2j3B+EX2&v2|W9RKqDTy?btE}Rj1|#Y5 z#%Aqq7eimYelf(h%PGpsm$U3Sdq!D0qS;-~`4=JO2}n6x;zdOD8D*RMgSe^LIf)e( zM#$;z_E@u=co4oj)~8v;ZzQZvfa+EyrMLBiiNvXel_HK;&d2>xDQuT&Kh~q;w|vdb z;cp!eRqlB_#5%l)sJ=*5RIQJ_Ln>ZJL`X|tT$3Fo% zVZY^kmYt{hDD8Z{e)9fcv-g8fBOTMwzH?lR=A71bFoLaOH2i~Q3k9#D%uhoHzmfC# zui_+Y_d{bWUB|TAEuEOzy8(-4}zsw-+#-TOO4CiEy34@ z&I+XVt!>W6Bd7R7`!nsW1$t@B9m2`cd^>*st|wZCJ|D~q{0-^H`{@WlE6xF(_v8C& zL(*jD89V`+4vtrN0_<||rO)gO9lkQ%0|To*XugX_csQ~*X4)gNdqmhllqDWcj?uFM z_50K|N5V3Wmo3q`MGD>CJunCHU%&rB4Z%{nj@8+lS4=`Wp7ExGY?oX(cYl_>x*qoU z8Hc*`_74D}8xU-jum%*kE6Z;9P9IB8`Qjqo18rc@8=jQ2CCp4>7+cK}f$S-I@QFnHwnKS)aUp%zBXpSi4ur6cwYZjrEhQ>oml}wsXA9KLI}dJ?k>=uO4f@T}{`)JT+{gj?mr^FOK{k zWxSC-3O~t@+^%4E^2~5(OVw)57gee`{H-z12%`;SZJEiT%U$X?>pItQ z_*;92SVI^s*4*4G^=<1q*ece+@v^mGzbHOARIF$@|td;O*Niyaor$PYzW_>3xS%-9k;zN^*HE`+|Z=fNV z%Y@N#b8mcT7ZCU3;qODjin)c+hLPHAP{w(DO-(&t6n{mG27_|v_|WkiIyfDyPuDT7 z$)JpUi({SI(7|7-gca8d>qFK3+6*+_Uf=1wt-glfeb0=>*`eiT;B82+^B`mw^RZG{ z_V~JeEk@&3Ak?cPs8<(rsaL9I=qj{AGyisDu^I zh}1-vTciaLbAbqX5G8AGq{(m37L-7kB(Xu#|)q;|W^` zhNeer29^$PV19>(^NMqHKK7n}id`LfcMQcB)wIlTKXNv5-yI`ZiglwcX<0MyJ4&w> zN|B|!I$RE+`zM&8&O7QmPoXw)T+DvLX)=tgsb=UXYGHSv#4n$;C(AClW`reS#gW2j z_y!mH{n+c{e2lh)tztCm#`%&FW~db6ui??b@#=^%6}tsbV+MHzI|q0~&m(VPTY1zD z+w*$JZrrJFWcqE?#CfQREG4`+cL&0D)qBQSI-=OV!a>|*?%+mqtxe>*B)lg7>m0U& zuTOxE>v@}-pJ4{sr1$X&aer%CGyM2sNBv$=o=vlIcaLcy+GH2|xsZ_|b!4DdC#T$_6LPKeD^6kr|3>f-+x> zGG`quCBG{R!EE&K*MSM8YC20w)^zv{^JU2IF3aY>Ks*h^w;qJ>Wqew@b7r=9EG;w}(s$RWVG1|qBm2Yl z`&XxH%$ho9X3EoPp?N?Q_vqkwIVQuH()8=V0Hply(ls4UEi;AaDgUA9TcIVlkE_-X@-Gp=-sHAlZ3YchiQK6NI~3k5U%Q}IO! zCm`o8r-|!=VSKzJ-aiG1HXa1$52L|KaCt;92{rLG)I^pN#*;!-*4oY+vn?Glf9L4H zl_qtafeCfB++|i3&S_ZnmybUy~nCpct#5(nB zjRG$LvC@NJ9gH@Ngl>%jZvZh62$o`Y7#-0mI4WVp>{w^|tXK`f>@bWrjEAZ%&iL%5 zShK08CUWf)hS)Br!gk{=QrjozQp*%s2rJqWf?*YRPg+j6am zoNrtgz(e1*K>Q+n=S}gQvy|}rT&}&=o}NG1(h-;UcaDxl8NpUD+Atp3J}O}`>XZ+4 zi{oM`u49IgzH(GTdmtPjSW3cb7o(17XBG*ycr?cO>aiFN!K_1!HjI-Uil)`XSBqWP zS_uk2L-$H+?e&;dux`WHGw6XJPS80xNvD+{BRChZ%c*&h&}mqehKJ)~1Y2krd0G_- zWw(fNo<%xXO2UdUNk^<~*WO$`eod&%#5LO3hsRVr4nptzYg(O zQ9R~l=lfcV16AZNc- zuv0jL$DcgMVP-_I8!zc;c0D;HwBy7OEq8@c@a~txcCE)IM8>-+r*#BNF$W=+4OWDU zuJiB1dV3-GShxf^-#8}2=(csB*>m@*jFergGWZR18?ru}a0B8Hq&w$=Xg?@ykGpr0 z#b})KZ{OeS(z`$)KOT9bq?ip+M+^gE45aVQ1@YvwstNoqA_VzfLO$-|m6YPtY;Vo$ysMJ`B(}z(8 z>h8HG+XQw#q1RomuZ%W~cmLTJTLW#0@Z51+EF~O`6TpC4X_cuao72w}PTZGG52w@&6CqpI|GuDn$^G#C#{ zu_ZdSk(#X@RqqMXW_+Jdo64~f4}g()~@_l z=u*r3oECWa4YTJks}9&%Y8FpZXLvc~Y&*xKr<|p{DOZfNST%V4ac9tueW6v`_k}nv zMoZX5JOjiKAjao{P*R*Gcr5n}O}l`+I`8SDSReN*n14L*SLm)M?{j(~ulNn;5BENX z@m#gPLZyLd;6bnsM&pY@-F+4H_Do2xyPQ{Cd!!y>zKw5CW)s{}OIxev{laq8b_H%n z7&jGVzBYM)`O}xDGZuVxI)f1`#qB(NML^uO_;ki+i-4fGH1;Io;?1$@5Zc{*Xm`0c z(5%u{CYGZAIQ~L7qr0d7;68)V;DMYq&?zu>ZNj9F*Cy~AMu={8y3R0K1F>S<+Jt=` z1WQTy(b-s&xhf~z4u2P)))TeleQ6mecp{lhOOMhe`>E2Ujl>uRC>Sn>TdOV@Fxe|vLn0>&gw*M!(Amg2H8jD0|40C7?f zWUqu3=ar7wwxh7wxmFqH`IgeB6zO}Zm)@+N>hLP{d zmz^CazYLVZ!}%j&Rcew8>P)0z3?=fZY}qEfGV?{5~$1L>wf@f$Bf@%^GV7`0)=LbMFB>xJ`# z%Y#eSFeYBemp&B;?1In`Y$2l!<4=_NS(JAbJp4w&iswu(^WVOx9XQjYfpe{U0}a7v zuo!I^i(h`&-v{4dZ+wGniR6Nsd9iNH4JM6<-W2Oas2pW97&aEw4y3@UT6j1vmSPK0 z9-r0@+zIK|dcs#kmPlC5ymZ8b<9kAjE>|)0;bA`~`!->XIBOX z-{_s$USYOr{~`8~wH)F}dHxfxn2+13p*9bthB!~W2*oV~c4HuV0MR!wHN;ZPo}*h; z{kAt7wt`jD@SV>m509`8HB8B}+f^7A@t!D83Us`5G-LPi7_-#L7!4t4<-1|Dj^H<5 zgyKaqjA;$O3Dy0nhWY%?8YUwotfYi>=7@~bf#J2PwEAGt}O1+0?RK;)^cN)Ozzu@jGFKCAPS8u4F0mpHihvMo3sm=?qQ99xU#x zC{xP37Y`#?O4ynbGqdbkF9;vmleLRGqso>t|0-9?PsxAN5`;bLul+k|mr>@p4_mW}(VTC%>&nXG2Sb_zrZmkeVOvmMT^%v?n}z-zXyL#@ zqannS;kYrPoi);8G*+E=mo?A8^I8xO$Hh|eyQ(n}f35i|q+i3sQW92PT^&);yb!kz z>9`N+U<6wwd=Tyqh^$I4o0r-z4t2uA2$o`f7|HnKoRg3Lx80RCSoHupqujwuU7Tyn z=Lg28%@1%3r%7oXTl;`0zhZu1ga@JVZ2{5m1MuP;2B()%^Q$NQa^J*Gq?~mKLP>GU zXBg)TcJaT068sP)$WokFIwJ9x0cO@iM>1Zuk7V#0FGBIHVY~}O7a;z4_(%pzNm#L& zaT|=4)UPb3TIg(GgQvDLf@8vb@P)$uETp^&QqFIL(PR0gEc>DGD2q!IcL`+=be7G{ zNN6`dBY{)Sxy$Gr#15TJAEMt;Jf&!a+`ftjQU_ z;bAF`S2!nX4Gi1qg=7_Hew7h{OEc=4&1Tj$g6{kK%55RVIYL_ z?BRo%_R_|^{)134VV~tUdD1o@cPmQn5`y&!7h1V{k<$&|hgB~eek?GhM}4#MTlGz0 zB&>t`qnzD6A`FNsUY*3wq1TNQx7*`KJx+6QK5O2xF3Mzh{+zMCIo4wx6CAy`W1X!O;-k}wvC(s(o-#N;SUQ?Fqk>FuWMdatTUUu@*`=$^$B^J~u_Ea%MEv!pqJMJ^Na1=ZRNq zJ6yKR-6y{*21CQho4zOXVxFqbOGZ^k;!^Q4`u~qm@#h;=AHV`Z&Va1=QBhG*04{a>fz?oOFfrem4K}KV(Z>c|&2E@i(5UQLRjWx?I z8KL*;RyV(|Q$4~>b^ce#7w!@8I|nVf#XF zVXbSk<7a;Xd{M%;#Qh!jCBnU>Ry+-(Vt1ea5q!0&(za8|S)Z_3m3|WUpcNGU#F8`e z#&-i^01zz2mgtCnS*`rpK$Hi9rG)dUW$Ung{)O=oE-Y8arBCAgn}E2-gAmTE>(Ahh z+!^C7M&myEi*Ncr0HV93gW5h}!&Cdhg;!DSL&yCBPuEV^hSn`VS~*5=tHx+}k?Yn@ zcox$0fZ{h^gu5@i>*y!b6ayyfU`x2QGmIG%-pp8%9_wrm#yb3l5zG&17|j+H3I*E6 zm}A?-XjU-~pw94TJv`6f4|#V z`O>`_g4qdm-oY+^Ukt@Ux`YS8Qo=iU^m3*>Y{@%FN1647g0h!W&NQ%9!Xml2c9xxz z?}G@xsLD5-Hz;2}7z@O=9t6kBXmEb~b20QJEPA8Uy(UX}tx{Zy@bFH0Gb4yvvM!fe zqINFp?e4_)O9qyfk8yV06XS5%GIOY2ufBM@WMCGg8&`;NxTGYkm{l1KUXgFF1t#RH z=v>WTQA03uD5KGq3@RD83s%*|!&b3AT}RFSoioaWV$HmGxGiBeRf*}@QrNvWPTIr| z|0o#WDY5 zwle3J*%pc^y)DEDVLEN~Y?fW5P+FeYDVW$uQK zPUW_RSW3c*@0BeCpHs@y&NDl<1Zr*H65!e{VO7I*CeQYX=hI$*RinICfh}}(qb$3^ zqW%`^21D%W{^p|1MrigHL$gZwUVk5#WnZW^G=fkTVpkXtUjQ)%2$u3P#JY&Z<@~`d zuu8n)tX*tZR&h)@-oArq8SxhEs#r*Lo!6hg`E;Zq0eCC%d zyTgkkBlSv^6MWlyKZ)GhQ}0GCQEwQ*Wdlyx&)YaVzsL@S7iEX|4Rb&0td76oi#m_5 zDcPBg@gfvsb53o46W>RF zNjZHJjOJVi&qVzJPJ8E4M)T0649*`xsCYRS4C41D?qBJnjIyj}E^&i5&TEBhR_N^Lk z>4^1zwGS>tUIma>94~)^!nk@4=w0h%6#o5PO@p(6cpM0p;x9@^oP4pf{}Uiu0>R%P zr>=ddPP01wY5B&<&D=C9g`sg5e z@xArj<#;)#af)vD@zCrwm7LS7D>)o5^Kvo|C}u2=^a{?n@qKW42&ZWN>TB(fSArE7 z`KEm7=+)jE9vlV4i}KY{39{{s27k$bEg9QN#X9~nu@MiJiiO;pv`H{29P9;qT z=P2tlj4Jz&hgL$mD9VK6VjYYISMjR7ab;0UwxE{qSIg_)`WLnMy(I9IstP06+MG50zcMFhGzw47 zU@7mKzRNJ)?ChJ)?$KjIMaPZ}F+##hO6QdA_sq(m>}SBdQ%gD9&N*!u=T0|s3SK=I zy5q{ZkXK6a;p&Luu&O64S~g~^hF}YItM)?2L(q}mql2YHhtg*luU#AxR9xP&BB)je zd1Xp|Z&7)zJ>rGgD8Vla6$oxa%1a~VETyeLX$W2sW*ria@=GGDRwE7L(M^*A!8hwT z+q%_pI9}##m)~8+_YZ!Y6gUjTPq`q}iWj2|BdvJbz_zmW9j8ovhgY_k9+jru5`#J%Xo@S6Fk8ti7sS^r9z%3EGDf?Gp3Nb*Oj+ts$c8?cUr~`m~%j zp4i?wqiRO1ImR=im$8-_C-CeZyIvY~&KL^B86bGH#8UFRdsj;KwUU8Sn9+aWnbETp z&+c)%&H8&nh5o>4s^75|PO}sVE9QD;vssD<{gv_#kExhXont}^_tfo9 zc}Uj;ir;ted~IqR#Lf+;WWo>*AkYb#yB%lVjLc&^6dft ze}o$EG8$YK%O=GOd+Xbt5i*Zhd9;k%ptjXa_+Wpmvk?!!VFce|fc*@eY9@3&5bHeQ zL9mo=mDQnU!UK>V2NcIEVRa({qj3*z_@2-y)Q~4pQy9TDgVBaDAp4%sobwHw26!03 z<sNW+hl4=3*J<3EwFy}6} zj}get5Gr2g;?2%X?O9b5DD`>v`!s8X=jh=YjvE{EE*P> zXv&l1e+nVt*3X=a-g52gYuNSv6@Nho9!-Zg-sXS&VY~OGN_u_xc<7PB{a|*9s zaRxfBcy!EMGr{WJ=vVu-dq1=|P3X_lA5Fg(BeD$`k%=Z1S7Bq4wf4+O`-u_nTY^qJ zf6_i!S~#7$r<9I=4x_%0G3sN){4XY38;?V3U0;jQhEZ)wo%jhrwDTZBKToly{c_T7 z^D=HYA~g8O<~nh5cTLEXjw5eRvsxWIX;0kK$6_?rWVVm=D{d;bif!jy!2YYZmZuNG z-OI1vxO-WpJSJtj)n)%lJMXMM7V9&Nw|mX>FN0P1a1`z~UL1rLqnQtJX)ayVIJK31Saf)ZaCPuK^L^UHWy%Xk@nu+)V1Gp(PBr1`)2%#HZ( zN`CK~uxb>nVyU)cXIZ&{xH!&@Xz}OEL3KYTOI0cOskI88Hy<*gH(Lu0OolWztc4gASH^u6RRh{N~tHCPa5oR4M#dRZn!Tm>09{Zf0q5Ou=z3TlC zyprOPQ_`weLSHrMd~yL;h3HSX7iEN!%2lDBb>Xcve~Dhg2(&&+nPl9%Y^S*pEY)^d zGi&IQH2=~+gUEY!IpBg1+6#x2eQQTqZcm#FMQoch0>(c&o z|Ij5fEgi9c$%d~)$G=j#q z<{WJ?8nM)-d4n5(kUfwr6;rjH)h}HZnV$6GJ{`L^}tjdcIV&j_ONj+5T%KnRN<>!`8&MeFr5n1x-K<+5t{ z(H3zw5SRsMh^u8^vG)9#?*DJ_Y>Uz0bSl^``4|w`&7mO@+BLW4!IFl>=2(nIzk6h8 z$~?4^7f@Dg`>OTbtnZqo`%7P*XnB)6SbNW<`)dVcEu-S5M)4Bo9Ocr%<-_{W`}|Zi zd<&n_^*u?^&I$ zr27jVpY5uT{Xgta*#pmIzYinWTSkMmu4~2khOlZHJa4QBkBGUQ^#6uhfR*yel>oIb@=&oe@c@H(DC*| zadC=Cr_s!rmhVcMKkn#g4Z%6#4A#;S@j0R67wGsS&m8NMx6X@u9f|Knt%KYqb)|u9l6cCKcTGh zqOAA{BlYU3&TE^<5>Hcbovt0GDz2@U7c>nB|;Rhq6WsJR+?)MsVEJlM9uu;GG^U(1RtYX9q zU1wXP(4U_-=U9w3j0!c9gC6KO;nE>>{*#g6KRJ1h#b|j?_3_*px1Et%q!x|8;7>Xr zqk$X2Pt+(XQp>)-;O|;i?xJ%e_z5GbKm3P(O~XZ*AovL*o@?}nzoCC=CJ27QhNB^lWtPqGo8{&K(Jb$nU|Y1rB$rhz^?1$(R-NZE{0CXitat&%y%R5;Y zV>A3OAC|Imzk^QuaY{j?PUN}hui4_OOc4A;m9!xCy?)WZF?n4k2!6tduUB03pE#79 z34)(6qT|hr{)_8^nIQNHBa&bG%ipS5N+t+?!iYAX{sli=2yxKhCybD^y5lg?i-r-U z{C`LCV#MLef8T3W7{Pflf^*V!a9)gvKJ&L;R!LAGzevv@=>u_>B6aZpLmcaJInqaU zFMj!M?{l|ZB_T`sX*xE1eL2z(GCJ&sNXt({e3*JBv-I0Z*7_HYwqk~r>qZgwyMI?z1MzGZHk6pcY&ioyB0bIq zLf;?3t&h>z&pEWRxw>67TWsW+j59}xPIS^DwLh+u8TF$TM(`77j^ak(cE`xQ$c_}s zEi!w--ENVz=)jGhW{($N3G3h|&K$*!$a*D*`DLBvVT2=vI|CxKm*_+%Ejn<|sIOG$ ztuTV0ICB&?g6~Ckq|l#7W-rl+{P*yEslEz1JA`#OQqUclz2FYF2wJS-yM7%hM1Lc* zm*_<2?>DE%t)G?)Gvxok*^hs!m7LG^kyZD98U7P1=2|>EH;i}dRr1cmyhPR$8DY@a zH@sv|t9Y>tf4>}S_b!DF)1`yIWoMR|Xcf(w;jdXxZaIEz z_nW~U7|(mS#&ech*nEohQ*?&^%w)0ZgEl3Zg$I+6blPJp;w&sRTx2jL#i z)t!pc-qd&=Mr=?Be&USC(TV(b!{A$v9VzVdjEwrxiB9^i=aE~E`H3?kyAg93rzFFMglgJXflS|f>XId;ZD9l_}E&F`T$sdsuf#}dvrI#JUhEit58qEn(g_lZvA zzZ*$>%ds;SMJMt|#%cYQV+Vm~N@P?+S<$E#errSOt#zajpN!0@q7(V=MiP3_(4ZeX zhwHz2OR`% zg^$b=Fsjk)L%$cNaR$WFe95JcvQxY<0C7SCZ(Br>YT51d2@zp3h- z&=BFB1L%8Ac^_32Sr97Ckyw#Y|L)AQoak$GBBvD^dm!5suKE|sN+MXCZ=r$9tgfCk z|7SP*Sh|k9*MEvz2gGyX5y!D4=F1gUenjOa)Jd`!Z5R!24z8L3#GRc34kB04HP)>$ zY5wT%&DwkEtiRickF2&!Hd`s#p>J^?j8QSye+&u-gs|MO zA4A{SW%&`QzLkuqKA{a`+H7yNxc_49HMw*I=ey|Pjg}8_!mwCz$3hl10w^c$Y@4L(Cu?|6s(HhZk7u*QWV@UVw8f zTI}wT9^-*^-m5Ew(b-9w?do2{-Bw~to8gw>d|B`=ix;YIk44p-NWx!erHx97EWYEI4GvJ~qUs~j#1?n@+{c`xzukk^v)pgw*^ zSutX7**#Xtqi6kZ;f6f!AFv-5(1&HweMCuch*6xq=)QrZpORrat`vr)cE*;&c{9u*;31LQ`eS6wU^MQi+A}Hm9y}l5^4#zojnp8c!A-TcdhmH5mb(xM zLyt$^b4F`5i2re{l62;8aM3S1RYgk=w{|x|%^x&<#)$W0VmAX(0%gTfnW7&oi9{C< zrOdOABR*UK@nJA?*GLLp2I6n{adqsnNUV{i7%hGnMpbw&XWY48YDE2&^mePqBl@$`f%R4@ILQCSQT*j!BQMI!u)Mhla#S|2b<#^~&W8ChQ zk+#J84CCLPPo;n%&E5(>7}03;ug<8(eV5T@#^YY>+zH=l!3gPb)Vs}S?0We)&Wk(Z z!grrBLi$0~!Wk{~5gFBx*84=B$*2;!Q-hhWr1DxQ@BS1FucSKbqS1+*<`vfP_h2R~ zsk|1-Xwktdsm{7+bRwsDh1D=JUrFV)P)3UmUP*PE@*FqUB2&~CO zR#KhyPhJaUbj%|yl6RwB6>+sIUiW$Kz$|Ml;+)>f5+A(%Zl3se(U%NHU&5_~rKE3n zVvy6ntQYJ89lc#TSn3RRi!4vb@c-Rfbi7u6crXBj+!Mz-SgK6{WsWz%fMK8BJ`7`_*cgWhS{Nq(;XI(Tp zk<(7858mA^_zwI4bDidgLHY7K59Y6Ls#cqYb?W-zb!w;g;r>IuaUH9+7k?`_6gu8^ z>0qgfN!zX2h@U_9oUHH0rq&9M2cnq^!BR4_bV^q|&wNFg*L1lQ1hHrI2w&~5d)bn9 z)^T}Fm(e1{E5gqDZge82oty-b`HC>F=`vanydvzZ<@H@gi;nxP?>eis z`dV)|GMef7uCqd`ul4HdyUyBcbRwr6v{;46s9k4&-Ed#RYr33vYEuv!F}Es-{$0|X zud_nS`b3IX?pR8V3^Ecqf8X`&BJUXF-@-MA$R?Dw%c$r!bJ|7(#uS$R}r zDVbk8Bj@1U%`)w&S!#LuUK%+++%PuS6@9yVtYs!BS3q z_(3{Y${F<^gkUM2Gu+=QmU80557NO>&Zz$&1WP%i{(}&1DK%%f$B!g=QKT>7d4dz| z%|u58A$h5M8GU~poG;HG?m@sP`N!IM?9)dz{XKW`(#I%g#^HWz8R1CrjN?HFXJ)~D zyBon$JTGxZgS&{Nu;cKYrFi~u4?+My=`#yQN}pv!$|{U-#yhs1b8=h72p%~dedKvq z2ao!Wls*H{t#Za~-6|(1w^fYDe559;P+WSu;<=VHb9xZMk<#Zr5kKzH!E-HV7R5T` zlvyLhPgDDM4xa~CXUX^^8nSgpwIny#V6b(C*{)}t4xgkOEI3a zRgzZcQspeEDv>*g%=};}_9=`=QaV`5twZU%qhl!6`dVW@wMZ$?IbZe<%oJx<2Ww$o zvH*Utl$?+IBC?8rTZUmKH6e5cGB!0ll(f`mFzWO zOWX)mZhG(IZi%Xol2)mD3_&=1ow@gswCZu(2*bEvYghRSLcR0%>R>6>;UFGDoj-&+ zueAFgAA9bubH#B0zKlO>*kul0@zp(a#V2e88(vKJXQ@3><1)}$3SJwyIIavzH`P_T zjOLQob+A>ER+cc@Fuo|?!0s^furDzCux1roqB9|EYWh+*+J_rAP0mZC8sVI1$6r6O zbgNF5YhbsWbJ*A2rGxWjG)|cgp5>d{ySzCGKVcoIQZdcH@9WNCmXqc2NayV8;xDN( zYL>4hq?`0AZwh~bl9IIIMbQ~g7WJRyTlrymbAd|-OSu?Nnk|i!6VjjK9b~J7{h;y( zY5x8*dutZrRAc^*zN^nxHe>M(;YxDc#QXH_Y2 z^L^hlKs;KavThZzwdjZz(?9U`N4si|cEwf+U&595)BIJjpMmv(9p?3DZ&P?a0iJW| zx;_7SV48npC1IeMv9LnQi$KhDm6fK0%FQt1&$WtI+zGM3c5@{(nMZ)zF%c`BIVpDd=G-w+_WdgJ|EVItkx@7i<3gEp9|0 z&n2y-bUvA@ed;8v2ciIej9@9@lTqb{JzgF5Mf_j{m#FSXPJjQ@UsuH1_3+~-j9`un z#G`IDk8h85)z8(g7|rz$mj4bjs;ZXA_R3Dl8zs{GGrx4#x%Y=U3DZzcN8mXl+@7ml zHi)tOxFfZEsaW&<(mI0qc37W|;3sZ`V(tOk+p&Sk*oPBl9pds~wCwlW*DcLIu3~Sr z68`?+H%iiq1&aHw2P4#b$7t*=nYA@Ex_Bx3r)Nvq@6sMz_}vQj!8YJ% zq_>lKUl@&VoajlRw6`nRPvFOOD`~~t!f4$p*1`F*wK%K)oH*1mZpYyQX+H^#Y+;MN*6gYsCI`UrtBcZGg8{(?JN~2{4DI`>T8} z3<8_pjt`xGxQd-0Kdu92)#3Wbdj4ziq2iE!>yav2Um`t&;@)92&dQwcm--xfo^C7k z-iLb}M&sL{QG94Ubi4r_Y!&xDx{k3m`lnukj+*%KTPtbB*2DU+o9YYGmmBwHzK&Zx z8Noa}j5ds|pPRl#K;&~FxHn)lxV*DC_od42;n85Gh&njfl6}F&{z(hpmn!l4j}-V$p= z$5H6G2_2lTq!ptFqj5J!-Lk%01FD!U@ndh@W##ZK6~Jw@+t0+9rSM~fq?Huc4Q^!n z$PA^U&g!Dw*Z zKmAwuMx8J5p0gi}W^2K2Ji!co3mw~CR%f3%zvt?&xMc{7$JtQ2KiF=( z^jDtR<_LV(rglCZVyh&rxRCVk+Mk*=H~Wm=6xug#lZIgS6Gj`xxISk>y%!cUCod{y z3J=4Zk7W4sB~8(|qI4E0o&mTKil2em9rPK18!@4EhQHCIJKR(XjoUV1RTo$_$z>H= zB1}_jN@e&HuE-A4^>-0tfZ!)?gkrYBUQROTFh%8YPD|0Fhj>j47;DG4UDx4{uG=IInaztKf^tp=2N$ zx^!^9oD=q6)mrVF2_Gh*yx4YT*wFnrov%yidiKiZ*=&_f;kn9@nBm_wbGpW>rSo4g zg0-{loReXcE44E9M(bGfY|B_p2eV`e|JBR2GW^x|3J3PH=E~FtKzLi}2)2aL;N@(% zC%6rD4yH}5KG;Go5peeJU**L*wY^MwAF2;w3c21t!*92rZRvgbU>+7mV;oE# znQ$J`v9ol9@UWa7k>Q_~V~)jW!^p9ui0{qctC)*_t73BbFx#6ju^qy$$tkZ&Yk&2B zD^tg#wa-UuX9Tx!`MYA5GmO@Ai})%)N6k~Z4whnl*z>xoh;KTi|AAGkouzbsxSxh+ zF+V6?#60&*5e>moI*(VbfmzJHC5xD&N)$1LMXoKziDeh3YAkZhE{Ct8Az92)&lWMC zb|Kh8UB|UGp9NFUcERSUl@-?qw*{X}oy%@Vt-53qQ+&hfMOy>1`$!eE3TOuq4&HH8ecfLH^V%>(Zt$)ejKk)oK zdN?j!Mrb)1g=^;xWr0<5T~;xIEd*CsV%|^>Al?Fkr6jF*gLK4?B`<~I8x*yzmx^i# z=1yR=VSE`Ep6UP(yE0t$@ z@^{6oi<^OfhbVrGfOoq=2@m;%=)~u**0xZXi@s05YL3$g5mKt zSn|r%8y}t_D>h7=+FI4wY;tx`XvvvDA(oP~;)K+7e6+8hneEVq)Eqx-NM(dD?$!D^ z-9Pr}i58=A0<=o|RkaoY2EH?&=+eb1bcJj)k8HN8he*()|bG#)C)L z(PtR#Vb$Wp8&WSF*^tUlmDhB4;7`gWc}M?+}`T?l6D(foK&>8cWCeV8ZyI5-)+ z2{z90YGuVfX=9gBr_`m;^BAN4=Nh9Jp?fZ^1S>Pd*4A57m%2tA9%UE})`(gC>^kGNC7c_xErFjfLUgN>nwY}@vGkK|3Bz3omXdTF z&MPjwH3?;vb5@54J3i}RDJ~J5%ji=*D67sd!*hP3A;7mZ{FMy<*Ar(zN8S_T<6Z$` z>fN$Z+GTVufOmV{H&ZR)UUCp!`!qKPCu|C3GdG1;2TO6?7{)0eHUY8ec_1h+Nh{_j z9ntHh)^^hA6~1C;SNPa=NvjeS3)QJs+_8Qzk2$$q0sGss1#H&AEKsZuUlX;Gyf__g zCrIB;-pa|ae0?(f(dA~lJa2y}kGZ)_0sB{%4o0v(?8|?#wVg9ymHA7prKWFy9 zeU1Et5jxZDx4UbG8bSIg7lNf^#nY;r;eS1!w5v0FYla3wdJ1lLGzYWS_Wh5OGHh?nDhfftJEgB(3vMes|o7Sm{ zJq|z4i>25?d`H2Le<8gQzOa;})wi0Ccx~R^)P1<8aU$-7WCWj4WHcByPX8GC_Ih#i z!j0l4$7m(3qPoJp^V7>|{uzIEM=RO7qlkAe%Bld$iV-ZuF&$k8OSyF@b|9Rr>HVI! zIdllOIwORwC2dBUf8=w0EnUad4wr)1nP6jwf`$+lpuRoR{7(+)Z7~}AeY|n@(fYT2 zo9o^7u@2@9Vl>!#mYcqzh%&r|C;)4hw7PFXk6?{l`jlDo(|mT5x%o5%Tf%7UhXdj{ zAQl3_Qj%8OJ33-*e4K66z3toN@HgZ+hiJS(%npPbT&k`O?FHg< zmk!RCb25xA<(4Ibo57xp(q*eyN*EoSh~o1@OrIAKvKL$tERj;NJC0@MdOXYfIG?$@a z6nwW%@LNRt3L)CZQtr}qT1mb6my*{5QNe`}OS<7Uhi29Yi_sWA;74otG0x=&+s>$Niwrt9BHKhCnoOAy`V%Z4P4` zJTVaC;OL_MU=sXj>*_^Wid!gl4jg+mSPG+>4bNGMJ=YP{(c0&srT-MTx3#M}=i0>R z-2Zsh4X_I9ubK|w{g~Gz-5=OG(qesv@yoG3Rp$Xw8wh^F2)#a*?CED$T9lfQYaxDA zCX!a{d(1O}=qnHxAYH_TU@6vzU3yjh=1Fgc?|=9SYe4i%81<)jw}f+G2~MRnO7G?3 z>pS)3-ni~4-49T@j1W$i59g%$Cok-)p$+5VCg*&CtGV}XWXDfY=QzPsJg z{PDLDKF{wXe8K|MVPBg6x1@m<^TSEGIlO}fQ~R6KCXGutG;NxglxZPU!obu&*!aOl0HhjCvuPy}p#Ja&v+h~CO_JKyJ zMfNvJ3&NL~#N) zUb0W-&F$&QZGadE1owU1M+qaCS}!w<4O_nP-T>kce1Gt}EvMqu-eXC(YK8f=A-kiW z$vBiyAV;kEQ&g;m5VYbK)tT{P8=v!iiT-LDdLP!|?tK&*`*+h1CFF;WlP(>cFI$MU zrF@4IhCq6=2Pm@Ljc|4qZ=60V^r^q1ech^P^ZUWxG83a=%&!#v<+CU&u*zwEFoJW! zZBV1W398a%tAr0g^|g#PjFO)nO6>t3z7X%p4@oQAbxy#`%MPV}0K{4sf~8m=zSX`z zl-dH)g@NKHZiG`GMH?24QJynGn7>r-qx%uHeWdS)S1Z~h@Z%?pV17uju~#@3TDd3M z9KSnSvr6zUEuVA(m+cN)^?n!ze+OOjS24x`aCoKG}QeUTBGxE5m+A;eoXelXfF zY5=hZh)J0sR1GrPFmkW%X~v#y5-N12Nr+34Sx9xno2bF+sKNJLH7Ioxsb}KP#QbW` zsW>&BOFgSv6HYia4`SU|p@&tiU{!%ktWx7ETZlW6I#l)MKr4}Pkn>_4f>3Dep>H_A z{BA|#)S=58r}7hFLsX-hu0s&1KpX^urQH09&Uh}Pni@G-hw$eq9h?)^^j{fZmb%#} z!S`>Y1b!lDHTJPS(cp}nj9?v%#?G_b9ql!kk)6kkjO&BnK`A$9X8F*69qj^;t_2h$ zSc*#;Bj@3YUNw{9CvK~p`pBQ!-=2t>>psk28NoUjjnyb1{sW>le*DCZP%Pbuynfc- z{$|Uh)F<)dc>;gq==_Ow8xOF};*AnseWp`o}LEj3tjTPn{> zcrK!U37-XG*|XbH>lfda%2Lc{sw0L>?PvboczeR6M%xn@AyO*R#Aw`c2t>0c+Y{oO zZBJk+Nvp_@j_CemYqL_!8u{3@3|5eg?I1--a$7)StYD>M{#G^ zUj-y$GH-WHU$$M(nO$75364j|T80y2$v9clHw`hC7ZB0lCvJoj$3f)BEQT1)Ktz8S zA!#Kg);ck-+wlBEmzBOc_^}@>#ibCA6`9XL$6CaT7{U4&Z5TDHBnBTwS^elLD`EKS z@&@*yS~D$1BQpBtYu?j9WN{(15+yWx(c`B>A8ssWFR+W*oG-HoYOICEvfxi%xkI`E zzPw}wQpKHw4e`q04F8z*vn~0%inkcXoT=Y>M*{Jd3&FNC+Ax|V?M=;!_{U}ZI4_ps zSP15c!}g|rk7!Ar04^BFr$e{lb&*ZQGz5FgXzY4d_ zl3vp;_}_)lFSjm)H0{J^$t8{V13He38Xd|9J&a&|x{eDOHSMH-FNDUrbZAnnFTDCu z6RRI@WA%eqJR}`qx+Qk&J8K!$PK^k)`E5jq5wd=vdNoF4C;5_U$>2z}_ec%W3JZVB za!z1l$B5GwBThrth{K}{qYa~Fy==+BCDVxO?;yrZXm-qXZ)AgYOv^JS|9t@xMkk6+QuGT-Huvu%LabfVsx9fI zn>hC8$c>(ym#jdkUX*jfzNJ_FX7F};XoHcV>2M>IKEs&U+;6@CM9Ev}A(oP~Vya^c z(RQ)6G-zsuul|$_A6un)j&&x*BaIny`Tr%~LVd`3yM}<&zZfn5Er`39$tQf*LPtaB zc*(7U@|ABKg~mN?pPuv0PL8&Btc$ieFJ^CLpK!zK8%4b8u3yee)-}{%RXX35BDQlKAkCwEW>B$UGy}^dD z7hiYB6Dyiey_jV)y)WU^I%hUq&n8Cz4jv7PgMKi`e!tH>gdC%Q(;we_`!KGFDF}w{a61TO8pMf zHc*VJk5Glx^B191chmXfrp4;@i!ZBNra(8RZRe-od4ZL5(*(rN_YH`p*3!S6Yv zb;OQ6+2Yiw#t3=0)oh8;m?ysUgjbDfEG1v8NAG@f=!o)IFWLD;hHu+i^wjhY@_WZV z8ES>ahZPphb4KfkYJL6}r}}o*F6$*~jAAr)w-r3*Mxj7$bY13tfaoc|y@zOJqy9RpMSr_)!CXj0B4FVjYYIcU#};agiAr zjUU`%xV_=@07jfW7;!GRMjRex7!6(ntgxhFo|ul=3_oE6mz!bSU114fg+)VXQq+!R z)yH9)lKvu6#7J6XB07@f{}e*fpEl1KI8jgbg0sGNa#f7QU6A%4E*g)`4oQYMuc~{*Uu=$|^e1NpBrCG`Jn_xttSb9jkVAvUcS2 z1Uk$gsp&I}f$K9z-T3pfhXFGwiu1GueFB8iys?Zezd**p%q=(6BzOB7>m({ zad6*DL3NvlW>xF{*13QuaG}k3i_!QhD)_Tk>Rj3tBg&jkvNkpI1U{QL(ON}loX-5L z|2kQ%J?=uBUp&sL|6NqzOt*p7kv?Osk1s_9c6f)OjRiZlS(gilUt9>5VtrUWAKW`x z;y7TgGc~E$an`E5o!3O zXgq_6B743Hr-)DCjoLA?x5a4Ok8|D(VwP`XL@>*cB}LzyU=1to3DmDP+Ir*l@z#d+ zogtYd|9f^ z#@VHMIJ?xQ?@TLsb5x*q(IiWx+U52HKB_v>;+!zk-`vHk&h{}P|J7O6&eW(tj%))g zNf#;O2|RROSXH+CQaNrX{J`pj$x_QcnQKk^2A=ooZ`C|A$J$iZ6POb{28dietE5zc zA76#*W1jU_c~4+n+c6d+j`W#l{k=XaFg1L$T*GfRct67U(Ft==wwL|jQoya9H;1Pv zPAB$5(6gTM1k(F_qLl*f4{fv{vfqah{cg{&Mn2;StSmS>TviHg7&GUWihm4#`~W`~ z!R5nfe1DXz5wA*@%Zkf~(Kvk;e;@_tOTzbay30qE8>6Maxqna1eXY~`6fD0k4)5RH z`_;gKcDG07o?6nX=R+iJ`gdQ+uCR0PTr@I5NnKr4-|F;BRG>sJX^B-%WJ{Jk+rPOG zEY+xNbE{ewPhjbv!VdG_pB+*f;EkG&H;Q$z)R9Ap*2M{)z?3K%eV#hHH~wX`l4)oq zELGvv+SbLXp1`0bGc6tQ*}hMLPs6HRu!<2gv(>j|&hZ4c_nBoe8Z1y5FL=L$=LON) z8S%`E4XkJ9dji=n%(57bJzgLD7R-Lvb4D1W-m!9{U0uyN+B&(ik#%e_&_m?rtgDTS z#CL~|p3uQkMT70Fb+Mj6>6T-ywMXBuK3?Go?DEPTZZk>*;?*8umO7c##adg(6PSK{ zJjRcAt$oWqfdS75qR--^Uxmjfmmh2i`-B@X`tJx{g;jFeiLEM=(Zbq}d|xbv+qpD9 zzW8%m$|!jL1`uq?%o;tcH(S7u%M-1V@g1$>3q66F0rC7?(=WWEq2o1|=j<)(L)@Uu z$?%ug%RsQ_ZFjb^I;`{rw&IREM&n+~xGeG5scyG$A-H@PjTM&I-@O}wC+@xvK*8g)U8UP)Os|xW;dv*Q=j<(`F`hqBgl;T!-Y6ac7Nv}{0s}n(&tKD|tQK_h z_J&nmq^#&2Jc+Tk(ri!Qg9fu)@iUH={2TSQz-C9-7y9#Ah4Q0h`l-p8FkwI3{M;LLg%Zc2!v!3>1 z9vU7w`MqN)MjOWKJyygUsPnU^b9d=xndk{D%rR518{@{1S@B1qBe$yt%Y8G~y7H1I zPD!}Pm0aHs2+V&>mXbNu#1}n*L$6K1oO+)1&MHsf^khN&v3Oa^ zb9m3!p+49uX-mD)8+>>Tb%0i)EX4Wqi7$HVpmaY)=`v#QqM6oQjAXC&ogHq85wvmo zJv&oB*mg!^roa7hZ(Zn^108NZlbSU4v6P&1V2%;8j-3u zC1;>(S-FG9jo>GYn6xfuU`@kCnIQNHBOX4IGtkh#G!q0rVZ^c0{Dcw5tz3bmgR3$@@DoNfcrRCA{FybGAovL*hK$J-s2utNh?fuSPcDEu z--kMvvQ|<&ZuRVby1?3jLk3TsR{|x%8t>_ut2qm?7;tN(M+Y>01EC_$c=Tj=9 zwI{n~mMqmay1wpW>(8Rp1^Cd(ylHiu1^{1LHsxOy<<2`>D z?>XyWsXBvOSyjLH1d5Ln#LYh0zmjn<-Q@>MEj!iDO55!T3@#yvMP>gBUV!H}TxG>l z=gW1mI`706>tXTS{eC!g>(r)5wf!nrpk|A&GC}YYRsVuGel%BLWAeI85d4G@2eai4 zoH&%634)(6qI~(>fs5;cnIQNHBlb7R9ca}oB@+ZcVZ>+sa>EZ7LL4;s2_qz}?(K{8 zaACx#y-r??Xnj2stHKD*ixHfYu7mSpL{zandRZkwf&3zGt)yGl%@e7E{~zM_(|IEO z$7(yz-JaoYyGlZq^3!xQ$(=XSM=?6=he*p$Lwr%`;lTJ0SA=Vg(K>>kFe1HD-uv{T z{Dcvs8t1*Yrxv4(XL}*9yS+Y3Fcx zj*u+HXx*wwm-9weff=1CB1v>Gf~7jXlrONa(m3ah5^4EqHQ1zEo=9)NZA`C0Zm-`? z&lBl!7=3>Pw?0OPZ!9!dx2tB0RXodZ<`2<{PFke+24a5V%pcqc+*la7f!L8kxkY9c z(TPqvtmD1M%)T$Z61Iw;IP(WLf^Q&pqzuEEKj4;Qnq}xW5c3mf{@_OBiaq0tS^jR= zDn|Z6SIo6|K5rP63yq8a9#p&frTRWU&3b3ECvZN$jLzSE{ie4Ibo6%VV5wDuXIMEglUbI2XFSKtH+!BhTmBy5 z(sfFd$MEPx9<{^wj7Dzm<_l8Gq?&VR~8AWmH8;$J)`gi(7XB^ZKj0Pk7+c!fCHoO(q;aI}?Mki`Iw3~aK66Lv1 zbRz%VNaCA&ov|o7kw-F4hu^5k&Ako+yKW+*8p?{s#PEIFq4{^;T1N_BGm$w}bRz%V zNaEX?otlbHbP$Y2yGr7_ew|StxAoG@g40G)ouQQ@cl|mufapXAtZx|v6;BNH>|_S7w3B;^Tg;xCoP^sS`emK zrN$45J3DDci`1QecV4p55P?3de27jdtnt;ixAa!qR1>&^4}%k|BMPe-!ut`gSTp>v_ab)h|*;QOKtgJ znbiVu^~IM2@x({fQHR5VQO+cAZFi1C@G_o zZLNv%1Sb2yq^luv?c1F!dsx;2p*y)=Zb%Bc}G8m{JLd zAat@jgmiphLyR7~j zq5=!o^taCM-eDzK*g0}t5G{KAn*0KENECo;jZ1;+27EH7pNy{n#MeNulvX}k>7Hp< zC`H{a&t=8sqoIx26TXdW0v%F=>^XbOXw>;L$K%n`!%=n8Te{4uV-CFyq)J%MbG3$MXvpMRGuh%bStyzx700_wa< z)d89mqj96o?v#|n@Z%9!#)z0F_E|}2S5L0RzBQzeBD#2Xu+(6;U+0A5&K3|XHSwbZ zmbo)3@ZP>8>zfn%El<2B@Ww7_yG1@}y8a?`TygpFd)8gnNwkbL*~hzD$zvD4ipPqx zjg{wnJXhW_8eA4-9t)zS+i3Ck_@Qjq%c|9xG4aZC_JeKLJ}}D& z4_3bKqXJDw4Rm?__SY-CWq?Qpf~8jdaoB2gDk?B4c8Cj6`HQG(>NY`^iaBt^s{CJ6 z;G-rjexl{e z`lRhf?x{}V*^;DrF6!)C{JciictikfL;|!~M*6QittprR)Q-c=i!>X+EMwW;l!7@h zeq_-Rb$0HK%o7=H7(eWr?Cput1uKxIV{zy2t?M#R+}q1yG`{91u88dNWUc+lE1 z4?E8`^|ly|-EFg$Bo{(ieT#O*h($|(vX*}p6&UwTUyITBF1WfR2=3kRtv!rbHTxH9 z`VUcoDX$N*7_F5q{>PbLN!lq5!nIJr}E0k zjtktV(=9oEFAxQQU>%HLec0XBx_taH_+g?38NvB78sB5TB?Q+4fe}Hos?1lbBKtjb z#H*M8O2)oH8*@bs!J`bL;rWV6LG0oP@9|=(K6BSZ_EzeMS67Wsma$g$0WjjcwKlT< zhS6AkOdcCA5%&&2$XrcD+27x{z*OeF;l7nv8aJcYleF=v^}^C@V$McHMp`+oeQQydHPH93Bu4`KF{J7V;IG*JsQaox%69f#>svy{ zaOh}Ib&nPGt|!o_u8!cG@Kt2)t9J2Uob=A?Lj1bNsswb4209x1Ek?$tAa-sehHfq* zsq>+OR?9cByRD~QZqn~!b_qY^K6*{+;iFpV%KITDjqi`mYrWXvZDS5+7K}M!eff$f z@W<^5;GJ_2oD=T){d!APX9u0rJLi9PO=Q=cUguM$6q{?^syX99HoN&}i{5)~V8nP5h31;e^J`z3)DpjQ7vRd#E8|cK>AkT?f0{&WyJh z4KBT^+2c1rM_CtwOM%hYFFIpLa6EK0L+P?rTvm*h+K9|Dkk-dRCt~K5h#>A4SL4VS zkDP+wxSA6sLqsXEV~5d#$ULsbkugSxbx>T*iIPPpa+>39xaH_d4)e=8ch;#{%88gU zT6C;=9drgOaCT~^?n8&K9>%bx;c2cReqxQRXna9;QGR8g$X3yvmk+_-@4dcidqXofnH780Ioych?CqXQKqLS}) z^}gX&!ctDejM0MNxSA6U9;%ZKmEIN_XPEMjD^SIib+Ej5h2Q3JWt2t3J#BL&c zW*9Aq%;Rbt8Dq2{;CUpj=0tcn+QVo;+%Lv*zj#adt&POhoM;$F#vT+`b0S#LiJW$F z600(gd2yVJ(So>NT+NBA=rJ}ucIU*;Sc>(D4z$FOYKcxXjH6^+(t@~OT+NA$FA z&b}PB(2Zazo@J=l9paQqEqJ2kBrbXY6|rf~9y& zyuVc}#Utna5iI45eGjsVrJS+vK?t{$8aeN=DoI`xd82r=baswpq9cNkyi~r7zP}F6 zmq*ci5HL#qv34E-^uAqx&z-#Vp4u6A-S0Ug94Q`mAB1qmSG|YRtzs!2ow-j8Ba*_7 z!*iD6QS=^!0D#iRS4T=8Ya>>L5l*kqwsTH*J%_aX zqZjAPIk~N3MCRia=?MG5bX)wf!=>%^;|o{D;SKLHbzgA5yEA_2$jaFb4#yTXu-`;@p(5x zf>2NR4DY=98h5<=R2TQqiD1C-u!jB*3-wZvM zi68T`V1@11eir)_)?t*wEW^I*hw_{)5rjhHK9*W>=E3;ep^DzyA$}q!fIF4&1d6pE z?9%aYp)TuGoRjl)BOD!%-Hx+2SG?uhQ~s8Z5t3G|gjp&KqY$jJVO3{X#rg8NWZm<+ zX=B4{03+Zz>yUgeK8p3r8%Y-Dgn8%>apt%Vw?bz7Tbd3zGhDMa&O{C#9!5APy-~hN zOHsNeTBD|e*f!WF!>A9>S5~;?OTbS<5GMy)2n9>L_PlC0eb3>?Pq=(^zKk<*C%o$Y zV1(NbRh!uD2OY($-}L2i>EL`N{cjbl;uRW-wTz*Qi>3^P*RAo6untX%$_>oi2jff+ zN;cT}R){U(6WLswhH?H>b+bdeYoTkMu4xE4EuG%n6PW+wNQ=>kn4EYe<#p7@C#Z9_ z#Enq3X&CSCDi&V^e(ZrCj1WGNf^|KC3C9uVWPL`{Gu6%du4()O}^i1c|nhrN2qA&cSt2)=(74=cl$Ef#T z0M020?tLV!dK@}*JwX1vuq26Gyz0Href7tbp&))fuk4sn5iXBF0ZaK3s-tMsF zA73I+j9@7)L)7_SHSFBqU-4}_c*Vz07@>2$^v}~HWdK&_+hK*C5hB&330CWBkF>a? z!Gv(3hMjTbim&rwT?gmOXz)xd&X=N2fbtXJkSW^U6X^KsCoUah$~CYn%{}ZpHT$rS z5v)UJpg9SIG3T&vzYD=qtnc1B=UU@BcWQ9($FG=Ui|q5gTX>&OSYX~d6crfwefKbj z%z(e50-?hnx!7UG?)%%f;H{!&>YGJPMlgqr?942HnZZ|mJEJeD(``?L`;!sEk@6Mx z8brO@Ptym+cj)*NI_5(M=jGO+_-k-S)mKrzYu#ha{@r3U1n&{mcYl6ac#apNzbzxQ zW);~koGksoq*UVYhw$U%ia>lGw5y-dt{A~m>=RD+Z(SMu7&sVu*%D=!`b(Gz$%^626454h^uj4tV7Zzk4FXW+)7e*Z-LOlhhpqP*<)-s zLP;@e3|7w3633$@Hb6@h1|!w_n9YdmA2+hq?rRpQa5dr6a#s@=AxuiE3V^u-yYv{X zBUp;LmvqFnmH!JaM-4uY8e|<@&x^ovr1Tj^)7SgjXS-iY?cD8BDnAi~;y;qVJG@bA zU+rs;Ja;MKk&H_Tj9@82D_$g=rnuVIe7w`~)S?}ar?MZyYw_E2h^t*2Vll@>c=#}) z&`N}1gZ*HAoYNh|+dyCzfL7v^C?i-OcH@rv#``z+;#@{qv6R?8yAt?hR^72mS%`TF z{1^d0Dre${;=kg$fmIz_nmI#jLbX%Zgjl<uW==IWW9DRRDqX_t|8g$-{YIeN{9Q4EfoJ>GCiaE+-JyK= ziRWqrV6^;Qy(_pCu|*R*uFCFEHt+5bOL4wB;zmqXJ1eYu>}X-F_imY)iQc0sx= ze%!xGT5*su8aEuFFByP-AhK4R}j-6nRcS9kk~XYkm?cd;VT8^{HYertxM^tZ4bj>;FJYjG#cq+y zSw6A2g@U^+Hmki3`8RUu;C!WY8-kgnZ<`T#KMEXs$U7T4>brEX6zjvCNE1_hDMw1% zV_okDOYysc{nRBwzS57CHmm-xH1@YUwhKc~+xNkRQ+l+EN$0UKDZZW1aSZPvBP6Z3 zcXS;c<3he#`AVBjTskyyHZj5)<+6?gjCMT=#>+XDYw~SCt%0RjAJ$8L85e&E zGXVU;d@{-sMo4-`S5Kh955haRebEyswK0CohYtSE;JLc65lsNg(ZWu$bNO_U!Le)5 z664SkSxUGcuYBwYJX>h2r6W%L`&ERGjN6iM;glEy?xj|Dc%u@5PR%AdKHc8Xn+Kj_ zhnwCK!KCvAELqoWY?wDnwMv}f-8L>o7*BAPMMH4;2oH}!8^$Vp*N(+^?Nxl&vgeXk zY;W3kGzRp@3S^S7r<6$c1o9+p{9_2YO#)BDdE!lev>DVa7Fl*`qU|yg1aDX?6lByu$1Ul z78*uEi`BkQ9w}=N%3s#zC(QLCe|P-oZmsqeFHqLb<3b46OTDw6K+2+77NfC?wcnXg z4fyfTf?_6H;`T!^3ZiTqC5L`5RmPlLvW$jc=0Kf&Zv+s9OP4WU143B#9sxtkhqtC# zETyyW&3z>~R0xP!E*)-!$_d{%)mQs0c)sxQvNn6mEH}F6FJ$WyI`U9uGkf;RnpJFx z@b&!z2AW+nr=te<-RvCd48%V`@Dn#eF$x;SYYkSWo@^CscEFDj%mb)1{M~+ic5njv z64|T9^}#-ITQH0<2jjhyfoSYPa7i=TFgo8D9@!VpQo`@k{emZ8x1Vk4h%w)e@|nJh zcJ73VTIn)^*@3`ww`Y`Z0uUL`--}R8LtHml9n4=h%sbc-?+07O<;MC9qvzB|gLUEg z9C*%B!XZ@dFHhj)iP@Hp==xzbUm;Hw``3r6*!+ZfigZ89yjRV)GDj7BpX&(L!D!s* z*`S)Q4G@i>hozX=NJlicR;Kz|$C^G@E8&(Qtd4W{c>>k({SKDR#YKF<->aA}{#M20 zC(HvW9E~Tzw>0HdL5!`xGPOH&{N6HFL%4M)-btJopHsv)2s*C0bTGdp>%(clFN*j+ zI90_w0t9PkDV;eoYGfAk`ZGn$&iFBcrF5RV%R{o5)1ED2R>n`*@IFKTy6oaqjiZlQ z^6)MoH2{_pZmRZwdjivP z%(1vFfG^|N+uqgC;e`%HuvLr(ufWeE6WYvOF!>AT!L!=fb+#(CkdjdOOo^5eX_^#bM zI3*g0?Rd|*J{ZBJfXEN5ngFY|xU6CXTZrA!jq`@q)s3-D{8&oTitS5BtZ&xbTseMI zs2qMgFX1^A&rxtf8i>h|4vpIsVkt>0hFBf3Gp4n@sLV=VfBbk3Cuzk=F7r9XHV&3% zAl@sx()XDQ!BRW}G>le9d)kLzYV14zVq+gaVFdF-;+*i|lIGbLbDD?i=QMeqD2$|P zZpiGUh7su3%IuhZmG6JqR{24TWv8giJFupqViv7n6dqZCQc+RPDifU}3dpF+*^?m4R?AUtRW&|^YYG~v04L3sP zfcPUyN636vooQh-?kgIRH`E^O_U{*BY@QV}CnTdWPaKvv)D(!P@nZykK`4QS4X@j)3D_o-=j0SIMz476%3Du5qHy{xGfC%8n zQj%6Ysoe6xhFGP2ayxwE^v5?2zaQKxIVa2+_VqJ+?AwqUjUPV|&eVdlJb|%K;~sa? zXBZ2b_c051&FRb4C8tl=R41+R1b+E$vc=4*%(#kE7SJ){;D*$dM>eE#zAUBdXb&B4 zb<62%+B2t*5v-5V*t>hQff-frkngQJhkV?_u~m%5J)^~o#rMHknfGzhf<5Q5lDGEg zC!WB8I5}TiCP#_*sW^Gx5GU^$!BT83MrZU4F|``|iq_M62JQ_QjrrA;qp@;kG=wpV z^I|Dsi(NOz6Uf_L7<-2t=x=|LIx1nu#!(3@#X5Av`hH{LPN9`#Ln~n&ETxy#|Nh+R z{RUQ*gH`;55yF4vql)s?se_h@~X0_;7W^2dKdW)Zps6dJXc- zf@>4&yH|?E_r|E!#Wkw2g~FKi@=;JsM=#YYT#p5}z6tT5viQRwnsxL2oBFx-N=X(MTFHQ{W zP`bhNG}ee$yAv3}Iv9-=$S&Qz=VvGR$uRZ5`D>D^d*eY z@+CBQ&#A09FJ_Y0OE)!pJ#$mbrS?d@L7Fr;pTOALz$prEBFIao)KGR|} z_7}c8D7+5$GdyQ+MM@!9Hx|6sXZS9jh_=`K8f~)_GvnzxR_1*x8SLWrX1s$u>*IM4 z*NtK9KK+XQkhM3ovew=ZKXD_}Sz9n{l9B z;@8@D;s)x88{wSW-q`m2xP9nLp1>Q$p37;#1gv;Pzk?g0>FvV3OIx1^Gw>I8A=q0+ zV}<_f4BvBj2W{6o$Wr`1f_rB}hVNM*asa_nl2#m+I%2`qre5r34Bu1DmPq-iFM7@i zD@AjAM{aj?BUHID8nf4m5BdJ?6JxLYI7YKdq||9iwh-Tg6&~`{?;B%x$poRSVl)`c zW<`ZgV}Hrkp8Eb0ky3j~be6Pjbsvsb`%6Sh?MM;EEaSJRz`s{N2s5#%og;>kI5jHt z5%&6Ybm`!Hg;&klg|ueru;6;Mk_xDE*1=M&4{HF8pY&bJg7p$ZkG`@Lb9#Y`by-x% zk6kr;U>WD@w#wPJ6#wimaRp(Ouvsxec)Z47=TZF6A6l#rx2yd3cW74AqGocFqNeb` zsU2H_R{84uaGi}yp&$_LfnXhOgvtp!gi2NmJ_@VATcaUZA6tl>%Lo6ju`7Yose1p{ zHptdudtciqvXwAn-l5)m-EoshS)&r#RLT+&8H}~aE|RU1BqS~7oz&c`C`Hng5ZNlx z;FmTc%m4elocDguZS?ui=W{-;_dehAoc)~poacPcRgqt?gLM`&4oeBsSRQ=5>O3~W z))9T`_m16-)()0f4I#X9%J+xS@ZRZK*?bc9aRq|ZplS_V2lBhx8OIG^JqlZmH&yYf ze_qAoQe<8=MkD8U7q+haqKfxDP@Gnq}3>F<^5E}n*U=JEmd4T%uQz)*VHU*xsbk65R?*@VtwFGgpT*1 zBN{+(DKOeF+!@7U%AbLC2;YwKnP9YGlsNikw(_!Igz(}h4#CxOQWc; z3#t@ij%t6fBBEB{R$F&+8vkdLYetewXXJs+ukmd3=}a1+Xk5gf1ZVkz%N!x(!&V|F zEFJ-vyGuB>)Lsa_4PNQ#wT!HGN2jfJxenx6`J}Qwa2o>QLHdaRf~DlyufYyzS7Xqw zrl4K1j$pe|`oL{?-7{v<7aLkL<~Fp1HFv}(NzU!t`)QoJI-Bm0de4|UfUtoO2F!As zlbqGf2mA}6VuE*@nU=E}V+mMEH3aJuX3SIhNsiNWD8ALIZEIsD^>1uV@7LH8X5O*5 zf!N#8H_q#;;?+}CCa;Y-0Eiw1AQX?V&hcA1d?GRxZMPfRF6$6Z(Gx!os-eH0GV2TO6e z$*AjlY;k?av*O8>XT??h-w0J}jK&$0Dhu4hy|43%B5+MHFRT2n7;teHUCvJTwnDYM zK}Ic4;!^Q4T1VUsM0+4Q6@XCj>Zy9E^a6JuQq<#7Jyl#joI-F+lwaVcK6IUTEf6dv z&x(InM?@z_+y~1y@rG1xq9K@HmeH_U&M1I4w*{IZOO&GN6uav#r0^=8CU zH3YN8G8(NUH{#BQj%v`s`Cxszjt&EJ-J9yywQ{bn8|OAW*tNJ*ItPC2#JQ`p>0aGG z*F6qIegMHz!lwHaSnJwPnuNA{|6{rC6%FcII~vxt_)QSuvl}05|C~FmVMD7!lZKY? z?3MrrUB9Z&X&lHp|L&7OJOji4AXo?U`f^P0PrSCSdtyox>#G?}G#wnT@cq8`5YA9M z@f`9oBWr4UIrQ2cGQ#od9$bUU8&h9}?;q45RtAXg0tlWHg+)-I4Wn=6?ATW;GFd0wb8+n$cLd_bqQ; zSuMpY6i5}LbshDG-=911!&Ga9m#X!1=I@q#%Ty64NA^DC!VhbzKHMsMU< zv44xcayW0&^Q_DtEecgIy8scYloID8S8sv{#RQIfgzh+Rminct_17WzCsJJUtfX|- z>}9LZnOloj^Tq_qidzPk8}^FcIA^viQO(;{s+z}A@~rs6b;M`?w6}Iv%5x`H&2t$c zyxz|jhrdVnQE`@Tg~mNH?S|&4`&hUo1`(>|gD3TDdut?gd|xroWhr@9oaCHB>&#Hy%jHHV*Q_{hu@8MPit$&ErmcNsns>_GltlKcwNs^tZ7>Zi*E$Yv)vPOtRj^ zopTsPOJEe`H^QdMsj6e4CB8fXE%DL+wYTnTl4tghK3r(Kye1w^5!cZ&X~2))A+SPhu@mx=T>Hj1d0SYx}}mxfgg1NH=bY>3rUN{cu(5 z>YuB|dFWNDxaI4u{fXY^%^e4-T8j_r2$m8ZJ`a6^?9%Dr{q*ids#v@5;XXgQxby3= zsqt}F^-aU5eQ3D#VNxOU&tiqNoO5ouHZh6P}6xYmT- zwo*InZT5IJ-mX-sWB>fS7t`RA;lVdUD_ur#NuzXM@8r#WW3#*c_02B7VFq2D8+X`C z9XxC6R`Y{EkK$g$trAw}x|!argQub=^G`+j4I?;zID-X5_~%p6!9ekwAVS?LaG|vi zM{9o!t(|o+noHdd3iQ^lZRfV%{anIUF=!xRDU?l))Alm(Jl4{T8W8P z!trvC5*F$QfZo1Q*3>md-<{ZrV=3+*I^wRH&&I^=Bi`!Vqd2#WhF$dXvgQvNDc-tP zx=*UcvyPo6<)4buJzU1jgmevfvdV6c+7sfh6h`B8&Au{bOCY`w1n(#j!@Q2DZk#f| zY*5{M2!RpIFfYF=UtYsVNj_yxt6kkIg1}!t@;ve`oX-4vrp;)0Y~ACzQ?S$U*TFj4 zS5a`Mp$X2SZ&)S1oSR^QYz_-#y;BHeX$)E*mESYI?(Dj5=V?u>y;GZLE1BT;pnu!V z^fIT+L3OHogX`-#=RE28m{qoWn(`3huRH#tV>JBwR<*ZYT$AVSe=pDFH$j9dH}Lzc zXm3@G<+*tu|mWMW0r!a$S6nm z7LhilJVd0e;;{Po=42g zkI|?6DRWNk>eeIms>g3DP(D7)>@PX@J%rX3Ic3&wP~DneUq^5X84X)_u~X(BkUkF` zEG5q>?K8b}hOvK7g>-y%c{MTaay~R2q#Gv=>pqq(K1FiJ2qT1BTX|_Q8fUPU7Ini&)%OLY zO67x7h#j&c`RN>dB@*D0GqC@q0!*~uoD)i;q=;m+E zMj0Ves&_FOBSWdRu@GzkU&02!2u>BF4P#)PK{4fF%5NAU-0I3}7QUPry}WzouQJcS zxXR?Rl4mtKbBSP917bK3Yv-*p`ArbxlN;=Zx|Dkv*F^C%4Nmn z!)UNZ{F?8UL)-o8+hmX5$g^Uu=XMOw%%*L;+@!s3(PDdDMlioUqv2&yb-1@~XKDA) zj?ymYT%J{K8GUI4OL3lXmJF#{i&Wi@RPh@|aBA@#bu8b_LC$w0=iF;qO3!)CK@DR= zP`U$9x?;^%mTk5U%kP)NSMiy9<^GTCj<-f|Z>Bd52$s@pp&EksSXhTVCpf9`tnMN- zjHkbtX+Heu_1F0;h^oBG3L^DOo=jPML_Z!u>Vz(p9AxL^s`x_4UU`UYzHYz2Z4s|8r1KxH;_;gxLfvD7z0`I^ykfnpcy9)D@OLJo@l~{;llf_t z#@>{0V~@XObNKYDams(063oXDjwI!5rV zL^#`bU%lw=J*nRM?{x%A>8UzC^txyTNH5x*>T$gCtnMpfG;WaG8g?ffZ{n3hV1(4a zs%J*yR)E2sFuT@fl zNc1jIkp)}3+{x-mUsG?GPhanY0=X2TKy+M3A|I6AU_R6+5eN9=6(jJhq=NaN5sq>w zQjAE9+M*;;R$A!>^r<}QsgnP_)4zfv|H|L-8I_%NJ6E-k=f%HLgq>R*nFnd4&XWHr9r8RUsj9QTjhwaPJe_G+J3WS0azh|hM?2Z} zAuH@mNt$QN^XNyy&d!kB+B30oe$E1%&TJFE2g}#d=3B@9*$F$tmtH_9Gqq z(XLXpYV@u4^tNFqcldmp(Kws&$BcJ>Ls`kqq>QjO-DZdH2s>ww&bJwD7=7|?q1z*T z9VhB{u)ppUcK#W=z-BaVCf!u3*={h(l>(z2>u7d!XZwF$!cL!!;L9Miu<6c!?;uiD zI8f)&N4nX$-NH_rnhR}4gCnJXqxZwOGwh4_P2+y5w%m1(J>agevo5mGW;ArXc04j4 z_0cwv^Wm*}+P&@$J0EmeXfqmnsb3t7pbvU7mT*2g{CdCrDN;4$>V-C=VWF?lE+(Ub z?8Gsm_g4?wuihDULVFk3j0St_JTF~&&@qBrAEQM^{axt2s?OzCzZZf1J<-#i$@}uX zUy&-@8lt5t<$;-Ya?g-6YWHxP`wI4>YL8EoJ3!?ufW)if$|*b7{tkWc{fST6jK*n> z)R$w|LB|JyoR97{-@dPH$ocfh5S!74v9M^{^gr<(^?RU|99a93U8!}*35SN*j5dr~ zd0TT<;r_vN+&{>vYG3SCdup4IQ!X~dW;Cp|TMMOwx6OlRv4$A_$m@xGFj`_Yvc5*H z%U{fgU!vT%lQaCWYu%kw6JM!3eq45a8~cIhLQeG|V{NWY!>BNDQA~Kemq78#)Mx+dy zYA^X9t^gftBGBJO!RJ{e$?Gbq)r%kiT_V6;J?S&P>PPg3Wkq>wM z<2gOhKPIAo^!)%Q00)Ge7B#Z$Rr{V#`n1c+-iRKEoJwSx!Wq=HOJ-Xgc3^A46*| zj6GpIU%4aXoINrbI;uXNmi7^JNdH*1X`wykQH)VTM%s7OUTDX5hMd{SQ{o7PHjLu2 zy!0X{-5*i9jF9o;mxqBUGcrDw9Nraj-s~;6POtgY?M{wKv_x7Rc;w=7!P;*;a#~q^KYa|#u7Cf=&Ki;edt`>+1ajahR7sV zmRa&Yg^*{K@<*TOyF(_l<3GngQ`95G<9_^bvc- z4ubM$0xf*+^Z;W2(|08NkXdTg`%l=Niie%#KW7FI z!`yo!`RM0oq;yHD^{jsO<5-c^duo0F@yzr)V%S0UCS5FDRX#^+W!cxZ2|JJ9KHrX3 zoNkxL>deCG4EEjki^i%WAD>A+Xf#LxBByBBS@!bGz{oJ~&!=-fLqD&Me$EIU4HylM zlmj)>n;{>o#J85lkLW$4?WgL5oymXC35+Fua#qGpAs@$}gAvol%&?0hRpz9bHlq!r z$(|P@ov?;`9&0#$!-(GVrrGaS3Ogm@Hp`br)XMo0I*OpISjW6_&)a_=2st0No@z51 zBlXd|H2B-aEs!jgGUIu>?q5J)XG}-j+qzBqt{|NA( z3hILqTD;(C+;BGJ)c<$}bePw*h>2&-Mj&J~n0PwmY`J@e&1geIJiGsyW{Xf(tx;Co z+Iw#pWTzulE5HK1<@7+iOY^XEFHV?aq<-ty^b*jq7dlvK$-pP=-L=9_nb+n75S6cc zCua<5@WDWj;&{1t;oD$Hh1f=Xf7HOrlG_!xrBx@V+RbkWJLiv!ox13{r(zY6k38f< z>vz=Jb;P>x-7$>o-jzrdBUnoBwWY3ami7R2jDrp?U8%v7_#T{BVxC^ohPgB)QVodD z0|;p){V*B~J2~HGG;GL6y2cg*;RFy;Yt`{pH0pZr!x0*5xc9>mIf)cZm89^l?qTQq z=NHA_}r0kYeodNYf^7R4`{Du*WUa#u(GL{#B;5UqDu^BohtSkV*Zy2%dSCrK^s|rBy8%8)4 zsyWktUR?l!-!S6o8>=~u+;@OjwDoKRw_1C+-8w#>D=D7Ozr4Ja{XpNab9uH{CTfi9 zl+zNub_sf|)RaQ7)bK^O+NGbs*wx@=AVzP$Ce{$`3VuZzf~79ItAm~T6lR9wf~fQ8 z=yZ%#-g}sFSO-h(>DR^XGA!(LNR?L7J}ih* zr^ZDFA?JevIcKTvg?iczGQ&=InjnS@jbs->KH}d}lyjC^ao@xC-l4G1)E30xGSkw( z!PtJ)MJtX*v*p{|S&VxP5uPMKzMXLM#)tnZ0y;lH&-za^8*fz16vo3p0 z0SJD>h|k}y=IsAAy8r~gVZ=4NsyWBk#0o(08%BIqxVqEp&YS`e{Du)PHK>k!1Q3!z zgWoVho-f?5NTnM`WR39SVno-a1*9sD;J6sUF(t;tID+G1#F;OVDlV%mh)YQHD0#l~ zWc5TH{QnTUX4OcH2Cwz4aj~CY%y|}~l%VNo`+kkYh{Nc3K15oAhFG?zrZau$ig>Lt zT1W63M(o>D;}T;czhT7aLdpLgcO_9qxK?sv3}-1u>-91F=H$dY!RP`JS)zjxEVcKc zr*Qk*g4Y-Zz zHOTFCeXAOY5vMJoFOA^V$7p;-KHJF3eyEuzY2-D7zZyu+@SjEM{ggV^+q>*Ig5UV7 zfgl36HCyVoW?u>%!-;hQZuCq%iw@k-Z9UqyLtF>H@mB*uMB%?mSnsy(7DxC}m{k+& zgyanWS#+%Ke#Wfh_J||+jlUWQA}&k)*-TmfU>xB~VLVT)6OuFd_xSzi=Jh4}#1Xy} zICB!~gyanV9lN|Oa?B3j^@}5XDeO-s)(OcO{_jgqly5%j_f+rxO8V@4w8${~iifaH zT(QXJb+BP{JNJFgt@tWhh_50kg+#lexo^U?Pue3N4?81XlvT#Os|rUHyD=kJiuHln zXu}O@UtvDS+FjGZEAOLE%(62d4m;6Pi=ZPZyFu&@%;)e7(-16`HGGbJ*MP8dpuEh^ zA71rU+A`=6zgJEbOXYk#*FKN={H0$m%;)HZ3tl#hlpo{?!Y@&t!;>?3){ft?Znj!G zB#z)W{!AT2;2w8#;l^Qcgf9ip>BJn4I;Rx?m^QDx!7P6x?w7%zF9lAJ#2lWS;XjMi zg0-ikMXnkdNAMeerVb)vi{?Zp+&(If@TGXBPR`)pv3HR2`As(-y?_YL=jx7RX+3H_ zN3Tu){KMuK5&XuVk&`p{ci7?<&B;}%@}+QhPGZ(i&hVcv<~&#BoZt8}au9JCtpv6P zOVFyd>n-C#s+5h)Fxm~<>ZV?Hq1W;(?9Zaf8T`B4E%URhMh0KXF#K6GIfH*U42)Nv z8n67hFFC`1#%i7BS|bZ%qNm10e;(8kjE;|Rt{UO|lyJPs8JZ4hiLPpieu?thCpm+E zH?q*$UDewCxhOeNx8T`AEwbsdT z$Lzc?iuyH`oZ%xFjfRnxmsZD1+IeAC^Jo3!3|=$vvys($d?W9zM=s2q{t6&D!$<3@ z51d~@OB6pZX^B1pJDQ1gLUIQGZWx$3JvDRsb&j(@iS>)Oz;rDvRT$@|9sh4~#74_lk;CMNO@NaKXKIeK^4c5l320nt*&ZTgn_6pv8ce3L1GJ+Cd3$2C={V#JO^ZzT5a7>%1rH=8*lVUu|THW`+h)_O@|zfMP- zvOdUJUJ`e08Q?p+0QX2O~IMM#E=&ef3x;_m#UVkj&JQtv_1{Zb&hAhbmiHvXYTX0rX6qU z@3`v-wQ^#8STnryV{9FAel13ZBE#Rn-G(7&L*7*Tz~SYIea=<;mL~Qi4P$_DbNWKm zAejC&#Q6Rz5__qP2Fqr#uj998<2wg;#`{t!f4rI4;nfjai(H#7tBj&R@VLtejtQrP z2A6Nv8;Gm`f=6dYgCDL%j~MoYy`PW|9(Q@HWi&>(H|EFg0OAE8SV~4}TyW^zICGAz zBhFk~rQlt%FJDdUpUS+O*fBFIKD;Ss9`ex+`8YsQXQsbqw<{8M?in=GmfzK`9KHqf zLy;0d>;;0Qrv0!qu_vq}s?MAefgh&_-&c#Jx_`4Qu_vq}{;Y9rte{k}l%9{$v)4wj zX7^<6&hd&qwFk*)td<6SoF->}4k1;HI5G5%#6CBpF-Lv8DK;HBzw;tO?S`@xqYb0& zh8JT!q2pPkifc_~>ZP5-&QpsP1ZuGI*a>OJpyM>g60TcD$V&D&Y*`;}6ibo(VIS3d z6*_nfm)U0#JQ9YVecsk*oJGgBXN#Yu_{qquwj~;Ny03rHW(3EC{@-{@nhC@*=wJj( z$y}oJ!K&8!P^2Wr#62?OkdE1#!K%_V?4*}m7@yCxQ3{zai|6v|tq#QujJq->GD604 z)m|BmJH5*fjvWC)d{`J!bir2pD(oUBU$-zm8z}UJvJxK_upwv&nK@M{F!};Q-2%=! zIJb;8j0UAwMrxs~K1Qnejm+mNx4MpN7jI2}3>L`YaSNpXop~Z_bz<+I+k#<~dwf7s z-#gkz)Og^ng#QhrF^W#ii-nP@El8D&=W2B3c=dKww(SLybQA*xoYt zD+%&fsUZ+{f(< z1z70W#?ENb!4`Vo-kzMn&uqDmTf7Re(6f!5(V~Mb^uAr4ZS0H|1Y78RyE@z087+wa zjo6y7(EE0Ewy`t1b-gPiZPBCVqepQoVSA#C)c#KOHN$_(#ecwW# zoWajhv;HnX!8U!i$}?JYAm<62zHeP-OEaSdaVh)mrEI;rP2ab~vsM1TZ2G?SJUN4( z{g@?P2aca>h^to-(YlNe}JV}pXgwlzHfrMvIP1S?GOxyKb!q=j$ab^uCQJR(aMZ2wCN0wU04Tx8ti3RmMu+Lf^F4lAMwF=6odX>Ew^? zJaRt&@n*Z~@UU}JUl~PfjlZkF8Hc50UFy$j#Wze%Kach9p^KIYl@gYE?U$|g$MEsW zzfW}ZD{IAM9&8nuKUhla4F1^u^vgAJzQg=cHZXs%RA}!ub+8n#&Mu8$DS!3yU#Vg#Ui(~H2TS=o{QsqcrTkg{ zzYr|NYlcgwilzJ={{Pa!QvR&}UkH}+XZ`;|1f|rP;a~a4l8z#Mkk<+RetiKt5(tS) z#mneR>)?2K{qQdY5+#4Eo##G%R@3{rAD2Ey`74g#$iN6+idP)}h45DvJhlfBEXC^* ze>S*?$ckqiIcF(eKl}?J0HE}hg)gPAG7@DKNBHv{r=4R8riu|fbNc!y=W!i8>-$pr z3P4YlKX2=)@?#37iV?g5@byuuP+WSu;kXXJ0E| z2Q+tNKka!e;b^RGr0K{tE)9N}g42<@Bj4 zkqd}|^1)J^r#K=@>0qg#4yEscjyhOnco$Qpa?bHeK2)Ay@e21}BYQg{bX?~AdW%0 zOaQ@B^8B+Ga)z9e{qrzVbs8yZf|PMe_$;AbgVDoNBR!DwX9GFscsVBQM{P^9+C6&K z9go0o`1B#`yHJA{Pk?%ybM0I*l>#hkuSdMg)6bZVBWFxb3FlltQ~h1q{)lQsZ?>f!JpyB z=GO7YPP$L@Iq9-?IjOlXDeSzmb7DMI>NF_M(JxN-p7{K<`TS?6P1eq-(od65dZTQt zF7`QHd`&PSs6(aDFm4%GJ#rY|AJ|pY5E5^>lkle5`;5(Kc(5TKCy|eakDqk;jXbM! zsd~=$7jI>K-S41Vtp7m`Atz3Ehr>>VZj<9W6dJte8_T9YhcWRUjES6*AVQU!KKcY( zq8g*5RjLs~5WcrMk3RCOMw}o5H`t*^$+@_o4i&E;RG0YT#r_E;q zb4c+y0ly2u>BFaZ>O4+r4{V+-^=m;5VES{dE6&4wZ zEWrCQ^{d~_>OE>$HUcA=e9Qk0Cq+JmW5Tqd{|;JqYsz`zu% z3<4wMSxM<^B=1&P7?GQrzzLwGiqV`x!?<$8%GlFD^aX;Y)pm1rqsp*TtlOu{@gLSYJ>%)y~E0fH}5WfVL3hGdvBe)Ua zIWv6<@^Lrv!3cS-HW_20F*we)pz;J?oDV9wHHxNqOOn95Ou1zQvoILO6tu*HXo(WI z^)V+0*Cy^{ZIx-=i`xVxFhaO9mV5$Ui?>I|+m+AzQ4ffFAUy+trI>Al^?}P`<&`mY zLV=+DqkBF`Dyd z7$Z+)THPPnn>z)8-v~Frcl+Qua&jCz0h#ecbo>uqNT)FRFoJb3+AuP||28%QR<$m$ zs8d;p)ngZZ4$uu=EzX&qm?+Wj1XrI>#}M}!{!F(zzQI5!$+ zTTms+Y3G>WFN&OR`{j@5nZtiXSt^)w#g~Fxt&R_iEW&Ki8uh^l89zS#HRL?_*92SF zQ8n|-n&VjeoWyQ8zhQ*%8eE69){P(Iwksp2c5Q20`^&sF2#jDU`CW0#=m?f#z62f7 z?S&k3#Ll{2Hv~p#JPO3_fZL#IyXMg=>spxz!mv>P^04z~q37ZZ3*AFur&(RBmNd47 z#Z3!)x9+auJ-Vxk$KxwAtjO;^%guo%g}tl4uj1_iipMK?Rva>n#!cg0J9}3)-{c-o z-{i6mj#uhQ^?8&P5FH@BJb+*+dG=?uJt>8~(MZvXK&m(;tlKbd-&)BnRxAacfcm)0 zV=dYtF4`OvdA zOUds(FU}XG{)tS(Dx;ft){u5?W86Z~Yi}s*%}1(=?bA~g)S+~P_al8=&S9*O&j)mH z3Rxd+!kn37R@hb7YaHkwEX92VW8#z?vlbA2zC(|q{vpqbjYLQ6PfLxA$4Gr2Mruxp z@IL%q6MQl^PO&*AoM_ou$$h72iuD`=9@KdUh` z$?uAR$S}@+S;-v^>6-&OI9`s)FkZ;}Ju(aV=p4w0CPjHNjBCFb7sD=DoGXl_m{6Vpqsyy(#moED19tj$=(rrS0ZR$r&e6JI zxcp4Db;OVVycbdT-N{I%Mm6F3sB$AX8G2pdtVsttw}|M-{Iq>U&NS@Cnwo1$xIUI4 zC2O9zz+aB96 zNA7piKhhA)6vJq6{PzFZt+}+Obu)snQB-*_?3B%#9p}kZtd}~UCEF8&2*s=^zbmw? zV-i-*tRP~|CJZmtL)DZGKHp6{kcvGdJ|dDbv5^pg#JJ{Mj7~gam#Q zL@16HTZFNn=R zOS}^;k>eFkjux}SP7Yc+#{@Th`z>>3#rocYfDT5mK5&wE%ywJVsb@)G9rCPltFwhJ z1Y!WB4Fq8YO#+j~(6gA0iIG%i3!T^@+wBkOwgDYMgo+95ht1wH-$OngMc}kE8;PEe z*V6jCrwcc-#v^d5I3+qG>4V|^?%-mLtN{T8O9>zg4)uTLOp!MH^ZC zCWufBsJMr>%*xzhJyWgC_or$I<}DRQ(&tBlhh?kiXqK}gcjtYn)@lUS!Q7~f#%bkm z(jz#@;o(GwR)bs$T!t8ZPCXL?r<#YaVXdr$x#f}hVaI!PfvqE6-8{+sA==QJjlgf1 z>xJtEr|Z9%WKM^4b^!>bgVFdRda!l6x(|}m&ZWR91SjB^e>Xjj6~IZ1?JOniFg0Jo zdGP%UY#lMHaEF{DK>QOxa2+rj+*HGwnLU!4c;gW`F6OM!^KtpZ&CDxHH1SFX5F9U~ zaX;s6&CE}VH}T2?#ZvODxNvmDxm^>ZZ_iKl3N1+05X^GJXmDmOt!jpTYGVEJLlcX0 z%j~woTe|~(8MC`eOZ0AAnVZ`y)tZgK2yT7yyW--7y<|aEbN7!;talL@!BVUb_p2?h zYKn9P1lG<{I-Bl^%tF?wI#sRy2#jDUoxOGUghJMeYpPn$A_(u;%NX^S9iOAI`ssXR zX=4gm$F8kvCD+mstWVd`r*p}4WoO`=OMUcw8EkJQ7TR31hEZwH{gJoOYvnu+BUlHc z!9?)&#OT$i@8zg>j*F$Z{&8;NwleO4TT{F~2rR|SMmpm4{Ed+wNLAcVpIZYsAYPmo zc0Rjffz2`D-q#+f>55m4OV=O-=Lz5EEy}p>v`+ET+v=%e1g8*IoJM8bN86-$HUdk@ zvtnh^5y$HN;YQlk@X8}Ff|)58jXRN2?(-h4x6+irt3G*FwpX2T@eLrZufNiq9YC-Y zuNlG2z57Y;fp(qDWw&)Q`3)nO;SpzLwqI-YyRD>k2Li7wg#l5mCUrK%AHKcTS_$b6 z1t1iwqyDwMua;}pzN)VESOMSXigA&@MPOGt*ufjya;v*2W2?(+PI*>rh|F^c?&#Pl zcYX2dUIhe3Ft;J2aU*p0DR(!dXCv?$SDqCEBBQZPTvW#W;+7O|1Og-E+dwfRGTJcC z%rE0Eg7hZ^Ak^1{hBh`~g`9-d@iPUikkuCiuad5H7{Xn}?kS zZKlV+CKNj9#ODmb*3VOsy__mJfY3!un~)hq)3M!90+hLc{0{!~#fvS^z@D z%V?Y>>o(llG_a(37=g75bL7MxVQ0@4SZaxP53{NBptpcB{tC-tZ%w9gJXohS3XS$uW#2-v-7K9zPh3uc)s_dfnGeiZ1acMfnXQn0*ti z1c(r%(*p>W(o=Q$ub)LeKt38EA6$woC9Fet!!z^PM)~rZe$8;t>R;OJfxvGV!TMmo z+IzcI5w&;7(QA%e@ebd;Af>IZF@c zEwy}b3U$Q0m~lE|wkeCiZH!MpI}?yvf@1PjEZr${vU2Sa$Pzw*D^xSxi_*Gyl_{U-<^Nc z6pq(Ju#t=#rt|vp3S`FN zIaQonwi?09;*aMejd3O}8@m9E5RTu4;FN8Dd}dsS$`j67-aIcBLfe(QAQ-{%G8(=g z{Rc+WeRrG>K{S9xr_z|&aYk#EC&MV&{QXE}jEP$@CNhHKWwc>bM_Cm`IkiOK@?qvL zy{wiNZDW<`x!3KFzzAmkVl>v&4~~gj)x;y7wF_ePbFjS)oEON)6zF&y@kQ-9>S#x$A0Uwsv=jvR} z_}xEEE;V>1q#rC^-QsxVIgt{I>B8!D(k&_0cL=OQo>jcEdYzJm)9Yo0%Uf2+??ZY9 zg7zkW@XrY!>-|{TR~SnwBIlecIn|f~OHuLz3v4~-sT(ThtOw$n0D@D@50?tK&$~`Cjv{!vtsqr5sOcBiNJ5rga4pbR#FP;`Poy(2UI6>F2nAu`@iuW)A$0btgd<>@$r`4}7X3m9l%>ocARg5-_hFMAOQ-e}0 z3Gznyk;t?1_t5Xh88@hFPC>pO!i97KJ}m#77#!yiQhp$~&7(zSb3x%0uUa8}N0AZC zy<`};l*Tk*N63z!pu|C6iU|EtIgU3r}1eOZw zP`*DnS7iL*-qfXr^;72>8iM)3g!xPPOc_RctvzXK=42hh=`FRie}GJDb@r8DMhuHD$i>FROi^b zYkfttSm_jN4-mqurFNuQiuty12C`~l>z0F6tl9{SU@2znGK`nMsAyJ3{OzEJQx(*q zbQ{LOdgBxK{4hfJMAW{s{H|;#_@eLiTWmJIygK5`ixHecMq|xzpmA)%#l3Bo5-uF& zIie#zE794yY40X;;+{>WFp4Q(C1Dgx_#F`c8Q7DOI$QR>O=jJnHkmBN9AwN*W*D7o z7PbZ+sA8o_=~BF0qKr0-xm5~V;~{-Ch@jeJGy7LRM0~q0o zVT9v8$RnIE75aXCWulo@nXV^u_jfs&%Lrk~Q&QZ64Wk$k_W>~%fu)#NPe=4eK58Nl z5;z~6c21#T+|Vl1d!xaLs04n)2%T}U5fFnRy&`~MDLv;i+Gkp++y9LALEwCF3+Fsx zoru4FXjenguDGl?UXBU3vO~w19e+lb7obD+N7je48A&@L)6viG#K^#L2_uy9zvJEv z*19v9-sjKl$*nbG4}2i~x@8V2MqkK>@_}R>LA25bFYl#4Mby2PtexW(UM%H#h}mFw zZLj1{CA}j*mh|`yvuEjyS$EBNI`$Gg0dIjPAnRZ>_h!RrI^b+mNY+QgdSm7ZRH+G=+_La+{$7bBN~VRT&F(>v05wc9X&;CQ)iu*zuK(t0g5-@Ltf zz9|eOBgTZBiE{_q!ZfuGC)_&zG9(*|Os9*k8VBZL>}jpZTd`ByVFH1_W9 zIAR|DIN93tX|je8wBmTx*;~indBiNWKG`a{K}WEZoZJ7nU&uN3hIo?S`|I48fidxC z851cVEG3+WDo?oOEoDJ=2J-P*FdrnvdBQya=N`Ap{`9Qb7D0HtYHkQQKV*%zm@VG_cx$10~sG(uS0b=9KY0(yQrbSsQ$o!`CVgKA6X{~NI zEw@gGX}OFLeM4KrPo~sNo6&~RIK8EHIwjwng~0X9Y3I6uMHo4+gGq2(nT8?=bWYV(!Kp`hF5q_zB_+*zRNYmOqDvK1rP=hUkHL)Ul5_VMsa@dlPA4j zt8{ihLeLOED=FbW+lD**>Qxfn+ADzg1Jbh*_>G_$q1Z{mXIc7el3=s3#zGOuIq6A1i9o>fYO?M<~Y?8*R<|5eA_!rMFM z@|z$+=X+Iq^sIx?dOl>%DTg}Wfti!vFhXMb8W!(UL#Cs2f6r*1u($JSNx1ZsC!oIG z-9M;947?xScj&c@;8B~=@NTP|9aH{^EXCuwF#joEMey!y*&b0=10Fww#Uo*Jz_V{P zSd;T@^p+B-*7L$eT`ar zUF>}Ge=YJ&)}gW6f`LYrG*%huUu8dWaa|HbC@D@Mtmm&ya+lc+y?YS2jdBa;xdfb1 z&rEVxpl|d*;2M-?WgTKPtb-dSnuWSG#F+yf!S&2&tP?+*Xub&P+Yq=E<#}@d3w-*F zHjL|zW>~}D$#<_pV1(pW%~6atj2nTt719+7K&TpIH148%>q*P~xwAV>0@X8fN9%~* zOMu&Qhss6nZlL_HJJF?L^nn;Mff!&jI;q@Go+30tIEYq4(Z#35DrNYUg1Ql;i_ zP9aWn%&44m5BdjuE#thLDz~gd5DIM=x$Q?;jo<8)`_0Nux%?)`daCAKjG{oyTh=Kz z2dE%I&B%1XJbLM}^#x z-rnb@MXSx87G(tM&>2ZXK-`Zt^{xPdrC1+!6pgZG?VD4)+Ys2MCu zh`*QUptyoM6xuN67dmAwMf@kBhvN$BNbD#Z1J<>-ewdzTc14gJs@w`%N$EMi4Tx?t z^2{d+K&Z4c8XhV4y$}r)qKC$FgO|y%G>C#bu=^%MoLcM^l^o%n$=)4pj=*mip>vYo1;my$yQ60j7{OAULU^QX7!#B8gW~DL z`QWr`^>4h{w2il?*k0F4+Uv3ub2;mXrvHN_3avx}zmey}{fxx%jJ-SPH~<|I7!lNw zKpSS&;okQy+&UF@T|{h!|2(j zNP>rj<6W$dRyAb{Whu$iE}Y=|t&*JiX|?f5Z))+* zW+McSms2HZbw|LZ5iG^}a0e(Xsr7$+)*W0x>8g})N#hH0>aJLCw05~;gyRwmZLLW- zIcLv{+uIcy_eXVYX&pQNi#q{{1ciR-)T^J0v`Z$97g(=oaX-)sDZCC_S&tADF?a7TK{u<5_h9yWdY2IMam`QA}I7$@)NPjZ)J zG_*z{a6aT&)q#F*#v<7I8ntX_^$#FeN-PsAL3J~i=SU>*k z+R)m9AQlK!&wTTR{H{_6FN;qmnnNIc6oGXF5h|t&_L2#Z?ioM`TBU^1;AgP+XA6UJ z8GLhcS+PE@P4E=$+wZT)s7wT*)>=B^(-lX|-DoA}5crKeD=uffmAs#^JvuWf)q5X- z5zIEtXv28h+8%ue(ys>)oGM1cS-ti4=r%~-i@;LytX6tDV$A&g2~R+gQmsL3i>iNc zOos7FiG69RbQ!_*&uG}&i)_z*s(7krAaGpFfi1tQ6yhG-^WWy)3+aXd1joy0?0+O} z&%GMbw<55VJgfT95jVg6q;=odoy{T$Vs%n8i#)3tfP1iEFoOFzqjki_%|#No4>3ae zwwkXP4GxHQ6(e$&kj!coBU@;9L}sDc2%Uf2+l^IB76QLvgjP>R86b{8It_u}1QDt>4ded+bVywT literal 0 HcmV?d00001 diff --git a/resources/calib/temperature_tower/gen_belt_temp_tower.py b/resources/calib/temperature_tower/gen_belt_temp_tower.py new file mode 100644 index 0000000000..9182647acb --- /dev/null +++ b/resources/calib/temperature_tower/gen_belt_temp_tower.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Belt temperature-tower asset generator (discrete-provini design). + +A vertical temperature tower cannot be sliced on a belt printer, so lay a row of +DISCRETE provini (one per temperature) along the belt (designed Y) with a fixed +surface gap. Each provino is the chevron+arc unit (belt_temp_provino_unit.stl, +keel-first); its temperature is ENGRAVED upright into the 50 mm face — a raised +number would be an unsupported overhang on the belt. The C++ calib_temp belt branch +(Plater.cpp) injects one M104 per zone 70 layers INTO provino i: + print_z[i] = i * PITCH * cos(theta) + 70 * layer_height (theta = 45) +inside the body, not in the empty inter-provino gap (which has no sliced layers for +the event to attach to). PITCH below is the shared geometry contract with that code — +keep them in sync. + +Generates one STL per filament temp range used by Temp_Calibration_Dlg. +""" +import numpy as np, trimesh, os +from matplotlib.textpath import TextPath +from matplotlib.font_manager import FontProperties +from shapely.geometry import Polygon as ShPoly +from shapely.ops import unary_union + +HERE = os.path.dirname(os.path.abspath(__file__)) +UNIT = os.path.join(HERE, 'belt_temp_provino_unit.stl') # single provino, keel-first +SURF_GAP = 25.0 # surface-to-surface gap between provini (mm) — user spec +TEXT_H = 9.0 +TEXT_DEPTH = 0.8 # engraving depth (numbers are CUT into the face, not raised: + # a raised number is an unsupported Y-overhang on the belt) +TEXT_OVERSHOOT = 0.6 # extra height poking out of the face for a clean boolean cut + +# Temperature ranges (start, end) per filament family, 5 C step. File name encodes them. +RANGES = [(230,190),(270,230),(250,230),(280,240),(240,210),(320,280)] + +unit = trimesh.load(UNIT) +dY = unit.bounds[1,1] - unit.bounds[0,1] +PITCH = dY + SURF_GAP # designed-Y pitch == C++ contract constant +print(f"unit dY={dY:.2f} PITCH={PITCH:.3f} (C++ contract: print_z[i]=i*{PITCH:.3f}*cos45)") + +# 50 mm face normal (0,-1,1)/sqrt2 ; UPRIGHT basis u=+X det(+1) (verified non-mirrored) +n = np.array([0,-1,1.])/np.sqrt(2) +u = np.array([1,0,0.]); v = np.array([0,1,1.])/np.sqrt(2) +R = np.column_stack([u,v,n]) +fn = unit.face_normals; fc = unit.triangles_center; fa = unit.area_faces +sel = (fn@n) > 0.9 +face_c = (fc[sel]*fa[sel,None]).sum(0)/fa[sel].sum() + +def text_mesh(s): + tp = TextPath((0,0), s, size=TEXT_H, prop=FontProperties(family='DejaVu Sans')) + rings = [ShPoly(p) for p in tp.to_polygons() if len(p)>=3] + rings.sort(key=lambda r:r.area, reverse=True) + used=[False]*len(rings); parts=[] + for i,o in enumerate(rings): + if used[i]: continue + holes=[] + for j in range(i+1,len(rings)): + if not used[j] and o.contains(rings[j]): holes.append(rings[j].exterior.coords); used[j]=True + parts.append(ShPoly(o.exterior.coords,holes)); used[i]=True + poly = unary_union(parts) + geoms = list(poly.geoms) if poly.geom_type=='MultiPolygon' else [poly] + m = trimesh.util.concatenate([trimesh.creation.extrude_polygon(g,height=TEXT_DEPTH+TEXT_OVERSHOOT) for g in geoms]) + c = m.bounds.mean(axis=0); m.apply_translation([-c[0],-c[1],0]); return m + +for t_start, t_end in RANGES: + temps = list(range(t_start, t_end-1, -5)) + parts=[] + for i,T in enumerate(temps): + c = unit.copy(); c.apply_translation([0, i*PITCH, 0]) + t = text_mesh(str(T)); M=np.eye(4); M[:3,:3]=R; t.apply_transform(M) + # place the text spanning from TEXT_DEPTH inside the face to TEXT_OVERSHOOT outside, + # then CUT it out of the provino (engrave) — no raised material, no Y-overhang. + t.apply_translation(face_c - n*TEXT_DEPTH + np.array([0,i*PITCH,0])) + c = trimesh.boolean.difference([c, t], engine='manifold') + parts.append(c) + asset = trimesh.util.concatenate(parts) + out = os.path.join(HERE, f"belt_temp_tower_{t_start}_{t_end}.stl") + asset.export(out) + dims = np.round(asset.bounds[1]-asset.bounds[0],1) + wt = all(p.is_watertight for p in parts) + print(f" {t_start}->{t_end}: {len(temps)} zones bbox={dims} watertight={wt} -> {os.path.basename(out)}") diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 6bc6a30f0f..58d0ccb237 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -13297,14 +13297,96 @@ void Plater::calib_temp(const Calib_Params& params) { if (params.mode != CalibMode::Calib_Temp_Tower) return; - // ORCA-Belt: a counter-rotated tower would cantilever every block off a - // support wedge — print the temperature blocks as separate objects in - // native belt orientation instead. + // Belt printers build along the conveyor, not vertically — a tall tower cannot + // be sliced. Instead lay a row of DISCRETE provini (one per temperature) along + // the belt and change temperature in discrete steps via custom per-layer G-code + // (M104), injected in the empty gap just before each provino so the nozzle is + // settled by the time that provino prints. We deliberately do NOT call + // set_calib_params here: its per-layer interpolation (interpolate_value_across_ + // layers) would overwrite these discrete M104 events. { - double belt_angle_rad = 0.; - Vec3d belt_axis = Vec3d::UnitX(); - if (belt_calib_rotation_params(belt_angle_rad, belt_axis)) { - _calib_temp_belt_sectioned(params, std::abs(belt_angle_rad)); + auto belt_printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; + if (belt_printer_config->has("belt_printer") && belt_printer_config->opt_bool("belt_printer")) { + // Belt temperature-tower model selector (Calib_Params::test_model): + // 0 = "Standard" -> Joe's counter-rotated sectioned tower. + // 1 = "Overhang" -> engraved inverted-L provini that stress overhang + // print-quality at each discrete temperature (below). + if (params.test_model < 1) { + double belt_angle_rad = 0.; Vec3d belt_axis = Vec3d::UnitX(); + belt_calib_rotation_params(belt_angle_rad, belt_axis); + _calib_temp_belt_sectioned(params, std::abs(belt_angle_rad)); + return; + } + constexpr int TEMP_STEP = 5; // matches Temp_Calibration_Dlg + // Shared geometry contract with the offline asset generator + // (resources/calib/temperature_tower/gen_belt_temp_tower.py): provini are + // replicated along the belt (designed Y) at this pitch. The slicing plane + // is oblique (belt_slice_rotation_angle), so the per-zone advance in the + // layer print_z space the custom-gcode matcher uses is PITCH*cos(theta). + constexpr double PITCH_Y = 74.718; // designed-Y pitch == gen PITCH + const double angle = belt_printer_config->has("belt_slice_rotation_angle") + ? belt_printer_config->opt_float("belt_slice_rotation_angle") : 45.0; + const double zone_topz = PITCH_Y * std::cos(angle * M_PI / 180.0); + // The custom-gcode matcher attaches each event to a real sliced layer. The + // empty inter-provino gap has NO layers, so an event placed there is silently + // dropped. Fire it 70 layers ABOVE provino i's start instead — inside the + // provino body, past the gap and the overlap with provino i-1's tail, so the + // temperature change attaches and applies cleanly. (layer print_z steps by the + // process layer height.) + auto belt_print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + const double layer_h = belt_print_config->has("layer_height") + ? belt_print_config->opt_float("layer_height") : 0.2; + const double into_provino = 70.0 * layer_h; + + const int t_start = (int) lround(params.start); + const int t_end = (int) lround(params.end); + std::vector temps; + const int tstep = (t_start >= t_end) ? -TEMP_STEP : TEMP_STEP; + for (int t = t_start; (tstep < 0) ? (t >= t_end) : (t <= t_end); t += tstep) + temps.push_back(t); + if (temps.empty()) temps.push_back(t_start); + + const std::string calib_dir = Slic3r::resources_dir() + "/calib/temperature_tower/"; + std::string asset = calib_dir + "belt_temp_tower_" + std::to_string(t_start) + "_" + std::to_string(t_end) + ".stl"; + if (!boost::filesystem::exists(asset)) { + BOOST_LOG_TRIVIAL(warning) << "[belt_temp] no embossed provini for " << t_start << "->" << t_end + << ", falling back to 230_190 (embossed numbers will not match)"; + asset = calib_dir + "belt_temp_tower_230_190.stl"; + } + add_model(false, asset); + + // Place keel-first asset at the belt entry (designed Y = 0) so Z_gcode + // starts at 0, centered laterally on the bed, resting on the conveyor. + ModelObject* obj = model().objects[0]; + obj->ensure_on_bed(); + BoundingBoxf3 obb = obj->bounding_box_exact(); + auto bed_shape = belt_printer_config->option("printable_area")->values; + BoundingBoxf bed_ext = get_extents(bed_shape); + obj->translate_instances(Vec3d(bed_ext.center().x() - obb.center().x(), -obb.min.y(), 0.0)); + + auto belt_filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; + belt_filament_config->set_key_value("nozzle_temperature_initial_layer", new ConfigOptionInts(1, temps.front())); + belt_filament_config->set_key_value("nozzle_temperature", new ConfigOptionInts(1, temps.front())); + obj->config.set_key_value("brim_type", new ConfigOptionEnum(btNoBrim)); + obj->config.set_key_value("seam_slope_type", new ConfigOptionEnum(SeamScarfType::None)); + obj->config.set_key_value("overhang_reverse", new ConfigOptionBool(false)); + belt_printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); + + const int plate_idx = get_partplate_list().get_curr_plate_index(); + model().curr_plate_index = plate_idx; + CustomGCode::Info &cg_info = model().plates_custom_gcodes[plate_idx]; + cg_info.mode = CustomGCode::Mode::SingleExtruder; + cg_info.gcodes.clear(); + for (size_t i = 1; i < temps.size(); ++i) { + const double pz = double(i) * zone_topz + into_provino; // 70 layers into provino i + cg_info.gcodes.push_back(CustomGCode::Item{ + pz, CustomGCode::Custom, 1, "", + "M104 S" + std::to_string(temps[i]) + " ; belt temp zone " + std::to_string(temps[i]) }); + } + + changed_objects({ 0 }); + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_dirty(); + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->reload_config(); return; } } diff --git a/src/slic3r/GUI/calib_dlg.cpp b/src/slic3r/GUI/calib_dlg.cpp index 870b078aa1..0b2cc64f6a 100644 --- a/src/slic3r/GUI/calib_dlg.cpp +++ b/src/slic3r/GUI/calib_dlg.cpp @@ -395,6 +395,15 @@ Temp_Calibration_Dlg::Temp_Calibration_Dlg(wxWindow* parent, wxWindowID id, Plat method_box->Add(m_rbFilamentType, 0, wxALL | wxEXPAND, FromDIP(4)); v_sizer->Add(method_box, 0, wxTOP | wxRIGHT | wxLEFT | wxEXPAND, FromDIP(10)); + // Belt temperature-tower model: Standard (sectioned tower) vs Overhang (engraved + // inverted-L provini that stress overhang quality per temperature). Only affects + // belt printers; the upright tower ignores it. + auto labeled_box_model = new LabeledStaticBox(this, _L("Test model")); + auto model_box = new wxStaticBoxSizer(labeled_box_model, wxHORIZONTAL); + m_rbModel = new RadioGroup(this, { _L("Standard"), _L("Overhang") }, wxVERTICAL); + model_box->Add(m_rbModel, 0, wxALL | wxEXPAND, FromDIP(4)); + v_sizer->Add(model_box, 0, wxTOP | wxRIGHT | wxLEFT | wxEXPAND, FromDIP(10)); + // Settings wxString start_temp_str = _L("Start temp: "); wxString end_temp_str = _L("End temp: "); @@ -511,6 +520,7 @@ void Temp_Calibration_Dlg::on_start(wxCommandEvent& event) { m_params.start = start; m_params.end = end; m_params.mode = CalibMode::Calib_Temp_Tower; + m_params.test_model = m_rbModel->GetSelection(); m_plater->calib_temp(m_params); EndModal(wxID_OK); diff --git a/src/slic3r/GUI/calib_dlg.hpp b/src/slic3r/GUI/calib_dlg.hpp index 5ff0fbf3e3..c7a12471bf 100644 --- a/src/slic3r/GUI/calib_dlg.hpp +++ b/src/slic3r/GUI/calib_dlg.hpp @@ -62,6 +62,7 @@ protected: Calib_Params m_params; RadioGroup* m_rbFilamentType; + RadioGroup* m_rbModel; TextInput* m_tiStart; TextInput* m_tiEnd; TextInput* m_tiStep; From 0bca3fd2e56189cdc75c7f8924f653cae5019f86 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 12 Jun 2026 04:41:29 -0500 Subject: [PATCH 049/224] make belt printer specific temp tower only accessible to belt printers --- src/slic3r/GUI/calib_dlg.cpp | 32 +++++++++++++++++++++++++++----- src/slic3r/GUI/calib_dlg.hpp | 4 +++- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/slic3r/GUI/calib_dlg.cpp b/src/slic3r/GUI/calib_dlg.cpp index 0b2cc64f6a..89bb4de3aa 100644 --- a/src/slic3r/GUI/calib_dlg.cpp +++ b/src/slic3r/GUI/calib_dlg.cpp @@ -397,12 +397,14 @@ Temp_Calibration_Dlg::Temp_Calibration_Dlg(wxWindow* parent, wxWindowID id, Plat // Belt temperature-tower model: Standard (sectioned tower) vs Overhang (engraved // inverted-L provini that stress overhang quality per temperature). Only affects - // belt printers; the upright tower ignores it. + // belt printers; the upright tower ignores it, so the picker is only shown on + // belts. The dialog is cached across printer switches, so visibility is toggled + // per-show in on_show() rather than gated here at construction time. auto labeled_box_model = new LabeledStaticBox(this, _L("Test model")); - auto model_box = new wxStaticBoxSizer(labeled_box_model, wxHORIZONTAL); + m_model_box = new wxStaticBoxSizer(labeled_box_model, wxHORIZONTAL); m_rbModel = new RadioGroup(this, { _L("Standard"), _L("Overhang") }, wxVERTICAL); - model_box->Add(m_rbModel, 0, wxALL | wxEXPAND, FromDIP(4)); - v_sizer->Add(model_box, 0, wxTOP | wxRIGHT | wxLEFT | wxEXPAND, FromDIP(10)); + m_model_box->Add(m_rbModel, 0, wxALL | wxEXPAND, FromDIP(4)); + v_sizer->Add(m_model_box, 0, wxTOP | wxRIGHT | wxLEFT | wxEXPAND, FromDIP(10)); // Settings wxString start_temp_str = _L("Start temp: "); @@ -464,6 +466,11 @@ Temp_Calibration_Dlg::Temp_Calibration_Dlg(wxWindow* parent, wxWindowID id, Plat m_rbFilamentType->Connect(wxEVT_COMMAND_RADIOBOX_SELECTED, wxCommandEventHandler(Temp_Calibration_Dlg::on_filament_type_changed), NULL, this); + // Refresh the belt-only model picker on every show — the dialog is cached and + // reused across printer switches. + this->Connect(wxEVT_SHOW, wxShowEventHandler(Temp_Calibration_Dlg::on_show)); + m_model_box->ShowItems(is_belt_printer_selected()); + wxGetApp().UpdateDlgDarkUI(this); Layout(); @@ -520,12 +527,27 @@ void Temp_Calibration_Dlg::on_start(wxCommandEvent& event) { m_params.start = start; m_params.end = end; m_params.mode = CalibMode::Calib_Temp_Tower; - m_params.test_model = m_rbModel->GetSelection(); + // Picker only exists on belt printers; default non-belt to the Standard model. + m_params.test_model = m_rbModel ? m_rbModel->GetSelection() : 0; m_plater->calib_temp(m_params); EndModal(wxID_OK); } +void Temp_Calibration_Dlg::on_show(wxShowEvent& event) { + // ORCA-Belt: the dialog is cached across printer switches, so refresh the + // belt-only "Test model" picker on every show. The Overhang model only + // applies to belt printers; hide it (and resize the dialog) otherwise. + const bool belt = is_belt_printer_selected(); + if (m_model_box->AreAnyItemsShown() != belt) { + m_model_box->ShowItems(belt); + Layout(); + Fit(); + GetSizer()->SetSizeHints(this); + } + event.Skip(); +} + void Temp_Calibration_Dlg::on_filament_type_changed(wxCommandEvent& event) { int selection = event.GetSelection(); unsigned long start = 0, end = 0; diff --git a/src/slic3r/GUI/calib_dlg.hpp b/src/slic3r/GUI/calib_dlg.hpp index c7a12471bf..2ddf056dc6 100644 --- a/src/slic3r/GUI/calib_dlg.hpp +++ b/src/slic3r/GUI/calib_dlg.hpp @@ -59,10 +59,12 @@ protected: virtual void on_start(wxCommandEvent& event); virtual void on_filament_type_changed(wxCommandEvent& event); + void on_show(wxShowEvent& event); Calib_Params m_params; RadioGroup* m_rbFilamentType; - RadioGroup* m_rbModel; + RadioGroup* m_rbModel = nullptr; + wxStaticBoxSizer* m_model_box = nullptr; TextInput* m_tiStart; TextInput* m_tiEnd; TextInput* m_tiStep; From ef6f65eacc728eeebfa91fc528b43221177a2e9b Mon Sep 17 00:00:00 2001 From: Tommaso Bianchi Date: Thu, 18 Jun 2026 07:13:59 +0200 Subject: [PATCH 050/224] IdeaFormer IR3 V2: tune eSUN PLA white speed from HW max-vol-speed calibration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Physical max-volumetric-speed test (belt #62 v4 asset) on the IR3 V2 with eSUN PLA white: the wall stayed clean up to ~100 mm/s = ~20 mm3/s before under-extrusion. The shipped cap of 10 mm3/s was ~half the real ceiling and was silently throttling infill. - eSUN PLA @IdeaFormer IR3 V2: filament_max_volumetric_speed 10 -> 20 - 0.20mm Standard @IdeaFormer IR3 V2: sparse_infill_speed 200 (~18 mm3/s at the new cap, no longer throttled). Outer wall (45), PA (0.12), accel (1000) unchanged — accuracy preserved. - IdeaFormer.json version bump for profile-cache refresh. Co-Authored-By: Claude Opus 4.8 --- resources/profiles/IdeaFormer.json | 2 +- .../IdeaFormer/filament/eSUN PLA @IdeaFormer IR3 V2.json | 2 +- .../IdeaFormer/process/0.20mm Standard @IdeaFormer IR3 V2.json | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/resources/profiles/IdeaFormer.json b/resources/profiles/IdeaFormer.json index 7e3e108c3d..a021e7c164 100644 --- a/resources/profiles/IdeaFormer.json +++ b/resources/profiles/IdeaFormer.json @@ -1,6 +1,6 @@ { "name": "IdeaFormer", - "version": "02.00.00.00", + "version": "02.00.00.02", "force_update": "0", "description": "IdeaFormer belt printer configurations", "machine_model_list": [ diff --git a/resources/profiles/IdeaFormer/filament/eSUN PLA @IdeaFormer IR3 V2.json b/resources/profiles/IdeaFormer/filament/eSUN PLA @IdeaFormer IR3 V2.json index c438aac0fd..d34131842a 100644 --- a/resources/profiles/IdeaFormer/filament/eSUN PLA @IdeaFormer IR3 V2.json +++ b/resources/profiles/IdeaFormer/filament/eSUN PLA @IdeaFormer IR3 V2.json @@ -29,6 +29,6 @@ "0.12" ], "filament_max_volumetric_speed": [ - "10" + "20" ] } diff --git a/resources/profiles/IdeaFormer/process/0.20mm Standard @IdeaFormer IR3 V2.json b/resources/profiles/IdeaFormer/process/0.20mm Standard @IdeaFormer IR3 V2.json index f8e8a69e0f..b06b2e2192 100644 --- a/resources/profiles/IdeaFormer/process/0.20mm Standard @IdeaFormer IR3 V2.json +++ b/resources/profiles/IdeaFormer/process/0.20mm Standard @IdeaFormer IR3 V2.json @@ -13,6 +13,7 @@ "skirt_loops": "0", "skirt_distance": "0", "sparse_infill_pattern": "grid", + "sparse_infill_speed": "200", "support_base_pattern": "rectilinear", "support_interface_pattern": "rectilinear", "compatible_printers": [ From 2d69f6e17c56c7506520be03b0f1ea595ddd98a8 Mon Sep 17 00:00:00 2001 From: Tommaso Bianchi Date: Sat, 6 Jun 2026 16:00:37 +0200 Subject: [PATCH 051/224] belt: expose MachineFrameTransform's composed matrix Add a const accessor for the shear*scale transform so the G-code viewer can build the machine->model back-transform for the upright belt preview. --- src/libslic3r/GCode/MachineFrameTransform.hpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/libslic3r/GCode/MachineFrameTransform.hpp b/src/libslic3r/GCode/MachineFrameTransform.hpp index 425fe3004b..f5e71398da 100644 --- a/src/libslic3r/GCode/MachineFrameTransform.hpp +++ b/src/libslic3r/GCode/MachineFrameTransform.hpp @@ -33,6 +33,11 @@ public: bool is_active() const { return m_active; } + // The composed shear*scale transform (identity when inactive). Exposed so the + // G-code viewer can build the machine->model back-transform for the upright + // ("designed") belt preview. + const Transform3d& transform() const { return m_transform; } + private: bool m_active = false; Transform3d m_transform = Transform3d::Identity(); From 695a1f897a828d6e24b44559662fd79b7cb49567 Mon Sep 17 00:00:00 2001 From: Tommaso Bianchi Date: Sat, 6 Jun 2026 16:00:37 +0200 Subject: [PATCH 052/224] belt: render the G-code preview in model (Cartesian) space On a belt printer the emitted G-code is in the machine frame (45-deg sheared, axis-remapped, scaled), so the toolpath preview shows the print as a sheared slab floating off the bed. Map each toolpath vertex back to model/Cartesian space for the "designed" view. The back-transform is the inverse of the full G-code forward pipeline (BeltGCodeWriter::to_machine_coords): model = [BeltForward^-1 if !gcode_back_transform] . AxisRemap^-1 . MachineFrame^-1 built from config, so it handles any rotation / shear / scale / axis-remap combination, not just plain 45-deg belt slicing. Computed in load_as_gcode() from print.config() and applied per-vertex inside libvgcode::convert (display position only; layer_id, times and the volumetric/flow math keep the raw machine values, so the layer slider and stats are unaffected). - Toggle with the existing "Show designed view" checkbox / hotkey B; off shows the raw machine-frame G-code (useful for debugging the transform itself). Defaults to on. - Belt printers skip the same-result-id load cache so the upright view applies and the toggle takes effect even when the G-code is unchanged. - The object extrusions (layer_id >= 1) are anchored to the belt entry to drop the constant machine-origin offset (start-G-code belt advance) that the linear back-transform alone does not capture; start-G-code prime lines are excluded so they don't steal the anchor. --- src/slic3r/GUI/GCodeViewer.cpp | 81 ++++++++++++++++--- src/slic3r/GUI/GCodeViewer.hpp | 3 +- src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp | 42 ++++++++-- src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.hpp | 3 +- 4 files changed, 112 insertions(+), 17 deletions(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 1f9741e92b..963730e093 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -24,6 +24,8 @@ #include "GLToolbar.hpp" #include "GUI_Preview.hpp" #include "libslic3r/Print.hpp" +#include "libslic3r/BeltTransform.hpp" +#include "libslic3r/GCode/MachineFrameTransform.hpp" #include "libslic3r/Layer.hpp" #include "Widgets/ProgressDialog.hpp" #include "MsgDialog.hpp" @@ -1118,6 +1120,51 @@ std::vector GCodeViewer::get_plater_extruder() return m_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 (BeltGCodeWriter:: +// 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.) +static Transform3d compute_belt_back_transform(const PrintConfig& cfg) +{ + if (!cfg.belt_printer.value) + return Transform3d::Identity(); + + MachineFrameTransform mft; + mft.init_from_config(cfg); + const Transform3d mf_inv = mft.is_active() ? Transform3d(mft.transform().inverse()) + : Transform3d::Identity(); + + 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) { + BoundingBoxf bbox_bed(cfg.printable_area.values); + const Vec3d vmax(bbox_bed.max.x(), bbox_bed.max.y(), cfg.printable_height.value); + Matrix3d M = Matrix3d::Zero(); + Vec3d t = Vec3d::Zero(); + for (int i = 0; i < 3; ++i) { + const int axis = rr[i] % 3; + if (rr[i] < 3) M(i, axis) = 1.0; + else if (rr[i] < 6) M(i, axis) = -1.0; + else { M(i, axis) = -1.0; t[i] = vmax[axis]; } + } + ar.linear() = M; + ar.translation() = t; + } + 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; +} + //BBS: always load shell at preview void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const Print& print, const std::vector& str_tool_colors, const std::vector& str_color_print_colors, const BuildVolume& build_volume, @@ -1130,8 +1177,12 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const if (current_top_layer_only != required_top_layer_only) m_viewer.toggle_top_layer_only_view_range(); - // avoid processing if called with the same gcode_result - if (m_last_result_id == gcode_result.id && wxGetApp().is_editor()) { + // avoid processing if called with the same gcode_result. + // Belt printers are exempt: the toolpath geometry fed to libvgcode depends on + // the current designed/raw view state (back-transform applied in convert), so + // re-running the conversion is required for the upright view and for toggling + // it (hotkey B) to take effect even when the G-code itself is unchanged. + if (m_last_result_id == gcode_result.id && wxGetApp().is_editor() && !print.config().belt_printer.value) { //BBS: add logs BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": the same id %1%, return directly, result %2% ") % m_last_result_id % (&gcode_result); @@ -1172,8 +1223,18 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const return; } - // convert data from PrusaSlicer format to libvgcode format - libvgcode::GCodeInputData data = libvgcode::convert(gcode_result, str_tool_colors, str_color_print_colors, m_viewer); + // 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). + const bool is_belt = print.config().belt_printer.value; + const Transform3d belt_inv = (is_belt && m_belt_show_designed) + ? compute_belt_back_transform(print.config()) : Transform3d::Identity(); + const bool apply_belt = is_belt && m_belt_show_designed + && !belt_inv.matrix().isApprox(Transform3d::Identity().matrix()); + libvgcode::GCodeInputData data = libvgcode::convert(gcode_result, str_tool_colors, str_color_print_colors, m_viewer, + apply_belt ? &belt_inv : nullptr); //#define ENABLE_DATA_EXPORT 1 //#if ENABLE_DATA_EXPORT @@ -2303,12 +2364,12 @@ void GCodeViewer::render_toolpaths() { const Camera& camera = wxGetApp().plater()->get_camera(); Matrix4f view = camera.get_view_matrix().matrix().cast(); - // Belt "designed" view: apply the precomputed inverse of the full belt - // shear+scale transform so toolpaths appear upright (as originally designed) - // instead of transformed on the belt. - if (m_belt_show_designed && m_belt_view_enabled) { - view = (camera.get_view_matrix() * m_belt_inverse_transform).matrix().cast(); - } + // Belt "designed" (upright) view is now produced by back-transforming the + // toolpath GEOMETRY into model space at load time (see load_as_gcode -> + // libvgcode::convert with m_belt_inverse_transform). The camera is therefore + // left untouched here; transforming the view as well would double-apply the + // inverse. Keeping m_belt_inverse_transform on the geometry (not the camera) + // also keeps the bed and toolpaths in the same frame so they stay aligned. const libvgcode::Mat4x4 converted_view_matrix = libvgcode::convert(view); const libvgcode::Mat4x4 converted_projetion_matrix = libvgcode::convert(static_cast(camera.get_projection_matrix().matrix().cast())); #if VGCODE_ENABLE_COG_AND_TOOL_MARKERS diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index ca79a82584..d5173b02e5 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -245,7 +245,8 @@ 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 = false; // Toggle: show designed (upright) view via inverse shear + 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. Transform3d m_belt_inverse_transform{Transform3d::Identity()}; libvgcode::Viewer m_viewer; diff --git a/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp b/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp index 5b5c7da076..ffc549755b 100644 --- a/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp +++ b/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp @@ -189,10 +189,22 @@ Slic3r::PrintEstimatedStatistics::ETimeMode convert(const ETimeMode& mode) } GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::vector& str_tool_colors, - const std::vector& str_color_print_colors, const Viewer& viewer) + const std::vector& str_color_print_colors, const Viewer& viewer, + const Slic3r::Transform3d* belt_xform) { GCodeInputData ret; + // Belt printers: optionally map each vertex DISPLAY position from machine + // (G-code) space back to model/Cartesian space using the general belt + // back-transform (handles any mesh rotation + shear + axis remap, not just + // 45 deg). Only the rendered position is transformed; layer_id, times and + // the volumetric/flow math below keep the original machine-space values. + auto xform_pos = [belt_xform](const Slic3r::Vec3f& v) -> Vec3 { + if (belt_xform != nullptr) + return convert(Slic3r::Vec3f((*belt_xform * v.cast()).cast())); + return convert(v); + }; + // collect tool colors ret.tools_colors.reserve(str_tool_colors.size()); for (const std::string& color : str_tool_colors) { @@ -221,7 +233,7 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve // equal to the current one with the exception of the position, which should match the previous move position, // and the times, which are set to zero #if VGCODE_ENABLE_COG_AND_TOOL_MARKERS - const libvgcode::PathVertex vertex = { convert(prev.position), curr.height, curr.width, curr.feedrate, prev.actual_feedrate, + const libvgcode::PathVertex vertex = { xform_pos(prev.position), curr.height, curr.width, curr.feedrate, prev.actual_feedrate, curr.mm3_per_mm, curr.fan_speed, curr.temperature, 0.0f, convert(curr.extrusion_role), curr_type, static_cast(curr.gcode_id), static_cast(curr.layer_id), static_cast(curr.extruder_id), static_cast(curr.cp_color_id), { 0.0f, 0.0f }, @@ -229,7 +241,7 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve /* ORCA: Add Acceleration visualization support */ curr.acceleration, /* ORCA: Add Jerk visualization support */ curr.jerk }; #else - const libvgcode::PathVertex vertex = { convert(prev.position), curr.height, curr.width, curr.feedrate, prev.actual_feedrate, + const libvgcode::PathVertex vertex = { xform_pos(prev.position), curr.height, curr.width, curr.feedrate, prev.actual_feedrate, curr.mm3_per_mm, curr.fan_speed, curr.temperature, convert(curr.extrusion_role), curr_type, static_cast(curr.gcode_id), static_cast(curr.layer_id), static_cast(curr.extruder_id), static_cast(curr.cp_color_id), { 0.0f, 0.0f }, @@ -242,7 +254,7 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve } #if VGCODE_ENABLE_COG_AND_TOOL_MARKERS - const libvgcode::PathVertex vertex = { convert(curr.position), curr.height, curr.width, curr.feedrate, curr.actual_feedrate, + const libvgcode::PathVertex vertex = { xform_pos(curr.position), curr.height, curr.width, curr.feedrate, curr.actual_feedrate, curr.mm3_per_mm, curr.fan_speed, curr.temperature, result.filament_densities[curr.extruder_id] * curr.mm3_per_mm * (curr.position - prev.position).norm(), convert(curr.extrusion_role), curr_type, static_cast(curr.gcode_id), static_cast(curr.layer_id), @@ -251,7 +263,7 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve /* ORCA: Add Acceleration visualization support */ curr.acceleration, /* ORCA: Add Jerk visualization support */ curr.jerk }; #else - const libvgcode::PathVertex vertex = { convert(curr.position), curr.height, curr.width, curr.feedrate, curr.actual_feedrate, + const libvgcode::PathVertex vertex = { xform_pos(curr.position), curr.height, curr.width, curr.feedrate, curr.actual_feedrate, curr.mm3_per_mm, curr.fan_speed, curr.temperature, convert(curr.extrusion_role), curr_type, static_cast(curr.gcode_id), static_cast(curr.layer_id), static_cast(curr.extruder_id), static_cast(curr.cp_color_id), curr.time, @@ -263,6 +275,26 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve } ret.vertices.shrink_to_fit(); + // Belt designed view: the linear back-transform recovers the correct shape + // and orientation, but not the constant machine-frame origin offset baked + // into the G-code (e.g. the start-G-code belt advance + a G92 reset leaves a + // ~20 mm Z residual). Anchor the lowest extrusion to the belt entry (Y=0) so + // the toolpaths sit on the bed under the model shell. Independent of the + // offset's source, so it stays general across machines. + if (belt_xform != nullptr && !ret.vertices.empty()) { + // Anchor on the OBJECT extrusions only (layer_id >= 1): the start-G-code + // prime lines (layer 0) print at the belt origin, while the object prints + // after the start-G-code belt advance, so anchoring on the global min + // would lock onto the prime and leave the object offset. + float min_y = std::numeric_limits::max(); + for (const PathVertex& v : ret.vertices) + if (v.type == EMoveType::Extrude && v.layer_id >= 1 && v.position[1] < min_y) + min_y = v.position[1]; + if (min_y != std::numeric_limits::max() && std::abs(min_y) > 1e-3f) + for (PathVertex& v : ret.vertices) + v.position[1] -= min_y; + } + ret.spiral_vase_mode = result.spiral_vase_mode; return ret; diff --git a/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.hpp b/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.hpp index 35b5bd7b44..872f297ae6 100644 --- a/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.hpp +++ b/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.hpp @@ -71,7 +71,8 @@ extern Slic3r::PrintEstimatedStatistics::ETimeMode convert(const ETimeMode& mode // mapping from Slic3r::GCodeProcessorResult to libvgcode::GCodeInputData extern GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::vector& str_tool_colors, - const std::vector& str_color_print_colors, const Viewer& viewer); + const std::vector& str_color_print_colors, const Viewer& viewer, + const Slic3r::Transform3d* belt_xform = nullptr); // mapping from Slic3r::Print to libvgcode::GCodeInputData extern GCodeInputData convert(const Slic3r::Print& print, const std::vector& str_tool_colors, From 3fc3b8a8aedae1042cfe8f8166c51f3c0b25351f Mon Sep 17 00:00:00 2001 From: Tommaso Bianchi Date: Sat, 6 Jun 2026 19:39:57 +0200 Subject: [PATCH 053/224] belt: anchor the designed-view G-code preview onto the model bounding box MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The belt designed (upright) preview back-transforms the machine-frame G-code into model space with the linear belt inverse. That inverse recovers the print's shape and orientation, but not the per-object placement/lift translation: the object's position on the belt, the BeltSliceStrategy min-Z lift, and the centering pre-translate are applied OUTSIDE build_forward_transform() (see PrintObjectSlice.cpp), so its linear inverse cannot undo them. The result was a constant offset (~20 mm on the belt-advance axis) of the toolpaths from the model shell, on every model. Recover the missing translation generally — independent of the offset's exact source or the axis remap — by anchoring the back-transformed object body (extrusions on layer_id >= 1, i.e. excluding the layer-0 prime/skirt) onto the upright model bounding box, the same space the shells render in, and folding that translation into the belt inverse before converting to libvgcode. Replaces the previous Y=0 anchoring in LibVGCodeWrapper, which pinned the toolpaths to the belt entry rather than to the model and so left the offset in place for any object not sitting at the origin. --- src/slic3r/GUI/GCodeViewer.cpp | 28 ++++++++++++++++++- src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp | 23 +++------------ 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 963730e093..9baced5d78 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1229,10 +1229,36 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const // inverse (handles any mesh rotation + shear + axis remap). When off, the raw // machine-frame G-code is shown (useful for debugging the transform itself). const bool is_belt = print.config().belt_printer.value; - const Transform3d belt_inv = (is_belt && m_belt_show_designed) + Transform3d belt_inv = (is_belt && m_belt_show_designed) ? compute_belt_back_transform(print.config()) : Transform3d::Identity(); const bool apply_belt = is_belt && m_belt_show_designed && !belt_inv.matrix().isApprox(Transform3d::Identity().matrix()); + if (apply_belt) { + // The linear belt back-transform recovers the print's shape and orientation but not + // the per-object placement/lift translation: the object's position on the belt, the + // BeltSliceStrategy min-Z lift, and the centering pre-translate are applied OUTSIDE + // build_forward_transform() (see PrintObjectSlice.cpp), so its linear inverse leaves + // them un-undone. Uncorrected, the upright toolpaths float a fixed offset from the + // model shell. Recover the translation generally — independent of the offset's exact + // source or the axis remap — by anchoring the back-transformed object body onto the + // upright model bounding box (the same space the shells render in). + BoundingBoxf3 model_bb; + for (const PrintObject* po : print.objects()) + for (const ModelInstance* mi : po->model_object()->instances) + model_bb.merge(po->model_object()->instance_bounding_box(*mi)); + BoundingBoxf3 tp_bb; + for (const GCodeProcessorResult::MoveVertex& mv : gcode_result.moves) + if (mv.type == EMoveType::Extrude && mv.layer_id >= 1) // skip layer-0 prime/skirt + tp_bb.merge(Vec3d(belt_inv * mv.position.cast())); + if (model_bb.defined && tp_bb.defined) { + const Vec3d d = model_bb.min - tp_bb.min; + belt_inv = Transform3d(Eigen::Translation3d(d)) * belt_inv; + BOOST_LOG_TRIVIAL(debug) << "[BELT-PREVIEW] anchor: model_bb.min=(" + << model_bb.min.x() << "," << model_bb.min.y() << "," << model_bb.min.z() + << ") tp_bb.min=(" << tp_bb.min.x() << "," << tp_bb.min.y() << "," << tp_bb.min.z() + << ") d=(" << d.x() << "," << d.y() << "," << d.z() << ")"; + } + } libvgcode::GCodeInputData data = libvgcode::convert(gcode_result, str_tool_colors, str_color_print_colors, m_viewer, apply_belt ? &belt_inv : nullptr); diff --git a/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp b/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp index ffc549755b..015947e9bb 100644 --- a/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp +++ b/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp @@ -275,25 +275,10 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve } ret.vertices.shrink_to_fit(); - // Belt designed view: the linear back-transform recovers the correct shape - // and orientation, but not the constant machine-frame origin offset baked - // into the G-code (e.g. the start-G-code belt advance + a G92 reset leaves a - // ~20 mm Z residual). Anchor the lowest extrusion to the belt entry (Y=0) so - // the toolpaths sit on the bed under the model shell. Independent of the - // offset's source, so it stays general across machines. - if (belt_xform != nullptr && !ret.vertices.empty()) { - // Anchor on the OBJECT extrusions only (layer_id >= 1): the start-G-code - // prime lines (layer 0) print at the belt origin, while the object prints - // after the start-G-code belt advance, so anchoring on the global min - // would lock onto the prime and leave the object offset. - float min_y = std::numeric_limits::max(); - for (const PathVertex& v : ret.vertices) - if (v.type == EMoveType::Extrude && v.layer_id >= 1 && v.position[1] < min_y) - min_y = v.position[1]; - if (min_y != std::numeric_limits::max() && std::abs(min_y) > 1e-3f) - for (PathVertex& v : ret.vertices) - v.position[1] -= min_y; - } + // Note: the belt designed-view anchoring (recovering the per-object placement/ + // lift translation the linear back-transform cannot) is folded into belt_xform + // by the caller (GCodeViewer::load_as_gcode), which anchors onto the upright + // model bounding box. Nothing extra to do here. ret.spiral_vase_mode = result.spiral_vase_mode; From 8593d66a3998de8e87886251493f6d10bbc26eef Mon Sep 17 00:00:00 2001 From: Tommaso Bianchi Date: Sun, 7 Jun 2026 09:57:12 +0200 Subject: [PATCH 054/224] belt: correct the designed-view preview's belt-Z origin and reject mis-mapped outliers The Cartesian designed-view preview over-extended the toolpaths past the model shell by a height-proportional amount (up to ~20mm tall parts), most visibly on long multi-part prints; compact parts like a calibration cube looked fine. Two coupled causes: - Belt start G-code that primes with a Z advance and a 'G92 Z0' reset leaves a constant machine-Z origin in the GCodeProcessor, so move positions are stored as gcode_Z + origin. The linear back-transform mixes that constant with the gantry-Y term, leaving a per-move designed-Y error that min-corner anchoring cannot cancel when an elevated move (e.g. a bridge) happens to cancel it at the bbox minimum. Expose GCodeProcessorResult::belt_z_origin (the m_origin[Z] left by the start G-code) and subtract it before the back-transform. - Elevated features (bridges/overhangs) are mis-mapped by the linear inverse to outside the model body; build the anchor bbox only from moves within model_bb +/- 10mm, with a fallback to the full bbox when the clip would drop the bulk (object placed away from the belt entry) so the gross-offset case still anchors. Preview-only; G-code output is unchanged. --- src/libslic3r/GCode/GCodeProcessor.cpp | 7 ++++ src/libslic3r/GCode/GCodeProcessor.hpp | 6 ++++ src/slic3r/GUI/GCodeViewer.cpp | 47 ++++++++++++++++++++++---- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 003c0af05a..b15736ef57 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -4933,6 +4933,13 @@ void GCodeProcessor::process_G92(const GCodeReader::GCodeLine& line) if (line.has_z()) { m_origin[Z] = m_end_position[Z] - line.z() * lengths_scale_factor; any_found = true; + // Belt only: the start G-code's purge-blob advance + G92 Z0 resets leave a constant + // machine-Z origin offset here; the designed-view back-transform subtracts it so + // toolpaths map to the model's belt coordinate (gcode Z). Gated on belt_tilt_angle + // (set from the belt header, parsed before the body) so non-belt G-code processing + // is byte-identical — no unconditional work on the shared path. + if (m_result.belt_tilt_angle != 0.f) + m_result.belt_z_origin = m_origin[Z]; } if (line.has_e()) { diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index 3047c005ed..8fcf2e0c43 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -248,6 +248,11 @@ class Print; // Belt printer: physical tilt magnitude (deg) parsed from the slicing-rotation // header comment; used to enable the preview's belt view. float belt_tilt_angle{ 0.f }; + // Belt printer: machine-Z origin offset (mm) left in m_origin[Z] by the start + // G-code (purge-blob belt advance + G92 Z0 resets). Move positions are stored + // as gcode_Z + this offset, so the designed-view back-transform must subtract it + // to recover the model's belt coordinate. + float belt_z_origin{ 0.f }; RemapAxis preslice_remap_x{ RemapAxis::PosX }; RemapAxis preslice_remap_y{ RemapAxis::PosY }; RemapAxis preslice_remap_z{ RemapAxis::PosZ }; @@ -317,6 +322,7 @@ class Print; filament_change_count_map = other.filament_change_count_map; initial_layer_time = other.initial_layer_time; belt_tilt_angle = other.belt_tilt_angle; + belt_z_origin = other.belt_z_origin; preslice_remap_x = other.preslice_remap_x; preslice_remap_y = other.preslice_remap_y; preslice_remap_z = other.preslice_remap_z; diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 9baced5d78..1263e3fed6 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1231,6 +1231,14 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const const bool is_belt = print.config().belt_printer.value; Transform3d belt_inv = (is_belt && m_belt_show_designed) ? 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) + 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 && !belt_inv.matrix().isApprox(Transform3d::Identity().matrix()); if (apply_belt) { @@ -1246,17 +1254,42 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const for (const PrintObject* po : print.objects()) for (const ModelInstance* mi : po->model_object()->instances) model_bb.merge(po->model_object()->instance_bounding_box(*mi)); - BoundingBoxf3 tp_bb; + // Build the anchor bbox from surface toolpaths only. After the belt_z_origin + // correction the surface back-transforms onto the model, but a few elevated + // features (bridges/overhangs over the chevron gap) are mis-mapped by the linear + // inverse to well outside the model body; if one becomes the bbox minimum it + // drags the min-corner anchor by ~20mm. Drop moves that land clearly outside the + // (correct) model bbox — a geometric filter, not a role guess. + BoundingBoxf3 tp_bb_clip, tp_bb_full; + const double y_lo = model_bb.defined ? model_bb.min.y() - 10.0 : -1e30; + const double y_hi = model_bb.defined ? model_bb.max.y() + 10.0 : 1e30; for (const GCodeProcessorResult::MoveVertex& mv : gcode_result.moves) - if (mv.type == EMoveType::Extrude && mv.layer_id >= 1) // skip layer-0 prime/skirt - tp_bb.merge(Vec3d(belt_inv * mv.position.cast())); + if (mv.type == EMoveType::Extrude && mv.layer_id >= 1) { // skip layer-0 prime/skirt + const Vec3d p = belt_inv * mv.position.cast(); + tp_bb_full.merge(p); + if (p.y() >= y_lo && p.y() <= y_hi) + tp_bb_clip.merge(p); + } + // Use the clipped bbox only when it still holds the bulk of the body (outliers + // removed). If the object sits far from the belt entry the toolpaths are grossly + // offset and the clip would drop most of them — fall back to the full bbox so the + // min-corner anchor still recovers that gross translation rather than breaking. + const BoundingBoxf3& tp_bb = + (tp_bb_clip.defined && tp_bb_full.defined && + tp_bb_clip.size().y() >= 0.5 * tp_bb_full.size().y()) ? tp_bb_clip : tp_bb_full; if (model_bb.defined && tp_bb.defined) { + // Anchor the back-transformed toolpath body onto the upright model bbox by + // its MIN corner. (Center anchoring was tried and regressed when the toolpath + // and model bounding boxes differ in extent.) With the belt_z_origin + // correction above and the outlier-robust bbox below, the surface overlaps the + // shell to well under a millimetre when the object is at the belt entry; an + // object placed elsewhere in global-rotation mode still carries the placement + // translation, which this min-corner step recovers. const Vec3d d = model_bb.min - tp_bb.min; belt_inv = Transform3d(Eigen::Translation3d(d)) * belt_inv; - BOOST_LOG_TRIVIAL(debug) << "[BELT-PREVIEW] anchor: model_bb.min=(" - << model_bb.min.x() << "," << model_bb.min.y() << "," << model_bb.min.z() - << ") tp_bb.min=(" << tp_bb.min.x() << "," << tp_bb.min.y() << "," << tp_bb.min.z() - << ") d=(" << d.x() << "," << d.y() << "," << d.z() << ")"; + BOOST_LOG_TRIVIAL(debug) << "[BELT-PREVIEW] z_origin=" << gcode_result.belt_z_origin + << " model_bb.y=[" << model_bb.min.y() << "," << model_bb.max.y() + << "] tp_bb.y=[" << tp_bb.min.y() << "," << tp_bb.max.y() << "] d.y=" << d.y(); } } libvgcode::GCodeInputData data = libvgcode::convert(gcode_result, str_tool_colors, str_color_print_colors, m_viewer, From a4fb5af9e1c75bf66c1b6d488d4c9ac00c236e4d Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Sun, 21 Jun 2026 18:20:58 +0800 Subject: [PATCH 055/224] Don't allow adding more colors for non-semm printers on obj import color remapping dialog (#14275) --- src/slic3r/GUI/ObjColorDialog.cpp | 9 ++++++--- src/slic3r/GUI/ObjColorDialog.hpp | 5 +++-- src/slic3r/GUI/Plater.cpp | 4 ++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/slic3r/GUI/ObjColorDialog.cpp b/src/slic3r/GUI/ObjColorDialog.cpp index a8ef3f046f..47f62aa168 100644 --- a/src/slic3r/GUI/ObjColorDialog.cpp +++ b/src/slic3r/GUI/ObjColorDialog.cpp @@ -79,7 +79,7 @@ bool ObjColorDialog::Show(bool show) { } }; -ObjColorDialog::ObjColorDialog(wxWindow *parent, Slic3r::ObjDialogInOut &in_out, const std::vector &extruder_colours) +ObjColorDialog::ObjColorDialog(wxWindow *parent, Slic3r::ObjDialogInOut &in_out, const std::vector &extruder_colours, const bool semm) : DPIDialog(parent ? parent : static_cast(wxGetApp().mainframe), wxID_ANY, _(L("OBJ file import color")), @@ -109,7 +109,7 @@ ObjColorDialog::ObjColorDialog(wxWindow *parent, Slic3r::ObjDialogInOut &in_out, } bool ok = in_out.lost_material_name.empty() && !some_face_no_color; if (ok) { - m_panel_ObjColor = new ObjColorPanel(this, in_out, extruder_colours); + m_panel_ObjColor = new ObjColorPanel(this, in_out, extruder_colours, semm); m_panel_ObjColor->set_layout_callback([this]() { update_layout(); }); m_main_sizer->Add(m_panel_ObjColor, 1, wxEXPAND | wxALL, 0); } @@ -178,12 +178,13 @@ ObjColorDialog::ObjColorDialog(wxWindow *parent, Slic3r::ObjDialogInOut &in_out, } // This panel contains all control widgets for both simple and advanced mode (these reside in separate sizers) -ObjColorPanel::ObjColorPanel(wxWindow *parent, Slic3r::ObjDialogInOut &in_out, const std::vector &extruder_colours) +ObjColorPanel::ObjColorPanel(wxWindow *parent, Slic3r::ObjDialogInOut &in_out, const std::vector &extruder_colours, const bool semm) : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize /*,wxBORDER_RAISED*/) , m_obj_in_out(in_out) , m_input_colors(in_out.input_colors) , m_filament_ids(in_out.filament_ids) , m_first_extruder_id(in_out.first_extruder_id) + , m_semm(semm) { if (in_out.input_colors.size() == 0) { return; } for (const std::string& color : extruder_colours) { @@ -504,6 +505,7 @@ wxBoxSizer *ObjColorPanel::create_add_btn_sizer(wxWindow *parent) deal_add_btn(); deal_thumbnail(); }); + m_quick_add_btn->Enable(m_semm); // only SEMM printer can add more colors return btn_sizer; } @@ -859,6 +861,7 @@ bool ObjColorPanel::do_show(bool show) { bool ObjColorPanel::deal_add_btn() { + if (!m_semm) { return false; }// only SEMM printer can add more colors if (m_colours.size() > g_max_color) { return false; } deal_reset_btn(); std::vector new_icons; diff --git a/src/slic3r/GUI/ObjColorDialog.hpp b/src/slic3r/GUI/ObjColorDialog.hpp index bac3a80519..1ad74cdccd 100644 --- a/src/slic3r/GUI/ObjColorDialog.hpp +++ b/src/slic3r/GUI/ObjColorDialog.hpp @@ -20,7 +20,7 @@ class ObjColorPanel : public wxPanel { public: // BBS - ObjColorPanel(wxWindow *parent, Slic3r::ObjDialogInOut &in_out, const std::vector &extruder_colours); + ObjColorPanel(wxWindow *parent, Slic3r::ObjDialogInOut &in_out, const std::vector &extruder_colours, bool semm); ~ObjColorPanel(); void msw_rescale(); bool is_ok(); @@ -90,6 +90,7 @@ private: int m_color_num_recommend{0}; int m_color_cluster_num_by_algo{0}; int m_input_colors_size{0}; + bool m_semm; // only SEMM printer can add more colors std::vector m_colours;//from project and show right std::vector m_cluster_map_filaments;//show middle int m_max_filament_index = 0; @@ -112,7 +113,7 @@ private: class ObjColorDialog : public Slic3r::GUI::DPIDialog { public: - ObjColorDialog(wxWindow *parent, Slic3r::ObjDialogInOut &in_out, const std::vector &extruder_colours); + ObjColorDialog(wxWindow *parent, Slic3r::ObjDialogInOut &in_out, const std::vector &extruder_colours, bool semm); wxBoxSizer *create_btn_sizer(long flags, bool exist_error); void on_dpi_changed(const wxRect &suggested_rect) override; void update_layout(); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 87be0e52f4..2dc92f3b4b 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -6554,7 +6554,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ if (!boost::iends_with(path.string(), ".obj")) { return; } const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(); - ObjColorDialog color_dlg(nullptr, in_out, extruder_colours); + ObjColorDialog color_dlg(nullptr, in_out, extruder_colours, Sidebar::should_show_SEMM_buttons()); if (color_dlg.ShowModal() != wxID_OK) { in_out.filament_ids.clear(); } @@ -8816,7 +8816,7 @@ void Plater::priv::reload_from_disk() auto obj_color_fun = [this, &path](ObjDialogInOut &in_out) { if (!boost::iends_with(path, ".obj")) { return; } const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(); - ObjColorDialog color_dlg(nullptr, in_out, extruder_colours); + ObjColorDialog color_dlg(nullptr, in_out, extruder_colours, Sidebar::should_show_SEMM_buttons()); if (color_dlg.ShowModal() != wxID_OK) { in_out.filament_ids.clear(); } From 07f08dfe40f925904c5384be7b36ec554012d551 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Mon, 22 Jun 2026 00:50:51 +0800 Subject: [PATCH 056/224] bump version to 2.5.0-dev --- scripts/flatpak/com.orcaslicer.OrcaSlicer.metainfo.xml | 2 +- version.inc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/flatpak/com.orcaslicer.OrcaSlicer.metainfo.xml b/scripts/flatpak/com.orcaslicer.OrcaSlicer.metainfo.xml index 9ec11fa930..c8e4d401b8 100644 --- a/scripts/flatpak/com.orcaslicer.OrcaSlicer.metainfo.xml +++ b/scripts/flatpak/com.orcaslicer.OrcaSlicer.metainfo.xml @@ -45,7 +45,7 @@ #00695C - + https://github.com/OrcaSlicer/OrcaSlicer/releases/tag/nightly-builds

See the release page for detailed changelog.

diff --git a/version.inc b/version.inc index c283ecbad6..7ddaad214e 100644 --- a/version.inc +++ b/version.inc @@ -7,7 +7,7 @@ set(SLIC3R_APP_KEY "OrcaSlicer") if(NOT DEFINED BBL_INTERNAL_TESTING) set(BBL_INTERNAL_TESTING "0") endif() -set(SoftFever_VERSION "2.4.0-dev") +set(SoftFever_VERSION "2.5.0-dev") string(REGEX MATCH "^([0-9]+)\\.([0-9]+)\\.([0-9]+)" SoftFever_VERSION_MATCH ${SoftFever_VERSION}) set(ORCA_VERSION_MAJOR ${CMAKE_MATCH_1}) From 86c6a1a66fca360068a96225bb7dd72ef8056cac Mon Sep 17 00:00:00 2001 From: Heiko Liebscher Date: Mon, 22 Jun 2026 20:40:14 +0200 Subject: [PATCH 057/224] Improve German (de) translation (#14352) Co-authored-by: Claude Opus 4.8 (1M context) --- localization/i18n/de/OrcaSlicer_de.po | 208 +++++++++++++------------- 1 file changed, 108 insertions(+), 100 deletions(-) diff --git a/localization/i18n/de/OrcaSlicer_de.po b/localization/i18n/de/OrcaSlicer_de.po index edd14a18e2..4a6ce9c11f 100644 --- a/localization/i18n/de/OrcaSlicer_de.po +++ b/localization/i18n/de/OrcaSlicer_de.po @@ -27,7 +27,7 @@ msgid "left extruder" msgstr "linker Extruder" msgid "extruder" -msgstr "" +msgstr "Extruder" msgid "TPU is not supported by AMS." msgstr "TPU wird vom AMS nicht unterstützt." @@ -85,7 +85,7 @@ msgid "Serial:" msgstr "Seriennummer:" msgid "Version:" -msgstr "" +msgstr "Version:" msgid "Latest version" msgstr "Neueste Version" @@ -246,7 +246,7 @@ msgid "Reset" msgstr "Zurücksetzen" msgid "Enter" -msgstr "" +msgstr "Eingabe" msgid "Shortcut Key " msgstr "Tastenkürzel" @@ -261,7 +261,7 @@ msgid "Vertical" msgstr "Vertikal" msgid "Horizontal" -msgstr "" +msgstr "Horizontal" msgid "Remove painted color" msgstr "Gemalte Farbe entfernen" @@ -358,7 +358,7 @@ msgid "Single sided scaling" msgstr "Einseitige Skalierung" msgid "Position" -msgstr "" +msgstr "Position" msgid "Rotate (relative)" msgstr "Rotation (relativ)" @@ -426,7 +426,7 @@ msgid "Uniform scale" msgstr "einheitliche Skalierung" msgid "Planar" -msgstr "" +msgstr "Planar" msgid "Dovetail" msgstr "Schwalbenschwanz" @@ -450,7 +450,7 @@ msgid "Prism" msgstr "Prisma" msgid "Frustum" -msgstr "" +msgstr "Frustum" msgid "Square" msgstr "Quadrat" @@ -863,19 +863,19 @@ msgid "Emboss" msgstr "Prägen" msgid "NORMAL" -msgstr "" +msgstr "Normal" msgid "SMALL" msgstr "Schmal" msgid "ITALIC" -msgstr "kursiv" +msgstr "Kursiv" msgid "SWISS" -msgstr "" +msgstr "Swiss" msgid "MODERN" -msgstr "" +msgstr "Modern" msgid "First font" msgstr "erste Schriftart" @@ -964,7 +964,7 @@ msgid "Name has to be unique." msgstr "Name muss eindeutig sein." msgid "OK" -msgstr "" +msgstr "OK" msgid "Rename style" msgstr "Benenne Stil um" @@ -1458,7 +1458,7 @@ msgid "Restart selection" msgstr "Auswahl neu starten" msgid "Esc" -msgstr "" +msgstr "Esc" msgid "Cancel a feature until exit" msgstr "Abbrechen einer Funktion bis zum Verlassen" @@ -1592,6 +1592,9 @@ msgid "" "because they are restricted to the bed \n" "and only parts can be lifted." msgstr "" +"Es wird empfohlen, die Objekte zuerst zusammenzubauen,\n" +"da sie auf das Druckbett beschränkt sind \n" +"und nur Teile angehoben werden können." msgid "Face and face assembly" msgstr "Face und Face Zusammenbau" @@ -1629,7 +1632,7 @@ msgid "Process" msgstr "Prozess" msgid "Filament" -msgstr "" +msgstr "Filament" msgid "Machine" msgstr "Maschine" @@ -1769,7 +1772,7 @@ msgid "This is the newest version." msgstr "Dies ist die neueste Version." msgid "Info" -msgstr "" +msgstr "Info" msgid "Loading printer & filament profiles" msgstr "Lade Drucker- und Filamentprofile" @@ -1817,7 +1820,7 @@ msgid "Choose one file (GCODE/3MF):" msgstr "Wählen sie eine Datei (GCODE/3MF):" msgid "Ext" -msgstr "" +msgstr "Ext" msgid "Some presets are modified." msgstr "Einige Profileinstellungen wurden geändert." @@ -2087,7 +2090,7 @@ msgid "Ironing" msgstr "Glätten" msgid "Fuzzy skin" -msgstr "" +msgstr "Fuzzy skin" msgid "Extruders" msgstr "Extruder" @@ -2174,7 +2177,7 @@ msgid "Disc" msgstr "Scheibe" msgid "Torus" -msgstr "" +msgstr "Torus" msgid "Orca Cube" msgstr "Orca Würfel" @@ -2310,7 +2313,7 @@ msgstr "Standard" #, c-format, boost-format msgid "Filament %d" -msgstr "" +msgstr "Filament %d" msgid "current" msgstr "Aktuell" @@ -2571,10 +2574,10 @@ msgid "Edit Plate Name" msgstr "Ändere Plattenname" msgid "Name" -msgstr "" +msgstr "Name" msgid "Fila." -msgstr "" +msgstr "Fila." #, c-format, boost-format msgid "%1$d error repaired" @@ -2868,7 +2871,7 @@ msgid "Color" msgstr "Farbe" msgid "Pause" -msgstr "" +msgstr "Pause" msgid "Template" msgstr "Vorlage" @@ -2877,7 +2880,7 @@ msgid "Custom" msgstr "Benutzerdefiniert" msgid "Pause:" -msgstr "" +msgstr "Pause:" msgid "Custom Template:" msgstr "Benutzerdefinierte Vorlage:" @@ -2918,7 +2921,7 @@ msgid "Insert template custom G-code at the beginning of this layer." msgstr "Fügen Sie am Anfang dieses Layers benutzerdefinierten G-Code aus einer Vorlage ein." msgid "Filament " -msgstr "" +msgstr "Filament " msgid "Change filament at the beginning of this layer." msgstr "Wechseln Sie am Anfang dieses Layers das Filament." @@ -2951,7 +2954,7 @@ msgid "Check the status of current system services" msgstr "Überprüfen Sie den Status der aktuellen Systemdienste" msgid "code" -msgstr "" +msgstr "Code" msgid "Failed to connect to cloud service" msgstr "Verbindung zum Cloud-Dienst fehlgeschlagen" @@ -2998,7 +3001,7 @@ msgid "Off" msgstr "Aus" msgid "Filter" -msgstr "" +msgstr "Filter" msgid "Enabling filtration redirects the right fan to filter gas, which may reduce cooling performance." msgstr "Die Aktivierung der Filtration leitet den rechten Lüfter um, um Gas zu filtern, was die Kühlleistung verringern kann." @@ -3068,13 +3071,13 @@ msgid "Parts" msgstr "Teile" msgid "Aux" -msgstr "" +msgstr "Aux" msgid "Nozzle1" msgstr "Düse1" msgid "MC Board" -msgstr "" +msgstr "MC Board" msgid "Heat" msgstr "Heizen" @@ -3161,7 +3164,7 @@ msgid "Developer mode" msgstr "Entwicklermodus" msgid "Launch troubleshoot center" -msgstr "" +msgstr "Fehlerbehebungszentrum starten" msgid "" "All the selected objects are on a locked plate.\n" @@ -3472,7 +3475,7 @@ msgid "Orca Slicer is licensed under " msgstr "Orca Slicer ist lizenziert unter " msgid "GNU Affero General Public License, version 3" -msgstr "" +msgstr "GNU Affero General Public License, Version 3" msgid "Orca Slicer is based on PrusaSlicer and BambuStudio" msgstr "Orca Slicer basiert auf PrusaSlicer und BambuStudio" @@ -3497,7 +3500,7 @@ msgid "Today, OrcaSlicer is the most widely used and actively developed open-sou msgstr "Heute ist OrcaSlicer der am weitesten verbreitete und aktiv entwickelte Open-Source-Slicer in der 3D-Druck-Community. Viele seiner Innovationen wurden von anderen Slicern übernommen und treiben die gesamte Industrie voran." msgid "Version" -msgstr "" +msgstr "Version" msgid "AMS Materials Setting" msgstr "AMS Material Einstellung" @@ -3514,17 +3517,17 @@ msgid "" msgstr "Düsentemperatur" msgid "max" -msgstr "" +msgstr "max" msgid "min" -msgstr "" +msgstr "min" #, boost-format msgid "The input value should be greater than %1% and less than %2%" msgstr "Der Eingabewert sollte größer als %1% und kleiner als %2% sein" msgid "SN" -msgstr "" +msgstr "SN" msgid "Factors of Flow Dynamics Calibration" msgstr "Dynamische Flusskalibrierungsfaktoren" @@ -3671,7 +3674,7 @@ msgstr "" "Und Sie können nicht darauf klicken, um es zu ändern" msgid "AMS Slots" -msgstr "" +msgstr "AMS-Slots" msgid "Please select from the following filaments" msgstr "Bitte wählen Sie aus den folgenden Filamenten" @@ -3827,7 +3830,7 @@ msgid "AMS will attempt to estimate the remaining capacity of the Bambu Lab fila msgstr "AMS versucht, die verbleibende Kapazität der Bambu Lab Filamente zu schätzen." msgid "AMS filament backup" -msgstr "" +msgstr "AMS-Filament-Backup" msgid "AMS will continue to another spool with matching filament properties automatically when current filament runs out." msgstr "AMS wechselt automatisch zu einer anderen Spule mit denselben Filamenteigenschaften, wenn das aktuelle Filament leer ist." @@ -4246,7 +4249,7 @@ msgid "Changing filament" msgstr "Filament wechseln" msgid "M400 pause" -msgstr "" +msgstr "M400 Pause" msgid "Paused (filament ran out)" msgstr "Pausiert (Filament leer)" @@ -4663,7 +4666,7 @@ msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Ungültiges Format. Erwartetes Vektorformat: \"%1%\"" msgid "N/A" -msgstr "" +msgstr "Nicht verfügbar" msgid "Pick" msgstr "Wähle" @@ -4980,7 +4983,7 @@ msgid "Options" msgstr "Optionen" msgid "Extruder" -msgstr "" +msgstr "Extruder" msgid "Cost" msgstr "Kosten" @@ -5052,7 +5055,7 @@ msgid "Smooth" msgstr "Höhenunterschiede glätten" msgid "Radius" -msgstr "" +msgstr "Radius" msgid "Keep min" msgstr "Minimum beibehalten" @@ -6044,7 +6047,7 @@ msgid "Switch to timelapse files." msgstr "Wechseln zu Zeitrafferdateien." msgid "Video" -msgstr "" +msgstr "Video" msgid "Switch to video files." msgstr "Wechseln Sie zu Videodateien." @@ -6247,7 +6250,7 @@ msgid "Invert Roll axis" msgstr "Invertiere Roll-Achse" msgid "(LAN)" -msgstr "" +msgstr "(LAN)" msgid "Search" msgstr "Suche" @@ -6259,7 +6262,7 @@ msgid "Other Device" msgstr "Anderes Gerät" msgid "Online" -msgstr "" +msgstr "Online" msgid "Input access code" msgstr "Zugangscode eingeben" @@ -6272,7 +6275,7 @@ msgid "Log out successful." msgstr "Abmeldung erfolgreich." msgid "Offline" -msgstr "" +msgstr "Offline" msgid "Busy" msgstr "Beschäftigt" @@ -6328,7 +6331,7 @@ msgid "Parts Skip" msgstr "Teile überspringen" msgid "Stop" -msgstr "" +msgstr "Stopp" msgid "Layer: N/A" msgstr "Schicht: N/A" @@ -6464,10 +6467,10 @@ msgid "Silent" msgstr "Leise" msgid "Standard" -msgstr "" +msgstr "Standard" msgid "Sport" -msgstr "" +msgstr "Sport" msgid "Ludicrous" msgstr "Verrückt" @@ -6630,7 +6633,7 @@ msgstr "" #, c-format, boost-format msgid "%s information" -msgstr "" +msgstr "%s Informationen" msgid "Skip" msgstr "Überspringen" @@ -6702,7 +6705,7 @@ msgid "New network plug-in available." msgstr "Neues Netzwerk-Plugin verfügbar" msgid "Details" -msgstr "" +msgstr "Details" msgid "New printer config available." msgstr "Neue Druckerkonfiguration verfügbar." @@ -7553,7 +7556,7 @@ msgid "Importing to Orca Slicer failed. Please download the file and manually im msgstr "Der Import in Orca Slicer ist fehlgeschlagen. Bitte laden Sie die Datei manuell herunter und importieren Sie sie." msgid "INFO:" -msgstr "" +msgstr "INFO:" msgid "No accelerations provided for calibration. Use default acceleration value " msgstr "Keine Beschleunigungen für die Kalibrierung bereitgestellt. Verwenden Sie den Standardbeschleunigungswert" @@ -8075,7 +8078,7 @@ msgid "With this option enabled, you can print materials with a large temperatur msgstr "Mit dieser Option können Sie Materialien mit großen Temperaturunterschieden zusammen drucken." msgid "Touchpad" -msgstr "" +msgstr "Touchpad" msgid "Camera style" msgstr "Kamerastil" @@ -8177,13 +8180,16 @@ msgid "Renders cast shadows on the plate in realistic view." msgstr "Zeigt geworfene Schatten auf der Platte in der realistischen Ansicht an." msgid "Smooth normals" -msgstr "" +msgstr "Glatte Normalen" msgid "" "Applies smooth normals to the realistic view.\n" "\n" "Requires manual scene reload to take effect (right-click on 3D view → \"Reload All\")." msgstr "" +"Wendet glatte Normalen auf die realistische Ansicht an.\n" +"\n" +"Erfordert manuelles Neuladen der Szene, um wirksam zu werden (Rechtsklick auf 3D-Ansicht → \"Alle neu laden\")." msgid "Anti-aliasing" msgstr "Kantenglättung" @@ -8246,7 +8252,7 @@ msgid "Displays current viewport FPS in the top-right corner." msgstr "Zeigt die aktuelle FPS des Viewports in der oberen rechten Ecke an." msgid "Login region" -msgstr "" +msgstr "Anmeldungsregion" msgid "Stealth mode" msgstr "Unsichtbarer Modus" @@ -8268,7 +8274,7 @@ msgid "Network test" msgstr "Netzwerktest" msgid "Test" -msgstr "" +msgstr "Test" msgid "Cloud Providers" msgstr "Cloud-Anbieter" @@ -8314,10 +8320,10 @@ msgid "Color only" msgstr "Nur Farbe" msgid "Bambu network plug-in" -msgstr "" +msgstr "Bambu Netzwerk-Plugin" msgid "Enable Bambu network plug-in" -msgstr "" +msgstr "Bambu Netzwerk-Plugin aktivieren" msgid "Network plug-in version" msgstr "Netzwerk-Plugin-Version" @@ -8552,7 +8558,7 @@ msgid "Left filaments" msgstr "linke Filamente" msgid "AMS filament" -msgstr "" +msgstr "AMS-Filament" msgid "Right filaments" msgstr "rechte Filamente" @@ -8576,7 +8582,7 @@ msgid "Bundle presets" msgstr "Gebündelte Profile" msgid "System" -msgstr "" +msgstr "System" msgid "Unsupported presets" msgstr "Nicht unterstützte Profile" @@ -9431,7 +9437,7 @@ msgid "Tree supports" msgstr "Baumstützen" msgid "Multimaterial" -msgstr "" +msgstr "Multimaterial" msgid "Filament for Features" msgstr "Filament für Funktionen" @@ -9567,10 +9573,10 @@ msgid "Complete print" msgstr "Druck abgeschlossen" msgid "Filament start G-code" -msgstr "" +msgstr "Filament Start G-Code" msgid "Filament end G-code" -msgstr "" +msgstr "Filament Ende G-Code" msgid "Wipe tower parameters" msgstr "Reinigungsturm-Parameter" @@ -9662,7 +9668,7 @@ msgid "Motion ability" msgstr "Bewegungseinstellung" msgid "Normal" -msgstr "" +msgstr "Normal" msgid "Resonance Compensation" msgstr "Resonanzkompensation" @@ -10148,7 +10154,7 @@ msgstr "" msgctxt "Sync_AMS" msgid "Original" -msgstr "" +msgstr "Original" msgid "After mapping" msgstr "Nach dem Zuordnen" @@ -10277,7 +10283,7 @@ msgstr "Farbe und Typ des Filaments erfolgreich vom Drucker synchronisiert." msgctxt "FinishSyncAms" msgid "OK" -msgstr "" +msgstr "OK" msgid "Ramming customization" msgstr "Ramming-Anpassung" @@ -10571,7 +10577,7 @@ msgid "Support/Color Painting: adjust section position" msgstr "Stützen/Farbmalen: Position des Abschnitts anpassen" msgid "Gizmo" -msgstr "" +msgstr "Gizmo" msgid "Set extruder number for the objects and parts" msgstr "Extrudernummer für die Objekte und Teile einstellen" @@ -10742,10 +10748,10 @@ msgid "Air Pump" msgstr "Luftpumpe" msgid "Laser 10W" -msgstr "" +msgstr "Laser 10W" msgid "Laser 40W" -msgstr "" +msgstr "Laser 40W" msgid "Cutting Module" msgstr "Schneidemodul" @@ -11332,10 +11338,10 @@ msgid "Allow controlling BambuLab's printer through 3rd party print hosts." msgstr "Erlauben Sie die Steuerung von BambuLab-Druckern durch Drittanbieter-Druck-Hosts" msgid "Use 3MF instead of G-code" -msgstr "" +msgstr "Benutze 3MF statt G-Code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." -msgstr "" +msgstr "Aktivieren Sie diese Option, wenn der Drucker eine 3MF-Datei als Druckauftrag akzeptiert. Wenn aktiviert, sendet Orca Slicer die geslicete Datei als .gcode.3mf, anstatt als einfache .gcode-Datei." msgid "Printer Agent" msgstr "Drucker-Agent" @@ -12604,6 +12610,8 @@ msgid "" "Enable adaptive PA for overhangs as well as when flow changes within the same feature. This is an experimental option, as if the PA profile is not set accurately, it will cause uniformity issues on the external surfaces before and after overhangs.\n" "Not compatible with Prusa printers as they pause to process PA changes, which causes delays and defects." msgstr "" +"Aktivieren Sie adaptives PA auch für Überhänge sowie bei Flussänderungen innerhalb desselben Features. Dies ist eine experimentelle Option, da sie bei ungenau eingestelltem PA-Profil zu Uniformitätsproblemen auf den äußeren Oberflächen vor und nach Überhängen führen kann.\n" +"Nicht kompatibel mit Prusa-Druckern, da diese pausieren, um PA-Änderungen zu verarbeiten, was zu Verzögerungen und Defekten führt." msgid "Pressure advance for bridges" msgstr "Pressure Advance für Brücken" @@ -13068,7 +13076,7 @@ msgid "Grid" msgstr "Gitternetz" msgid "Tri-hexagon" -msgstr "" +msgstr "Tri-Hexagon" msgid "Cubic" msgstr "Kubisch" @@ -13107,7 +13115,7 @@ msgid "TPMS-FK" msgstr "TPMS-FK" msgid "Gyroid" -msgstr "" +msgstr "Gyroid" msgid "Lateral lattice angle 1" msgstr "Gitterwinkel 1" @@ -13288,7 +13296,7 @@ msgid "Travel speed of the first layer." msgstr "Bewegungsgeschwindigkeit der ersten Schicht" msgid "Number of slow layers" -msgstr "" +msgstr "Anzahl der langsamen Schichten" msgid "The first few layers are printed slower than normal. The speed is gradually increased in a linear fashion over the specified number of layers." msgstr "Die ersten paar Schichten werden langsamer als normal gedruckt. Die Geschwindigkeit wird allmählich linear über die angegebene Anzahl von Schichten erhöht." @@ -13429,7 +13437,7 @@ msgid "Displacement" msgstr "Verschiebung" msgid "Extrusion" -msgstr "" +msgstr "Extrusion" msgid "Combined" msgstr "Kombiniert" @@ -13463,7 +13471,7 @@ msgid "Billow" msgstr "Billow" msgid "Ridged Multifractal" -msgstr "" +msgstr "Ridged Multifractal" msgid "Voronoi" msgstr "Voronoi" @@ -14198,7 +14206,7 @@ msgstr "" "Bitte schalten Sie diese Option aus, wenn Sie Ringe testen." msgid "Min" -msgstr "" +msgstr "Min" msgid "Minimum speed of resonance avoidance." msgstr "Minimale Geschwindigkeit der Resonanzvermeidung." @@ -16011,7 +16019,7 @@ msgid "Export the objects as multiple STLs to directory." msgstr "Exportieren Sie die Objekte als mehrere STLs in ein Verzeichnis" msgid "Slice" -msgstr "" +msgstr "Slice" msgid "Slice the plates: 0-all plates, i-plate i, others-invalid" msgstr "Slicen sie die Druckplatten: 0-alle Druckplatten; i-Druckplatte i; andere ungültig" @@ -16516,7 +16524,7 @@ msgid "Hour" msgstr "Stunde" msgid "Minute" -msgstr "" +msgstr "Minute" msgid "Second" msgstr "Sekunde" @@ -16552,7 +16560,7 @@ msgid "Index of the current layer. One-based (i.e. first layer is number 1)." msgstr "Index der aktuellen Schicht. Einsbasiert (d.h. die erste Schicht ist Nummer 1)." msgid "Layer Z" -msgstr "" +msgstr "Schichthöhe Z" msgid "Height of the current layer above the print bed, measured to the top of the layer." msgstr "Höhe der aktuellen Schicht über dem Druckbett, gemessen an der Oberseite der Schicht." @@ -16719,7 +16727,7 @@ msgid "Error desc" msgstr "Fehlerbeschreibung" msgid "Extra info" -msgstr "" +msgstr "Zusätzliche Informationen" msgid "Flow Dynamics" msgstr "Flussdynamik" @@ -17125,13 +17133,13 @@ msgid "Address" msgstr "IP Adresse" msgid "Hostname" -msgstr "" +msgstr "Hostname" msgid "Service name" -msgstr "" +msgstr "Dienstname" msgid "OctoPrint version" -msgstr "" +msgstr "OctoPrint-Version" msgid "Searching for devices" msgstr "Suche nach Geräten" @@ -17169,7 +17177,7 @@ msgid "PA Pattern" msgstr "PA Muster" msgid "Start PA: " -msgstr "" +msgstr "Start PA:" msgid "End PA: " msgstr "Ende PA:" @@ -17278,7 +17286,7 @@ msgstr "" "Ende > Start + Schritt" msgid "VFA test" -msgstr "" +msgstr "VFA-Test" msgid "Start speed: " msgstr "Startgeschwindigkeit" @@ -17326,7 +17334,7 @@ msgstr "" "Fixed-Time-Bewegung noch nicht implementiert." msgid "Klipper version => 0.9.0" -msgstr "" +msgstr "Klipper Version => 0.9.0" msgid "" "RepRap firmware version => 3.4.0\n" @@ -17405,7 +17413,7 @@ msgid "SCV-V2" msgstr "SCV-V2" msgid "Start: " -msgstr "" +msgstr "Start:" msgid "End: " msgstr "Ende: " @@ -17649,25 +17657,25 @@ msgid "Export Log" msgstr "Protokoll exportieren" msgid "OrcaSlicer Version:" -msgstr "" +msgstr "OrcaSlicer Version:" msgid "System Version:" -msgstr "" +msgstr "Systemversion:" msgid "DNS Server:" -msgstr "" +msgstr "DNS-Server:" msgid "Test OrcaSlicer (GitHub)" -msgstr "" +msgstr "OrcaSlicer testen (GitHub)" msgid "Test OrcaSlicer (GitHub):" -msgstr "" +msgstr "OrcaSlicer testen (GitHub):" msgid "Test bing.com" -msgstr "" +msgstr "Bing.com testen" msgid "Test bing.com:" -msgstr "" +msgstr "Bing.com testen:" msgid "Log Info" msgstr "Protokoll Info" @@ -18261,7 +18269,7 @@ msgid "To use a custom CA file, please import your CA file into Certificate Stor msgstr "Um eine benutzerdefinierte CA-Datei zu verwenden, importieren Sie bitte Ihre CA-Datei in den Zertifikatspeicher / das Schlüsselbund." msgid "Login/Test" -msgstr "" +msgstr "Anmelden/Testen" msgid "Connection to printers connected via the print host failed." msgstr "Die Verbindung zu den über den Druck-Host verbundenen Druckern ist fehlgeschlagen." @@ -18569,22 +18577,22 @@ msgid "Standard profile for 0.4mm nozzle, prioritizing speed." msgstr "Standardprofil für 0,4 mm Düse, das die Geschwindigkeit priorisiert." msgid "High quality profile for 0.6mm nozzle, prioritizing print quality and strength." -msgstr "" +msgstr "Hochwertiges Profil für 0,6 mm Düse, das Druckqualität und Festigkeit priorisiert." msgid "Strength profile for 0.6mm nozzle, prioritizing strength." -msgstr "" +msgstr "Festigkeitsprofil für 0,6 mm Düse, das die Festigkeit priorisiert." msgid "Standard profile for 0.6mm nozzle, prioritizing speed." -msgstr "" +msgstr "Standardprofil für 0,6 mm Düse, das die Geschwindigkeit priorisiert." msgid "High quality profile for 0.8mm nozzle, prioritizing print quality." -msgstr "" +msgstr "Hochwertiges Profil für 0,8 mm Düse, das die Druckqualität priorisiert." msgid "Strength profile for 0.8mm nozzle, prioritizing strength." -msgstr "" +msgstr "Festigkeitsprofil für 0,8 mm Düse, das die Festigkeit priorisiert." msgid "Standard profile for 0.8mm nozzle, prioritizing speed." -msgstr "" +msgstr "Standardprofil für 0,8 mm Düse, das die Geschwindigkeit priorisiert." msgid "No AMS" msgstr "Kein AMS" From 14cec7239b7372d866a94914ada89246119213a8 Mon Sep 17 00:00:00 2001 From: foXaCe Date: Tue, 23 Jun 2026 01:13:19 +0200 Subject: [PATCH 058/224] i18n(fr): translate strings added after the post-refactor sync (#14304) --- localization/i18n/fr/OrcaSlicer_fr.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/localization/i18n/fr/OrcaSlicer_fr.po b/localization/i18n/fr/OrcaSlicer_fr.po index c4398d231b..f32ee3c763 100644 --- a/localization/i18n/fr/OrcaSlicer_fr.po +++ b/localization/i18n/fr/OrcaSlicer_fr.po @@ -7990,13 +7990,13 @@ msgid "Renders cast shadows on the plate in realistic view." msgstr "Affiche les ombres portées sur la plaque dans la vue réaliste." msgid "Smooth normals" -msgstr "" +msgstr "Normales lissées" msgid "" "Applies smooth normals to the realistic view.\n" "\n" "Requires manual scene reload to take effect (right-click on 3D view → \"Reload All\")." -msgstr "" +msgstr "Applique les normales lissées à la vue réaliste.\n\nNécessite un rechargement manuel de la scène pour prendre effet (clic droit sur la vue 3D → « Tout recharger »)." msgid "Anti-aliasing" msgstr "Anticrénelage" @@ -12747,7 +12747,7 @@ msgid "Z-buckling bias optimization (experimental)" msgstr "Optimisation du biais de flambage en Z (expérimental)" # TODO: Review, changed by lang refactor. PR 14254 -#, fuzzy, c-format, boost-format +#, no-c-format, no-boost-format msgid "Tightens the gyroid wave along the Z (vertical) axis at low infill density to shorten the effective vertical column length and improve Z-axis compression buckling resistance. Filament use is preserved. No effect at ~30% sparse infill density and above. Only applies when Sparse infill pattern is set to Gyroid." msgstr "Resserre l’onde gyroïde le long de l’axe Z (vertical) à faible densité de remplissage afin de raccourcir la longueur effective des colonnes verticales et d’améliorer la résistance au flambage en compression selon l’axe Z. La consommation de filament est préservée. Aucun effet à partir d’environ 30 % de densité de remplissage. S’applique uniquement lorsque le motif de remplissage est réglé sur Gyroïde." @@ -12989,7 +12989,7 @@ msgid "Travel speed of the first layer." msgstr "Vitesse de déplacement de la couche initiale" msgid "Number of slow layers" -msgstr "" +msgstr "Nombre de couches lentes" msgid "The first few layers are printed slower than normal. The speed is gradually increased in a linear fashion over the specified number of layers." msgstr "Les premières couches sont imprimées plus lentement que la normale. La vitesse augmente progressivement de manière linéaire sur le nombre de couches spécifié." From 0cdfb88357012c698c55163d2cd651c90ce3ac87 Mon Sep 17 00:00:00 2001 From: Ian Bassi Date: Mon, 22 Jun 2026 20:16:55 -0300 Subject: [PATCH 059/224] Lang: Gettext update (#14361) --- localization/i18n/OrcaSlicer.pot | 8 +- localization/i18n/ca/OrcaSlicer_ca.po | 8 +- localization/i18n/cs/OrcaSlicer_cs.po | 8 +- localization/i18n/de/OrcaSlicer_de.po | 8 +- localization/i18n/en/OrcaSlicer_en.po | 8 +- localization/i18n/es/OrcaSlicer_es.po | 8 +- localization/i18n/fr/OrcaSlicer_fr.po | 15 +- localization/i18n/hu/OrcaSlicer_hu.po | 8 +- localization/i18n/it/OrcaSlicer_it.po | 8 +- localization/i18n/ja/OrcaSlicer_ja.po | 8 +- localization/i18n/ko/OrcaSlicer_ko.po | 8 +- localization/i18n/lt/OrcaSlicer_lt.po | 8 +- localization/i18n/nl/OrcaSlicer_nl.po | 8 +- localization/i18n/pl/OrcaSlicer_pl.po | 8 +- localization/i18n/pt_BR/OrcaSlicer_pt_BR.po | 20 +- localization/i18n/ru/OrcaSlicer_ru.po | 8 +- localization/i18n/sv/OrcaSlicer_sv.po | 8 +- localization/i18n/th/OrcaSlicer_th.po | 7772 +++++-------------- localization/i18n/tr/OrcaSlicer_tr.po | 8 +- localization/i18n/uk/OrcaSlicer_uk.po | 8 +- localization/i18n/vi/OrcaSlicer_vi.po | 8 +- localization/i18n/zh_CN/OrcaSlicer_zh_CN.po | 8 +- localization/i18n/zh_TW/OrcaSlicer_zh_TW.po | 8 +- 23 files changed, 2074 insertions(+), 5893 deletions(-) diff --git a/localization/i18n/OrcaSlicer.pot b/localization/i18n/OrcaSlicer.pot index 293252b8b9..83fd738661 100644 --- a/localization/i18n/OrcaSlicer.pot +++ b/localization/i18n/OrcaSlicer.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-19 10:33-0300\n" +"POT-Creation-Date: 2026-06-22 20:13-0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -3883,6 +3883,9 @@ msgstr "" msgid "The recommended nozzle temperature for this filament type is [%d, %d] degrees Celsius." msgstr "" +msgid "Adaptive Pressure Advance model validation failed:\n" +msgstr "" + msgid "" "Too small max volumetric speed.\n" "Value was reset to 0.5" @@ -10637,6 +10640,9 @@ msgstr "" msgid "The precise wall option will be ignored for outer-inner or inner-outer-inner wall sequences." msgstr "" +msgid "The Adaptive Pressure Advance model for one or more extruders may contain invalid values." +msgstr "" + msgid "Filament shrinkage will not be used because filament shrinkage for the used filaments does not match." msgstr "" diff --git a/localization/i18n/ca/OrcaSlicer_ca.po b/localization/i18n/ca/OrcaSlicer_ca.po index e0c2212590..9c86db6907 100644 --- a/localization/i18n/ca/OrcaSlicer_ca.po +++ b/localization/i18n/ca/OrcaSlicer_ca.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-19 10:33-0300\n" +"POT-Creation-Date: 2026-06-22 20:13-0300\n" "PO-Revision-Date: 2025-03-15 10:55+0100\n" "Last-Translator: \n" "Language-Team: \n" @@ -4070,6 +4070,9 @@ msgstr "" msgid "The recommended nozzle temperature for this filament type is [%d, %d] degrees Celsius." msgstr "La temperatura recomanada del broquet d'aquest tipus de filament és de [%d, %d] graus Celsius." +msgid "Adaptive Pressure Advance model validation failed:\n" +msgstr "" + # TODO: Review, changed by lang refactor. PR 14254 msgid "" "Too small max volumetric speed.\n" @@ -11181,6 +11184,9 @@ msgstr "" msgid "The precise wall option will be ignored for outer-inner or inner-outer-inner wall sequences." msgstr "L'opció de paret precisa s'ignorarà per a les seqüències de parets exterior-interior o interior-exterior-interior." +msgid "The Adaptive Pressure Advance model for one or more extruders may contain invalid values." +msgstr "" + msgid "Filament shrinkage will not be used because filament shrinkage for the used filaments does not match." msgstr "La contracció del filament no s'utilitzarà perquè la contracció del filament per als filaments utilitzats difereix significativament." diff --git a/localization/i18n/cs/OrcaSlicer_cs.po b/localization/i18n/cs/OrcaSlicer_cs.po index ea07285de3..ce5f2ebe3a 100644 --- a/localization/i18n/cs/OrcaSlicer_cs.po +++ b/localization/i18n/cs/OrcaSlicer_cs.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-19 10:33-0300\n" +"POT-Creation-Date: 2026-06-22 20:13-0300\n" "PO-Revision-Date: \n" "Last-Translator: Jakub Hencl\n" "Language-Team: \n" @@ -4068,6 +4068,9 @@ msgstr "" msgid "The recommended nozzle temperature for this filament type is [%d, %d] degrees Celsius." msgstr "Doporučená teplota trysky pro tento typ filamentu je [%d, %d] stupňů Celsia." +msgid "Adaptive Pressure Advance model validation failed:\n" +msgstr "" + # TODO: Review, changed by lang refactor. PR 14254 msgid "" "Too small max volumetric speed.\n" @@ -11185,6 +11188,9 @@ msgstr "" msgid "The precise wall option will be ignored for outer-inner or inner-outer-inner wall sequences." msgstr "Možnost přesné stěny se ignoruje pro sekvence stěn vnější-vnitřní nebo vnitřní-vnější-vnitřní." +msgid "The Adaptive Pressure Advance model for one or more extruders may contain invalid values." +msgstr "" + msgid "Filament shrinkage will not be used because filament shrinkage for the used filaments does not match." msgstr "Smrštění filamentu nebude použito, protože hodnoty smrštění použitých filamentů se neshodují." diff --git a/localization/i18n/de/OrcaSlicer_de.po b/localization/i18n/de/OrcaSlicer_de.po index 4a6ce9c11f..67defac5e4 100644 --- a/localization/i18n/de/OrcaSlicer_de.po +++ b/localization/i18n/de/OrcaSlicer_de.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-19 10:33-0300\n" +"POT-Creation-Date: 2026-06-22 20:13-0300\n" "PO-Revision-Date: \n" "Last-Translator: Heiko Liebscher \n" "Language-Team: \n" @@ -4079,6 +4079,9 @@ msgstr "" msgid "The recommended nozzle temperature for this filament type is [%d, %d] degrees Celsius." msgstr "Die empfohlene Düsentemperatur für diesen Filamenttyp beträgt [%d, %d] °C." +msgid "Adaptive Pressure Advance model validation failed:\n" +msgstr "" + # TODO: Review, changed by lang refactor. PR 14254 msgid "" "Too small max volumetric speed.\n" @@ -11224,6 +11227,9 @@ msgstr "" msgid "The precise wall option will be ignored for outer-inner or inner-outer-inner wall sequences." msgstr "Die Option für präzise Wände wird für Außen-Innen- oder Innen-Außen-Innen-Wand-Sequenzen ignoriert." +msgid "The Adaptive Pressure Advance model for one or more extruders may contain invalid values." +msgstr "" + msgid "Filament shrinkage will not be used because filament shrinkage for the used filaments does not match." msgstr "Filament-Schrumpfung wird nicht verwendet, da sich die Filament-Schrumpfung für die verwendeten Filamente signifikant unterscheidet." diff --git a/localization/i18n/en/OrcaSlicer_en.po b/localization/i18n/en/OrcaSlicer_en.po index 2202b8ea2f..cb5d1552ec 100644 --- a/localization/i18n/en/OrcaSlicer_en.po +++ b/localization/i18n/en/OrcaSlicer_en.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-19 10:33-0300\n" +"POT-Creation-Date: 2026-06-22 20:13-0300\n" "PO-Revision-Date: 2026-06-17 15:44-0300\n" "Last-Translator: Alexandre Folle de Menezes\n" "Language-Team: \n" @@ -3879,6 +3879,9 @@ msgstr "" msgid "The recommended nozzle temperature for this filament type is [%d, %d] degrees Celsius." msgstr "" +msgid "Adaptive Pressure Advance model validation failed:\n" +msgstr "" + msgid "" "Too small max volumetric speed.\n" "Value was reset to 0.5" @@ -10633,6 +10636,9 @@ msgstr "" msgid "The precise wall option will be ignored for outer-inner or inner-outer-inner wall sequences." msgstr "" +msgid "The Adaptive Pressure Advance model for one or more extruders may contain invalid values." +msgstr "" + msgid "Filament shrinkage will not be used because filament shrinkage for the used filaments does not match." msgstr "" diff --git a/localization/i18n/es/OrcaSlicer_es.po b/localization/i18n/es/OrcaSlicer_es.po index 9978b8a2b0..786e43696e 100644 --- a/localization/i18n/es/OrcaSlicer_es.po +++ b/localization/i18n/es/OrcaSlicer_es.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-19 10:33-0300\n" +"POT-Creation-Date: 2026-06-22 20:13-0300\n" "PO-Revision-Date: \n" "Last-Translator: Ian A. Bassi <>\n" "Language-Team: \n" @@ -4077,6 +4077,9 @@ msgstr "" msgid "The recommended nozzle temperature for this filament type is [%d, %d] degrees Celsius." msgstr "La temperatura recomendada de la boquilla para este tipo de filamento es de [%d, %d] grados Celsius." +msgid "Adaptive Pressure Advance model validation failed:\n" +msgstr "" + # TODO: Review, changed by lang refactor. PR 14254 msgid "" "Too small max volumetric speed.\n" @@ -11213,6 +11216,9 @@ msgstr "" msgid "The precise wall option will be ignored for outer-inner or inner-outer-inner wall sequences." msgstr "La opción de pared precisa se ignorará para secuencias de paredes externo-interno o interno-externo-interno." +msgid "The Adaptive Pressure Advance model for one or more extruders may contain invalid values." +msgstr "" + msgid "Filament shrinkage will not be used because filament shrinkage for the used filaments does not match." msgstr "La contracción del filamento no se utilizará porque la contracción del filamento para los filamentos utilizados difiere significativamente." diff --git a/localization/i18n/fr/OrcaSlicer_fr.po b/localization/i18n/fr/OrcaSlicer_fr.po index f32ee3c763..a67471e0d5 100644 --- a/localization/i18n/fr/OrcaSlicer_fr.po +++ b/localization/i18n/fr/OrcaSlicer_fr.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-19 10:33-0300\n" +"POT-Creation-Date: 2026-06-22 20:13-0300\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: Guislain Cyril, Thomas Lété\n" @@ -3972,6 +3972,9 @@ msgstr "" msgid "The recommended nozzle temperature for this filament type is [%d, %d] degrees Celsius." msgstr "La température de buse recommandée pour ce type de filament est de [%d, %d] degrés Celsius." +msgid "Adaptive Pressure Advance model validation failed:\n" +msgstr "" + msgid "" "Too small max volumetric speed.\n" "Value was reset to 0.5" @@ -7996,7 +7999,10 @@ msgid "" "Applies smooth normals to the realistic view.\n" "\n" "Requires manual scene reload to take effect (right-click on 3D view → \"Reload All\")." -msgstr "Applique les normales lissées à la vue réaliste.\n\nNécessite un rechargement manuel de la scène pour prendre effet (clic droit sur la vue 3D → « Tout recharger »)." +msgstr "" +"Applique les normales lissées à la vue réaliste.\n" +"\n" +"Nécessite un rechargement manuel de la scène pour prendre effet (clic droit sur la vue 3D → « Tout recharger »)." msgid "Anti-aliasing" msgstr "Anticrénelage" @@ -10970,6 +10976,9 @@ msgstr "" msgid "The precise wall option will be ignored for outer-inner or inner-outer-inner wall sequences." msgstr "L'option de paroi précise sera ignorée pour les séquences de parois extérieure-intérieure ou intérieure-extérieure-intérieure." +msgid "The Adaptive Pressure Advance model for one or more extruders may contain invalid values." +msgstr "" + msgid "Filament shrinkage will not be used because filament shrinkage for the used filaments does not match." msgstr "Le rétrécissement du filament ne sera pas utilisé car le rétrécissement des filaments utilisés ne correspond pas." @@ -12747,7 +12756,7 @@ msgid "Z-buckling bias optimization (experimental)" msgstr "Optimisation du biais de flambage en Z (expérimental)" # TODO: Review, changed by lang refactor. PR 14254 -#, no-c-format, no-boost-format +#, fuzzy, c-format, boost-format msgid "Tightens the gyroid wave along the Z (vertical) axis at low infill density to shorten the effective vertical column length and improve Z-axis compression buckling resistance. Filament use is preserved. No effect at ~30% sparse infill density and above. Only applies when Sparse infill pattern is set to Gyroid." msgstr "Resserre l’onde gyroïde le long de l’axe Z (vertical) à faible densité de remplissage afin de raccourcir la longueur effective des colonnes verticales et d’améliorer la résistance au flambage en compression selon l’axe Z. La consommation de filament est préservée. Aucun effet à partir d’environ 30 % de densité de remplissage. S’applique uniquement lorsque le motif de remplissage est réglé sur Gyroïde." diff --git a/localization/i18n/hu/OrcaSlicer_hu.po b/localization/i18n/hu/OrcaSlicer_hu.po index 490c3276a1..0142e6415f 100644 --- a/localization/i18n/hu/OrcaSlicer_hu.po +++ b/localization/i18n/hu/OrcaSlicer_hu.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-19 10:33-0300\n" +"POT-Creation-Date: 2026-06-22 20:13-0300\n" "Language: hu\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -4062,6 +4062,9 @@ msgstr "" msgid "The recommended nozzle temperature for this filament type is [%d, %d] degrees Celsius." msgstr "Az ajánlott fúvóka hőmérséklet ehhez a filament típushoz [%d, %d] Celsius-fok" +msgid "Adaptive Pressure Advance model validation failed:\n" +msgstr "" + # TODO: Review, changed by lang refactor. PR 14254 msgid "" "Too small max volumetric speed.\n" @@ -11196,6 +11199,9 @@ msgstr "" msgid "The precise wall option will be ignored for outer-inner or inner-outer-inner wall sequences." msgstr "A pontos fal opció figyelmen kívül lesz hagyva külső-belső vagy belső-külső-belső fali sorrend esetén." +msgid "The Adaptive Pressure Advance model for one or more extruders may contain invalid values." +msgstr "" + msgid "Filament shrinkage will not be used because filament shrinkage for the used filaments does not match." msgstr "A filament zsugorodása nem lesz figyelembe véve, mert a használt filamentekhez megadott zsugorodási értékek nem egyeznek." diff --git a/localization/i18n/it/OrcaSlicer_it.po b/localization/i18n/it/OrcaSlicer_it.po index 0901abff53..d079a657bf 100644 --- a/localization/i18n/it/OrcaSlicer_it.po +++ b/localization/i18n/it/OrcaSlicer_it.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-19 10:33-0300\n" +"POT-Creation-Date: 2026-06-22 20:13-0300\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -4063,6 +4063,9 @@ msgstr "" msgid "The recommended nozzle temperature for this filament type is [%d, %d] degrees Celsius." msgstr "La temperatura dell'ugello consigliata per questo filamento è [%d, %d] gradi Celsius." +msgid "Adaptive Pressure Advance model validation failed:\n" +msgstr "" + # TODO: Review, changed by lang refactor. PR 14254 msgid "" "Too small max volumetric speed.\n" @@ -11178,6 +11181,9 @@ msgstr "" msgid "The precise wall option will be ignored for outer-inner or inner-outer-inner wall sequences." msgstr "L'opzione parete precisa verrà ignorata per le sequenze di pareti esterno-interno o interno-esterno-interno." +msgid "The Adaptive Pressure Advance model for one or more extruders may contain invalid values." +msgstr "" + msgid "Filament shrinkage will not be used because filament shrinkage for the used filaments does not match." msgstr "La compensazione del restringimento del materiale non verrà utilizzata perché il fenomeno di restringimento nei filamenti utilizzati varia in modo significativo." diff --git a/localization/i18n/ja/OrcaSlicer_ja.po b/localization/i18n/ja/OrcaSlicer_ja.po index db9445db1e..55b1632470 100644 --- a/localization/i18n/ja/OrcaSlicer_ja.po +++ b/localization/i18n/ja/OrcaSlicer_ja.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-19 10:33-0300\n" +"POT-Creation-Date: 2026-06-22 20:13-0300\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -4045,6 +4045,9 @@ msgstr "" msgid "The recommended nozzle temperature for this filament type is [%d, %d] degrees Celsius." msgstr "このフィラメントで推奨ノズル温度は %d ~ %d ℃です。" +msgid "Adaptive Pressure Advance model validation failed:\n" +msgstr "" + # TODO: Review, changed by lang refactor. PR 14254 msgid "" "Too small max volumetric speed.\n" @@ -11096,6 +11099,9 @@ msgstr "" msgid "The precise wall option will be ignored for outer-inner or inner-outer-inner wall sequences." msgstr "正確な壁オプションは外側-内側または内側-外側-内側の壁シーケンスでは無視されます。" +msgid "The Adaptive Pressure Advance model for one or more extruders may contain invalid values." +msgstr "" + msgid "Filament shrinkage will not be used because filament shrinkage for the used filaments does not match." msgstr "使用するフィラメントの収縮率が大きく異なるため、フィラメント収縮率は使用しません。" diff --git a/localization/i18n/ko/OrcaSlicer_ko.po b/localization/i18n/ko/OrcaSlicer_ko.po index 5983d1293f..f24e29a07e 100644 --- a/localization/i18n/ko/OrcaSlicer_ko.po +++ b/localization/i18n/ko/OrcaSlicer_ko.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-19 10:33-0300\n" +"POT-Creation-Date: 2026-06-22 20:13-0300\n" "PO-Revision-Date: 2025-06-02 17:12+0900\n" "Last-Translator: crwusiz \n" "Language-Team: \n" @@ -4060,6 +4060,9 @@ msgstr "" msgid "The recommended nozzle temperature for this filament type is [%d, %d] degrees Celsius." msgstr "이 필라멘트 유형의 권장 노즐 온도는 [%d, %d]°C입니다" +msgid "Adaptive Pressure Advance model validation failed:\n" +msgstr "" + # TODO: Review, changed by lang refactor. PR 14254 msgid "" "Too small max volumetric speed.\n" @@ -11140,6 +11143,9 @@ msgstr "" msgid "The precise wall option will be ignored for outer-inner or inner-outer-inner wall sequences." msgstr "" +msgid "The Adaptive Pressure Advance model for one or more extruders may contain invalid values." +msgstr "" + msgid "Filament shrinkage will not be used because filament shrinkage for the used filaments does not match." msgstr "" diff --git a/localization/i18n/lt/OrcaSlicer_lt.po b/localization/i18n/lt/OrcaSlicer_lt.po index 83f1c8d567..5148dd0776 100644 --- a/localization/i18n/lt/OrcaSlicer_lt.po +++ b/localization/i18n/lt/OrcaSlicer_lt.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-19 10:33-0300\n" +"POT-Creation-Date: 2026-06-22 20:13-0300\n" "PO-Revision-Date: 2025-10-25 23:01+0300\n" "Last-Translator: Gintaras Kučinskas \n" "Language-Team: \n" @@ -4055,6 +4055,9 @@ msgstr "" msgid "The recommended nozzle temperature for this filament type is [%d, %d] degrees Celsius." msgstr "Rekomenduojama šio tipo gijos spausdinimo temperatūra yra nuo %d iki %d laipsnių Celsijaus." +msgid "Adaptive Pressure Advance model validation failed:\n" +msgstr "" + # TODO: Review, changed by lang refactor. PR 14254 msgid "" "Too small max volumetric speed.\n" @@ -11129,6 +11132,9 @@ msgstr "" msgid "The precise wall option will be ignored for outer-inner or inner-outer-inner wall sequences." msgstr "Tiksli sienos parinktis bus ignoruojama išorinės-vidinės arba vidinės-išorinės-vidinės sienų sekose." +msgid "The Adaptive Pressure Advance model for one or more extruders may contain invalid values." +msgstr "" + msgid "Filament shrinkage will not be used because filament shrinkage for the used filaments does not match." msgstr "Gijų susitraukimas nenaudojamas, nes naudojamų gijų susitraukimas labai skiriasi." diff --git a/localization/i18n/nl/OrcaSlicer_nl.po b/localization/i18n/nl/OrcaSlicer_nl.po index 6fe4919539..ca4fcd689b 100644 --- a/localization/i18n/nl/OrcaSlicer_nl.po +++ b/localization/i18n/nl/OrcaSlicer_nl.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-19 10:33-0300\n" +"POT-Creation-Date: 2026-06-22 20:13-0300\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -4017,6 +4017,9 @@ msgstr "" msgid "The recommended nozzle temperature for this filament type is [%d, %d] degrees Celsius." msgstr "De aanbevolen mondstuk temperatuur voor dit type filament is [%d, %d] graden Celsius." +msgid "Adaptive Pressure Advance model validation failed:\n" +msgstr "" + # TODO: Review, changed by lang refactor. PR 14254 msgid "" "Too small max volumetric speed.\n" @@ -11025,6 +11028,9 @@ msgstr "" msgid "The precise wall option will be ignored for outer-inner or inner-outer-inner wall sequences." msgstr "" +msgid "The Adaptive Pressure Advance model for one or more extruders may contain invalid values." +msgstr "" + msgid "Filament shrinkage will not be used because filament shrinkage for the used filaments does not match." msgstr "" diff --git a/localization/i18n/pl/OrcaSlicer_pl.po b/localization/i18n/pl/OrcaSlicer_pl.po index 185e1f8626..bdcec97fd6 100644 --- a/localization/i18n/pl/OrcaSlicer_pl.po +++ b/localization/i18n/pl/OrcaSlicer_pl.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: OrcaSlicer 2.3.0-rc\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-19 10:33-0300\n" +"POT-Creation-Date: 2026-06-22 20:13-0300\n" "PO-Revision-Date: \n" "Last-Translator: Krzysztof Morga <>\n" "Language-Team: \n" @@ -4065,6 +4065,9 @@ msgstr "" msgid "The recommended nozzle temperature for this filament type is [%d, %d] degrees Celsius." msgstr "Zalecana temperatura dyszy dla tego typu filamentu wynosi [%d, %d] stopni Celsjusza" +msgid "Adaptive Pressure Advance model validation failed:\n" +msgstr "" + # TODO: Review, changed by lang refactor. PR 14254 msgid "" "Too small max volumetric speed.\n" @@ -11155,6 +11158,9 @@ msgstr "" msgid "The precise wall option will be ignored for outer-inner or inner-outer-inner wall sequences." msgstr "" +msgid "The Adaptive Pressure Advance model for one or more extruders may contain invalid values." +msgstr "" + msgid "Filament shrinkage will not be used because filament shrinkage for the used filaments does not match." msgstr "Kompensacja skurczu filamentu nie będzie zastosowana, ponieważ skurcz używanych filamentów jest zbyt zróżnicowany." diff --git a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po index d6e6bb514c..c31f1a3e95 100644 --- a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po +++ b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-18 09:16-0300\n" +"POT-Creation-Date: 2026-06-22 20:13-0300\n" "PO-Revision-Date: 2026-06-19 20:49-0300\n" "Last-Translator: Alexandre Folle de Menezes\n" "Language-Team: Portuguese, Brazilian\n" @@ -3995,6 +3995,9 @@ msgstr "" msgid "The recommended nozzle temperature for this filament type is [%d, %d] degrees Celsius." msgstr "A temperatura do bico recomendada para este tipo de filamento é [%d, %d] graus Celsius." +msgid "Adaptive Pressure Advance model validation failed:\n" +msgstr "" + msgid "" "Too small max volumetric speed.\n" "Value was reset to 0.5" @@ -11102,6 +11105,9 @@ msgstr "" msgid "The precise wall option will be ignored for outer-inner or inner-outer-inner wall sequences." msgstr "A opção de parede precisa será ignorada para sequências externa-interna ou interna-externa-interna." +msgid "The Adaptive Pressure Advance model for one or more extruders may contain invalid values." +msgstr "" + msgid "Filament shrinkage will not be used because filament shrinkage for the used filaments does not match." msgstr "A contração de filamento não será usada porque a contração dos filamentos usados difere significativamente." @@ -13103,9 +13109,6 @@ msgstr "Velocidade de deslocamento da primeira camada." msgid "Number of slow layers" msgstr "" -msgid "This is the number of top interface layers." -msgstr "Este é o número de camadas de interface superiores." - msgid "The first few layers are printed slower than normal. The speed is gradually increased in a linear fashion over the specified number of layers." msgstr "As primeiras camadas são impressas mais lentamente do que o normal. A velocidade é aumentada gradualmente de forma linear sobre o número especificado de camadas." @@ -15122,6 +15125,9 @@ msgstr "" msgid "Top interface layers" msgstr "Camadas de interface superior" +msgid "This is the number of top interface layers." +msgstr "Este é o número de camadas de interface superiores." + msgid "Bottom interface layers" msgstr "Camadas de interface inferior" @@ -19208,7 +19214,8 @@ msgstr "" "Divida suas impressões em placas\n" "Você sabia que pode dividir um modelo que tem diversas peças em placas individuais distintas prontas para imprimir? Isso simplifica o processo de manter o controle de todas as peças." -#: resources/data/hints.ini: [hint:Speed up your print with Adaptive Layer Height] +#: resources/data/hints.ini: [hint:Speed up your print with Adaptive Layer +#: Height] msgid "" "Speed up your print with Adaptive Layer Height\n" "Did you know that you can print a model even faster by using the Adaptive Layer Height option? Check it out!" @@ -19280,7 +19287,8 @@ msgstr "" "Melhorar a resistência\n" "Você sabia que pode usar mais voltas de parede e maior densidade de preenchimento esparso mais alta para melhorar a resistência do modelo?" -#: resources/data/hints.ini: [hint:When do you need to print with the printer door opened] +#: resources/data/hints.ini: [hint:When do you need to print with the printer +#: door opened] msgid "" "When do you need to print with the printer door opened?\n" "Did you know that opening the printer door can reduce the probability of extruder/hotend clogging when printing lower temperature filament with a higher enclosure temperature? There is more info about this in the Wiki." diff --git a/localization/i18n/ru/OrcaSlicer_ru.po b/localization/i18n/ru/OrcaSlicer_ru.po index a853217c74..832f2332da 100644 --- a/localization/i18n/ru/OrcaSlicer_ru.po +++ b/localization/i18n/ru/OrcaSlicer_ru.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: OrcaSlicer V2.3.2 beta2\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-19 10:33-0300\n" +"POT-Creation-Date: 2026-06-22 20:13-0300\n" "PO-Revision-Date: 2026-02-25 13:38+0300\n" "Last-Translator: Felix14_v2\n" "Language-Team: Felix14_v2 (ДС/ТГ: @felix14_v2, почта: aleks111001@list.ru), Andylg \n" @@ -4176,6 +4176,9 @@ msgstr "" msgid "The recommended nozzle temperature for this filament type is [%d, %d] degrees Celsius." msgstr "Диапазон рекомендуемых температур печати данным типом материала составляет [%d, %d] градусов Цельсия." +msgid "Adaptive Pressure Advance model validation failed:\n" +msgstr "" + # TODO: Review, changed by lang refactor. PR 14254 msgid "" "Too small max volumetric speed.\n" @@ -11506,6 +11509,9 @@ msgstr "" msgid "The precise wall option will be ignored for outer-inner or inner-outer-inner wall sequences." msgstr "Параметр точной стенки будет проигнорирован для последовательностей стен «внешняя-внутренняя» или «внутренняя-внешняя-внутренняя»." +msgid "The Adaptive Pressure Advance model for one or more extruders may contain invalid values." +msgstr "" + msgid "Filament shrinkage will not be used because filament shrinkage for the used filaments does not match." msgstr "Компенсация усадки материала не будет использоваться, поскольку усадка у используемых материалов не совпадает." diff --git a/localization/i18n/sv/OrcaSlicer_sv.po b/localization/i18n/sv/OrcaSlicer_sv.po index 2e514d04e9..e47b3e2816 100644 --- a/localization/i18n/sv/OrcaSlicer_sv.po +++ b/localization/i18n/sv/OrcaSlicer_sv.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-19 10:33-0300\n" +"POT-Creation-Date: 2026-06-22 20:13-0300\n" "Language: sv\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -4007,6 +4007,9 @@ msgstr "" msgid "The recommended nozzle temperature for this filament type is [%d, %d] degrees Celsius." msgstr "Rekommenderad nozzel temperatur med denna filament typ är [%d, %d] grader celius" +msgid "Adaptive Pressure Advance model validation failed:\n" +msgstr "" + # TODO: Review, changed by lang refactor. PR 14254 msgid "" "Too small max volumetric speed.\n" @@ -11001,6 +11004,9 @@ msgstr "" msgid "The precise wall option will be ignored for outer-inner or inner-outer-inner wall sequences." msgstr "" +msgid "The Adaptive Pressure Advance model for one or more extruders may contain invalid values." +msgstr "" + msgid "Filament shrinkage will not be used because filament shrinkage for the used filaments does not match." msgstr "" diff --git a/localization/i18n/th/OrcaSlicer_th.po b/localization/i18n/th/OrcaSlicer_th.po index bbf14d80a1..62f825f1ae 100644 --- a/localization/i18n/th/OrcaSlicer_th.po +++ b/localization/i18n/th/OrcaSlicer_th.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-19 10:33-0300\n" +"POT-Creation-Date: 2026-06-22 20:13-0300\n" "PO-Revision-Date: 2026-06-19 13:40+0700\n" "Last-Translator: Icezaza\n" "Language-Team: Thai\n" @@ -38,34 +38,20 @@ msgstr "AMS ไม่รองรับ TPU" msgid "AMS does not support 'Bambu Lab PET-CF'." msgstr "AMS ไม่รองรับ 'Bambu Lab PET-CF'" -msgid "" -"Please cold pull before printing TPU to avoid clogging. You may use cold " -"pull maintenance on the printer." -msgstr "" -"โปรดทำ cold pull ก่อนพิมพ์ TPU เพื่อหลีกเลี่ยงการอุดตัน คุณสามารถใช้การบำรุงรักษาแบบ cold " -"pull บนเครื่องพิมพ์ได้" +msgid "Please cold pull before printing TPU to avoid clogging. You may use cold pull maintenance on the printer." +msgstr "โปรดทำ cold pull ก่อนพิมพ์ TPU เพื่อหลีกเลี่ยงการอุดตัน คุณสามารถใช้การบำรุงรักษาแบบ cold pull บนเครื่องพิมพ์ได้" -msgid "" -"Damp PVA will become flexible and get stuck inside AMS, please take care to " -"dry it before use." +msgid "Damp PVA will become flexible and get stuck inside AMS, please take care to dry it before use." msgstr "PVA ที่ชื้นจะมีความยืดหยุ่นและอาจติดค้างใน AMS โปรดระวังและอบให้แห้งก่อนใช้งาน" msgid "Damp PVA is flexible and may get stuck in extruder. Dry it before use." msgstr "PVA ที่ชื้นมีความยืดหยุ่นและอาจติดค้างในชุดดันเส้น โปรดอบให้แห้งก่อนใช้งาน" -msgid "" -"The rough surface of PLA Glow can accelerate wear on the AMS system, " -"particularly on the internal components of the AMS Lite." -msgstr "" -"พื้นผิวที่ขรุขระของ PLA Glow สามารถเร่งการสึกหรอในระบบ AMS ได้ " -"โดยเฉพาะอย่างยิ่งกับชิ้นส่วนภายในของ AMS Lite" +msgid "The rough surface of PLA Glow can accelerate wear on the AMS system, particularly on the internal components of the AMS Lite." +msgstr "พื้นผิวที่ขรุขระของ PLA Glow สามารถเร่งการสึกหรอในระบบ AMS ได้ โดยเฉพาะอย่างยิ่งกับชิ้นส่วนภายในของ AMS Lite" -msgid "" -"CF/GF filaments are hard and brittle, it's easy to break or get stuck in " -"AMS, please use with caution." -msgstr "" -"เส้นพลาสติก CF/GF มีความแข็งและเปราะบาง แตกหักหรือติดค้างใน AMS ได้ง่าย " -"โปรดใช้งานด้วยความระมัดระวัง" +msgid "CF/GF filaments are hard and brittle, it's easy to break or get stuck in AMS, please use with caution." +msgstr "เส้นพลาสติก CF/GF มีความแข็งและเปราะบาง แตกหักหรือติดค้างใน AMS ได้ง่าย โปรดใช้งานด้วยความระมัดระวัง" msgid "PPS-CF is brittle and could break in bended PTFE tube above Toolhead." msgstr "PPS-CF มีความเปราะบางและอาจแตกหักในท่อ PTFE ที่โค้งงอเหนือหัวพิมพ์" @@ -208,12 +194,8 @@ msgid "Lay on Face" msgstr "วางบนหน้า" #, boost-format -msgid "" -"Filament count exceeds the maximum number that painting tool supports. Only " -"the first %1% filaments will be available in painting tool." -msgstr "" -"จำนวนเส้นพลาสติกเกินจำนวนสูงสุดที่เครื่องมือพ่นสีรองรับ เฉพาะเส้นพลาสติก %1% " -"แรกเท่านั้นที่จะสามารถใช้ได้ในเครื่องมือวาดภาพ" +msgid "Filament count exceeds the maximum number that painting tool supports. Only the first %1% filaments will be available in painting tool." +msgstr "จำนวนเส้นพลาสติกเกินจำนวนสูงสุดที่เครื่องมือพ่นสีรองรับ เฉพาะเส้นพลาสติก %1% แรกเท่านั้นที่จะสามารถใช้ได้ในเครื่องมือวาดภาพ" msgid "Color Painting" msgstr "ระบายสี" @@ -309,8 +291,7 @@ msgstr "ลบผิวฟัซซี" msgid "Reset selection" msgstr "รีเซ็ตการเลือก" -msgid "" -"Warning: Fuzzy skin is disabled, painted fuzzy skin will not take effect!" +msgid "Warning: Fuzzy skin is disabled, painted fuzzy skin will not take effect!" msgstr "คำเตือน: ผิวฟัซซีถูกปิดใช้งาน ผิวฟัซซีที่ทาสีจะไม่มีผล!" msgid "Enable painted fuzzy skin for this object" @@ -726,12 +707,8 @@ msgid "Decimate ratio" msgstr "อัตราการลดรายละเอียด" #, boost-format -msgid "" -"Processing model '%1%' with more than 1M triangles could be slow. It is " -"highly recommended to simplify the model." -msgstr "" -"การประมวลผลโมเดล '%1%' ที่มีรูปสามเหลี่ยมมากกว่า 1 ล้านรูปอาจช้า " -"ขอแนะนำอย่างยิ่งให้ทำให้โมเดลง่ายขึ้น" +msgid "Processing model '%1%' with more than 1M triangles could be slow. It is highly recommended to simplify the model." +msgstr "การประมวลผลโมเดล '%1%' ที่มีรูปสามเหลี่ยมมากกว่า 1 ล้านรูปอาจช้า ขอแนะนำอย่างยิ่งให้ทำให้โมเดลง่ายขึ้น" msgid "Simplify model" msgstr "ลดรายละเอียดโมเดล" @@ -894,9 +871,7 @@ msgstr "ขั้นสูง" msgid "Reset all options except the text and operation" msgstr "รีเซ็ตตัวเลือกทั้งหมด ยกเว้นข้อความและการดำเนินการ" -msgid "" -"The text cannot be written using the selected font. Please try choosing a " -"different font." +msgid "The text cannot be written using the selected font. Please try choosing a different font." msgstr "ไม่สามารถเขียนข้อความโดยใช้แบบอักษรที่เลือกได้ โปรดลองเลือกแบบอักษรอื่น" msgid "Embossed text cannot contain only white spaces." @@ -1178,12 +1153,8 @@ msgid "Font \"%1%\" can't be used. Please select another." msgstr "ไม่สามารถใช้แบบอักษร \"%1%\" ได้ โปรดเลือกรายการอื่น" #, boost-format -msgid "" -"Can't load exactly same font (\"%1%\"). Application selected a similar one " -"(\"%2%\"). You have to specify font for enable edit text." -msgstr "" -"ไม่สามารถโหลดแบบอักษรเดียวกันทุกประการ (\"%1%\") แอปพลิเคชันเลือกแอปพลิเคชันที่คล้ายกัน " -"(\"%2%\") คุณต้องระบุแบบอักษรเพื่อให้สามารถแก้ไขข้อความได้" +msgid "Can't load exactly same font (\"%1%\"). Application selected a similar one (\"%2%\"). You have to specify font for enable edit text." +msgstr "ไม่สามารถโหลดแบบอักษรเดียวกันทุกประการ (\"%1%\") แอปพลิเคชันเลือกแอปพลิเคชันที่คล้ายกัน (\"%2%\") คุณต้องระบุแบบอักษรเพื่อให้สามารถแก้ไขข้อความได้" msgid "No symbol" msgstr "ไม่มีสัญลักษณ์" @@ -1298,9 +1269,7 @@ msgstr "ประเภทจังหวะที่ไม่ได้กำห msgid "Path can't be healed from self-intersection and multiple points." msgstr "เส้นทางไม่สามารถรักษาได้จากทางแยกตัวเองและหลายจุด" -msgid "" -"Final shape contains self-intersection or multiple points with same " -"coordinate." +msgid "Final shape contains self-intersection or multiple points with same coordinate." msgstr "รูปร่างสุดท้ายมีจุดตัดกันเองหรือหลายจุดที่มีพิกัดเดียวกัน" #, boost-format @@ -1477,8 +1446,7 @@ msgstr "ยกเลิกคุณลักษณะจนกว่าจะอ msgid "Measure" msgstr "วัด" -msgid "" -"Please confirm explosion ratio = 1, and please select at least one object." +msgid "Please confirm explosion ratio = 1, and please select at least one object." msgstr "โปรดยืนยันอัตราส่วนการระเบิด = 1 และโปรดเลือกวัตถุอย่างน้อยหนึ่งชิ้น" msgid "Edit to scale" @@ -1648,13 +1616,11 @@ msgstr "เส้นพลาสติก" msgid "Machine" msgstr "เครื่อง" -msgid "" -"The configuration package was loaded, but some values were not recognized." +msgid "The configuration package was loaded, but some values were not recognized." msgstr "โหลดแพ็คเกจการกำหนดค่าแล้ว แต่ไม่รู้จักค่าบางค่า" #, boost-format -msgid "" -"The configuration file “%1%” was loaded, but some values were not recognized." +msgid "The configuration file “%1%” was loaded, but some values were not recognized." msgstr "โหลดไฟล์การกำหนดค่า \"%1%\" แล้ว แต่ระบบไม่รู้จักค่าบางค่า" msgid "Loading configuration" @@ -1708,22 +1674,14 @@ msgstr "ไฟล์ SLA ที่ปกปิด" msgid "Draco files" msgstr "ไฟล์เดรโก" -msgid "" -"OrcaSlicer will terminate because of running out of memory. It may be a bug. " -"It will be appreciated if you report the issue to our team." -msgstr "" -"OrcaSlicer จะยุติลงเนื่องจากหน่วยความจำไม่เพียงพอ มันอาจจะเป็นข้อผิดพลาด " -"เราจะยินดีเป็นอย่างยิ่งหากคุณรายงานปัญหาดังกล่าวให้ทีมงานของเราทราบ" +msgid "OrcaSlicer will terminate because of running out of memory. It may be a bug. It will be appreciated if you report the issue to our team." +msgstr "OrcaSlicer จะยุติลงเนื่องจากหน่วยความจำไม่เพียงพอ มันอาจจะเป็นข้อผิดพลาด เราจะยินดีเป็นอย่างยิ่งหากคุณรายงานปัญหาดังกล่าวให้ทีมงานของเราทราบ" msgid "Fatal error" msgstr "ข้อผิดพลาดร้ายแรง" -msgid "" -"OrcaSlicer will terminate because of a localization error. It will be " -"appreciated if you report the specific scenario this issue happened." -msgstr "" -"OrcaSlicer จะยุติลงเนื่องจากข้อผิดพลาดในการแปล " -"เราจะยินดีอย่างยิ่งหากคุณรายงานสถานการณ์เฉพาะที่ปัญหานี้เกิดขึ้น" +msgid "OrcaSlicer will terminate because of a localization error. It will be appreciated if you report the specific scenario this issue happened." +msgstr "OrcaSlicer จะยุติลงเนื่องจากข้อผิดพลาดในการแปล เราจะยินดีอย่างยิ่งหากคุณรายงานสถานการณ์เฉพาะที่ปัญหานี้เกิดขึ้น" msgid "Critical error" msgstr "ข้อผิดพลาดร้ายแรง" @@ -1755,8 +1713,7 @@ msgid "Connect %s failed! [SN:%s, code=%s]" msgstr "เชื่อมต่อ %s ล้มเหลว! [SN:%s, รหัส=%s]" msgid "" -"Orca Slicer requires the Microsoft WebView2 Runtime to operate certain " -"features.\n" +"Orca Slicer requires the Microsoft WebView2 Runtime to operate certain features.\n" "Click Yes to install it now." msgstr "" "Orca Slicer ต้องใช้ Microsoft WebView2 Runtime เพื่อใช้งานคุณสมบัติบางอย่าง\n" @@ -1808,8 +1765,7 @@ msgstr "กำลังแสดงหน้าต่างหลัก" msgid "" "The OrcaSlicer configuration file may be corrupted and cannot be parsed.\n" "OrcaSlicer has attempted to recreate the configuration file.\n" -"Please note, application settings will be lost, but printer profiles will " -"not be affected." +"Please note, application settings will be lost, but printer profiles will not be affected." msgstr "" "ไฟล์การกำหนดค่า OrcaSlicer อาจเสียหายและไม่สามารถแยกวิเคราะห์ได้\n" "OrcaSlicer พยายามสร้างไฟล์การกำหนดค่าใหม่\n" @@ -1845,19 +1801,13 @@ msgstr "ต่อ" msgid "Some presets are modified." msgstr "ค่าที่ตั้งล่วงหน้าบางส่วนได้รับการแก้ไข" -msgid "" -"You can keep the modified presets for the new project, discard, or save " -"changes as new presets." -msgstr "" -"คุณสามารถเก็บค่าที่ตั้งล่วงหน้าที่แก้ไขแล้วไว้ในโปรเจ็กต์ใหม่ " -"ทิ้งหรือบันทึกการเปลี่ยนแปลงเป็นค่าที่ตั้งล่วงหน้าใหม่ได้" +msgid "You can keep the modified presets for the new project, discard, or save changes as new presets." +msgstr "คุณสามารถเก็บค่าที่ตั้งล่วงหน้าที่แก้ไขแล้วไว้ในโปรเจ็กต์ใหม่ ทิ้งหรือบันทึกการเปลี่ยนแปลงเป็นค่าที่ตั้งล่วงหน้าใหม่ได้" msgid "User logged out" msgstr "ผู้ใช้ออกจากระบบ" -msgid "" -"You are currently in Stealth Mode. To log into the Cloud, you need to " -"disable Stealth Mode first." +msgid "You are currently in Stealth Mode. To log into the Cloud, you need to disable Stealth Mode first." msgstr "คุณอยู่ในโหมดซ่อนตัวอยู่ หากต้องการเข้าสู่ระบบ Cloud ให้ปิดโหมดซ่อนตัวก่อน" msgid "Stealth Mode" @@ -1872,43 +1822,33 @@ msgstr "ไม่อนุญาตให้ใช้ไฟล์โครงก msgid "Open Project" msgstr "เปิดโปรเจกต์" -msgid "" -"The version of Orca Slicer is too low and needs to be updated to the latest " -"version before it can be used normally." -msgstr "" -"Orca Slicer " -"เวอร์ชันต่ำเกินไปและจำเป็นต้องอัปเดตเป็นเวอร์ชันล่าสุดก่อนจึงจะสามารถใช้งานได้ตามปกติ" +msgid "The version of Orca Slicer is too low and needs to be updated to the latest version before it can be used normally." +msgstr "Orca Slicer เวอร์ชันต่ำเกินไปและจำเป็นต้องอัปเดตเป็นเวอร์ชันล่าสุดก่อนจึงจะสามารถใช้งานได้ตามปกติ" msgid "" "Cloud sync conflict: this preset has a newer version in OrcaCloud.\n" -"Pull downloads the cloud copy. Force push overwrites it with your local " -"preset." +"Pull downloads the cloud copy. Force push overwrites it with your local preset." msgstr "" "ความขัดแย้งในการซิงก์ Cloud: ค่าที่ตั้งไว้ล่วงหน้านี้มีเวอร์ชันใหม่กว่าใน Orca Cloud\n" "ดึงข้อมูลจะดาวน์โหลดสำเนาจากคลาวด์ บังคับส่งจะเขียนทับด้วยค่าที่ตั้งไว้ล่วงหน้าในเครื่องของคุณ" msgid "" "Cloud sync conflict: a preset with this name already exists in OrcaCloud.\n" -"Pull downloads the cloud copy. Force push overwrites it with your local " -"preset." +"Pull downloads the cloud copy. Force push overwrites it with your local preset." msgstr "" "ความขัดแย้งในการซิงก์ Cloud: มีค่าที่ตั้งไว้ล่วงหน้าชื่อนี้อยู่ใน Orca Cloud แล้ว\n" "ดึงข้อมูลจะดาวน์โหลดสำเนาจากคลาวด์ บังคับส่งจะเขียนทับด้วยค่าที่ตั้งไว้ล่วงหน้าในเครื่องของคุณ" msgid "" -"Cloud sync conflict: a preset with the same name was previously deleted from " -"the cloud.\n" -"Delete will delete your local preset. Force push overwrites it with your " -"local preset." +"Cloud sync conflict: a preset with the same name was previously deleted from the cloud.\n" +"Delete will delete your local preset. Force push overwrites it with your local preset." msgstr "" "ความขัดแย้งในการซิงก์ Cloud: ค่าที่ตั้งไว้ล่วงหน้าชื่อเดียวกันเคยถูกลบจากคลาวด์แล้ว\n" "ลบจะลบค่าที่ตั้งไว้ล่วงหน้าในเครื่องของคุณ บังคับส่งจะเขียนทับด้วยค่าที่ตั้งไว้ล่วงหน้าในเครื่องของคุณ" msgid "" -"Cloud sync conflict: there was an unexpected or unidentified preset " -"conflict.\n" -"Pull downloads the cloud copy. Force push overwrites it with your local " -"preset." +"Cloud sync conflict: there was an unexpected or unidentified preset conflict.\n" +"Pull downloads the cloud copy. Force push overwrites it with your local preset." msgstr "" "ความขัดแย้งในการซิงก์ Cloud: พบความขัดแย้งของค่าที่ตั้งไว้ล่วงหน้าที่ไม่คาดคิดหรือระบุไม่ได้\n" "ดึงข้อมูลจะดาวน์โหลดสำเนาจากคลาวด์ บังคับส่งจะเขียนทับด้วยค่าที่ตั้งไว้ล่วงหน้าในเครื่องของคุณ" @@ -1929,43 +1869,31 @@ msgstr "กำลังดึงข้อมูลเครื่องพิม msgid "Please try updating OrcaSlicer and then try again." msgstr "โปรดลองอัปเดต OrcaSlicer แล้วลองอีกครั้ง" -msgid "" -"The certificate has expired. Please check the time settings or update " -"OrcaSlicer and try again." +msgid "The certificate has expired. Please check the time settings or update OrcaSlicer and try again." msgstr "ใบรับรองหมดอายุแล้ว โปรดตรวจสอบการตั้งค่าเวลาหรืออัปเดต OrcaSlicer แล้วลองอีกครั้ง" -msgid "" -"The certificate is no longer valid and the printing functions are " -"unavailable." +msgid "The certificate is no longer valid and the printing functions are unavailable." msgstr "ใบรับรองไม่ถูกต้องอีกต่อไป และฟังก์ชันการพิมพ์ไม่พร้อมใช้งาน" -msgid "" -"Internal error. Please try upgrading the firmware and OrcaSlicer version. If " -"the issue persists, contact support." -msgstr "" -"ข้อผิดพลาดภายใน โปรดลองอัปเกรดเฟิร์มแวร์และเวอร์ชัน OrcaSlicer หากปัญหายังคงอยู่ " -"โปรดติดต่อฝ่ายสนับสนุน" +msgid "Internal error. Please try upgrading the firmware and OrcaSlicer version. If the issue persists, contact support." +msgstr "ข้อผิดพลาดภายใน โปรดลองอัปเกรดเฟิร์มแวร์และเวอร์ชัน OrcaSlicer หากปัญหายังคงอยู่ โปรดติดต่อฝ่ายสนับสนุน" msgid "" -"To use OrcaSlicer with Bambu Lab printers, you need to enable LAN mode and " -"Developer mode on your printer.\n" +"To use OrcaSlicer with Bambu Lab printers, you need to enable LAN mode and Developer mode on your printer.\n" "\n" "Please go to your printer's settings and:\n" "1. Turn on LAN mode\n" "2. Enable Developer mode\n" "\n" -"Developer mode allows the printer to work exclusively through local network " -"access, enabling full functionality with OrcaSlicer." +"Developer mode allows the printer to work exclusively through local network access, enabling full functionality with OrcaSlicer." msgstr "" -"หากต้องการใช้ OrcaSlicer กับเครื่องพิมพ์ Bambu Lab คุณต้องเปิดใช้งานโหมด LAN " -"และโหมดนักพัฒนาซอฟต์แวร์บนเครื่องพิมพ์ของคุณ\n" +"หากต้องการใช้ OrcaSlicer กับเครื่องพิมพ์ Bambu Lab คุณต้องเปิดใช้งานโหมด LAN และโหมดนักพัฒนาซอฟต์แวร์บนเครื่องพิมพ์ของคุณ\n" "\n" "โปรดไปที่การตั้งค่าเครื่องพิมพ์ของคุณและ:\n" "1. เปิดโหมด LAN\n" "2. เปิดใช้งานโหมดนักพัฒนาซอฟต์แวร์\n" "\n" -"โหมดนักพัฒนาซอฟต์แวร์ช่วยให้เครื่องพิมพ์ทำงานผ่านการเข้าถึงเครือข่ายท้องถิ่นโดยเฉพาะ " -"เปิดใช้งานฟังก์ชันการทำงานเต็มรูปแบบด้วย OrcaSlicer" +"โหมดนักพัฒนาซอฟต์แวร์ช่วยให้เครื่องพิมพ์ทำงานผ่านการเข้าถึงเครือข่ายท้องถิ่นโดยเฉพาะ เปิดใช้งานฟังก์ชันการทำงานเต็มรูปแบบด้วย OrcaSlicer" msgid "Network Plug-in Restriction" msgstr "ข้อจำกัดปลั๊กอินเครือข่าย" @@ -2005,23 +1933,14 @@ msgstr "" "ไม่สามารถย้ายค่าที่ตั้งล่วงหน้าของผู้ใช้:\n" "%s" -msgid "" -"The number of user presets cached in the cloud has exceeded the upper limit, " -"newly created user presets can only be used locally." -msgstr "" -"จำนวนค่าที่ตั้งไว้ล่วงหน้าของผู้ใช้ที่แคชไว้ในคลาวด์เกินขีดจำกัดสูงสุด " -"ค่าที่ตั้งไว้ล่วงหน้าของผู้ใช้ที่สร้างขึ้นใหม่สามารถใช้ได้เฉพาะในเครื่องเท่านั้น" +msgid "The number of user presets cached in the cloud has exceeded the upper limit, newly created user presets can only be used locally." +msgstr "จำนวนค่าที่ตั้งไว้ล่วงหน้าของผู้ใช้ที่แคชไว้ในคลาวด์เกินขีดจำกัดสูงสุด ค่าที่ตั้งไว้ล่วงหน้าของผู้ใช้ที่สร้างขึ้นใหม่สามารถใช้ได้เฉพาะในเครื่องเท่านั้น" msgid "Sync user presets" msgstr "ซิงค์การตั้งค่าล่วงหน้าของผู้ใช้" -msgid "" -"The preset content is too large to sync to the cloud (exceeds 1MB). Please " -"reduce the preset size by removing custom configurations or use it locally " -"only." -msgstr "" -"เนื้อหาที่ตั้งไว้ล่วงหน้ามีขนาดใหญ่เกินกว่าจะซิงค์กับระบบคลาวด์ (เกิน 1MB) " -"โปรดลดขนาดที่กำหนดไว้ล่วงหน้าโดยการลบการกำหนดค่าที่กำหนดเองออกหรือใช้เฉพาะในเครื่องเท่านั้น" +msgid "The preset content is too large to sync to the cloud (exceeds 1MB). Please reduce the preset size by removing custom configurations or use it locally only." +msgstr "เนื้อหาที่ตั้งไว้ล่วงหน้ามีขนาดใหญ่เกินกว่าจะซิงค์กับระบบคลาวด์ (เกิน 1MB) โปรดลดขนาดที่กำหนดไว้ล่วงหน้าโดยการลบการกำหนดค่าที่กำหนดเองออกหรือใช้เฉพาะในเครื่องเท่านั้น" #, c-format, boost-format msgid "%s updated from %s to %s" @@ -2073,12 +1992,8 @@ msgstr "การอัปโหลดอย่างต่อเนื่อง msgid "Select a G-code file:" msgstr "เลือกไฟล์ G-code:" -msgid "" -"Could not start URL download. Destination folder is not set. Please choose " -"destination folder in Configuration Wizard." -msgstr "" -"ไม่สามารถเริ่มการดาวน์โหลด URL ไม่ได้ตั้งค่าโฟลเดอร์ปลายทาง " -"โปรดเลือกโฟลเดอร์ปลายทางในตัวช่วยสร้างการกำหนดค่า" +msgid "Could not start URL download. Destination folder is not set. Please choose destination folder in Configuration Wizard." +msgstr "ไม่สามารถเริ่มการดาวน์โหลด URL ไม่ได้ตั้งค่าโฟลเดอร์ปลายทาง โปรดเลือกโฟลเดอร์ปลายทางในตัวช่วยสร้างการกำหนดค่า" msgid "Import File" msgstr "นำเข้าไฟล์" @@ -2256,15 +2171,11 @@ msgid "Orca String Hell" msgstr "ออร์ก้าสตริงนรก" msgid "" -"This model features text embossment on the top surface. For optimal results, " -"it is advisable to set the 'One Wall Threshold (min_width_top_surface)' to 0 " -"for the 'Only One Wall on Top Surfaces' to work best.\n" +"This model features text embossment on the top surface. For optimal results, it is advisable to set the 'One Wall Threshold (min_width_top_surface)' to 0 for the 'Only One Wall on Top Surfaces' to work best.\n" "Yes - Change these settings automatically\n" "No - Do not change these settings for me" msgstr "" -"รุ่นนี้มีข้อความนูนบนพื้นผิวด้านบน เพื่อให้ได้ผลลัพธ์ที่ดีที่สุด ขอแนะนำให้ตั้งค่า 'เกณฑ์ผนังด้านเดียว " -"(min_width_top_surface)' เป็น 0 เพื่อให้ 'ผนังด้านเดียวเท่านั้นบนพื้นผิวด้านบน' " -"เพื่อให้ทำงานได้ดีที่สุด\n" +"รุ่นนี้มีข้อความนูนบนพื้นผิวด้านบน เพื่อให้ได้ผลลัพธ์ที่ดีที่สุด ขอแนะนำให้ตั้งค่า 'เกณฑ์ผนังด้านเดียว (min_width_top_surface)' เป็น 0 เพื่อให้ 'ผนังด้านเดียวเท่านั้นบนพื้นผิวด้านบน' เพื่อให้ทำงานได้ดีที่สุด\n" "ใช่ - เปลี่ยนการตั้งค่าเหล่านี้โดยอัตโนมัติ\n" "ไม่ - อย่าเปลี่ยนการตั้งค่าเหล่านี้ให้ฉัน" @@ -2668,9 +2579,7 @@ msgstr "ทั่วไป" msgid "Switch to per-object setting mode to edit modifier settings." msgstr "สลับไปที่โหมดการตั้งค่าต่อวัตถุเพื่อแก้ไขการตั้งค่าตัวแก้ไข" -msgid "" -"Switch to per-object setting mode to edit process settings of selected " -"objects." +msgid "Switch to per-object setting mode to edit process settings of selected objects." msgstr "สลับไปที่โหมดการตั้งค่าต่ออ็อบเจ็กต์เพื่อแก้ไขการตั้งค่ากระบวนการของอ็อบเจ็กต์ที่เลือก" msgid "Remove paint-on fuzzy skin" @@ -2685,17 +2594,14 @@ msgstr "ลบส่วนที่เป็นของแข็งออกจ msgid "Delete negative volume from object which is a part of cut" msgstr "ลบปริมาตรเชิงลบออกจากวัตถุซึ่งเป็นส่วนหนึ่งของการตัด" -msgid "" -"To save cut correspondence you can delete all connectors from all related " -"objects." +msgid "To save cut correspondence you can delete all connectors from all related objects." msgstr "หากต้องการบันทึกการติดต่อตัด คุณสามารถลบตัวเชื่อมต่อทั้งหมดออกจากวัตถุที่เกี่ยวข้องทั้งหมดได้" msgid "" "This action will break a cut correspondence.\n" "After that, model consistency can't be guaranteed.\n" "\n" -"To manipulate with solid parts or negative volumes you have to invalidate " -"cut information first." +"To manipulate with solid parts or negative volumes you have to invalidate cut information first." msgstr "" "การกระทำนี้จะตัดความเชื่อมโยงของข้อมูลการตัด\n" "หลังจากนั้นจะไม่สามารถรับประกันความสอดคล้องของโมเดลได้\n" @@ -2750,13 +2656,10 @@ msgstr "เลเยอร์" msgid "Selection conflicts" msgstr "ข้อขัดแย้งในการคัดเลือก" -msgid "" -"If the first selected item is an object, the second should also be an object." +msgid "If the first selected item is an object, the second should also be an object." msgstr "หากรายการแรกที่เลือกเป็นวัตถุ รายการที่สองก็ควรเป็นวัตถุด้วย" -msgid "" -"If the first selected item is a part, the second should be a part in the " -"same object." +msgid "If the first selected item is a part, the second should be a part in the same object." msgstr "หากรายการแรกที่เลือกเป็นส่วนหนึ่ง รายการที่สองควรเป็นส่วนในวัตถุเดียวกัน" msgid "The type of the last solid object part cannot be changed." @@ -2786,12 +2689,8 @@ msgid "Repairing was canceled" msgstr "การซ่อมแซมถูกยกเลิก" #, c-format, boost-format -msgid "" -"\"%s\" will exceed 1 million faces after this subdivision, which may " -"increase slicing time. Do you want to continue?" -msgstr "" -"\"%s\" จะมีเกิน 1 ล้านผิวหน้าหลังจากการแบ่งย่อยนี้ ซึ่งอาจเพิ่มเวลาการแบ่งส่วน " -"คุณต้องการดำเนินการต่อหรือไม่?" +msgid "\"%s\" will exceed 1 million faces after this subdivision, which may increase slicing time. Do you want to continue?" +msgstr "\"%s\" จะมีเกิน 1 ล้านผิวหน้าหลังจากการแบ่งย่อยนี้ ซึ่งอาจเพิ่มเวลาการแบ่งส่วน คุณต้องการดำเนินการต่อหรือไม่?" #, c-format, boost-format msgid "\"%s\" part's mesh contains errors. Please repair it first." @@ -3005,23 +2904,14 @@ msgstr "โหลด" msgid "Unload" msgstr "ถอนเส้น" -msgid "" -"Choose an AMS slot then press \"Load\" or \"Unload\" button to automatically " -"load or unload filament." -msgstr "" -"เลือกช่อง AMS แล้วกดปุ่ม \"โหลด\" หรือ \"ถอนเส้น\" เพื่อโหลดหรือถอนเส้นพลาสติกโดยอัตโนมัติ" +msgid "Choose an AMS slot then press \"Load\" or \"Unload\" button to automatically load or unload filament." +msgstr "เลือกช่อง AMS แล้วกดปุ่ม \"โหลด\" หรือ \"ถอนเส้น\" เพื่อโหลดหรือถอนเส้นพลาสติกโดยอัตโนมัติ" -msgid "" -"Filament type is unknown which is required to perform this action. Please " -"set target filament's informations." -msgstr "" -"ไม่ทราบประเภทเส้นพลาสติกซึ่งจำเป็นต่อการดำเนินการนี้ กรุณาตั้งค่าข้อมูลของเส้นพลาสติกเป้าหมาย" +msgid "Filament type is unknown which is required to perform this action. Please set target filament's informations." +msgstr "ไม่ทราบประเภทเส้นพลาสติกซึ่งจำเป็นต่อการดำเนินการนี้ กรุณาตั้งค่าข้อมูลของเส้นพลาสติกเป้าหมาย" -msgid "" -"Changing fan speed during printing may affect print quality, please choose " -"carefully." -msgstr "" -"การเปลี่ยนความเร็วพัดลมระหว่างการพิมพ์อาจส่งผลต่อคุณภาพการพิมพ์ โปรดเลือกอย่างระมัดระวัง" +msgid "Changing fan speed during printing may affect print quality, please choose carefully." +msgstr "การเปลี่ยนความเร็วพัดลมระหว่างการพิมพ์อาจส่งผลต่อคุณภาพการพิมพ์ โปรดเลือกอย่างระมัดระวัง" msgid "Change Anyway" msgstr "เปลี่ยนยังไงก็ได้" @@ -3032,25 +2922,14 @@ msgstr "ปิด" msgid "Filter" msgstr "กรอง" -msgid "" -"Enabling filtration redirects the right fan to filter gas, which may reduce " -"cooling performance." -msgstr "" -"การเปิดใช้งานการกรองจะเปลี่ยนเส้นทางพัดลมที่เหมาะสมไปกรองก๊าซ " -"ซึ่งอาจลดประสิทธิภาพการทำความเย็น" +msgid "Enabling filtration redirects the right fan to filter gas, which may reduce cooling performance." +msgstr "การเปิดใช้งานการกรองจะเปลี่ยนเส้นทางพัดลมที่เหมาะสมไปกรองก๊าซ ซึ่งอาจลดประสิทธิภาพการทำความเย็น" -msgid "" -"Enabling filtration during printing may reduce cooling and affect print " -"quality. Please choose carefully." -msgstr "" -"การเปิดใช้งานการกรองระหว่างการพิมพ์อาจลดความเย็นและส่งผลต่อคุณภาพการพิมพ์ " -"โปรดเลือกอย่างระมัดระวัง" +msgid "Enabling filtration during printing may reduce cooling and affect print quality. Please choose carefully." +msgstr "การเปิดใช้งานการกรองระหว่างการพิมพ์อาจลดความเย็นและส่งผลต่อคุณภาพการพิมพ์ โปรดเลือกอย่างระมัดระวัง" -msgid "" -"The selected material only supports the current fan mode, and it can't be " -"changed during printing." -msgstr "" -"วัสดุที่เลือกรองรับเฉพาะโหมดพัดลมปัจจุบันเท่านั้น และไม่สามารถเปลี่ยนแปลงได้ในระหว่างการพิมพ์" +msgid "The selected material only supports the current fan mode, and it can't be changed during printing." +msgstr "วัสดุที่เลือกรองรับเฉพาะโหมดพัดลมปัจจุบันเท่านั้น และไม่สามารถเปลี่ยนแปลงได้ในระหว่างการพิมพ์" msgid "Cooling" msgstr "การระบายความร้อน" @@ -3077,31 +2956,17 @@ msgstr "อินเนอร์ลูป" msgid "Top" msgstr "บน" -msgid "" -"The fan controls the temperature during printing to improve print quality. " -"The system automatically adjusts the fan's switch and speed according to " -"different printing materials." -msgstr "" -"พัดลมจะควบคุมอุณหภูมิระหว่างการพิมพ์เพื่อปรับปรุงคุณภาพการพิมพ์ " -"ระบบจะปรับสวิตช์และความเร็วของพัดลมโดยอัตโนมัติตามวัสดุการพิมพ์ที่แตกต่างกัน" +msgid "The fan controls the temperature during printing to improve print quality. The system automatically adjusts the fan's switch and speed according to different printing materials." +msgstr "พัดลมจะควบคุมอุณหภูมิระหว่างการพิมพ์เพื่อปรับปรุงคุณภาพการพิมพ์ ระบบจะปรับสวิตช์และความเร็วของพัดลมโดยอัตโนมัติตามวัสดุการพิมพ์ที่แตกต่างกัน" -msgid "" -"Cooling mode is suitable for printing PLA/PETG/TPU materials and filters the " -"chamber air." +msgid "Cooling mode is suitable for printing PLA/PETG/TPU materials and filters the chamber air." msgstr "โหมดทำความเย็นเหมาะสำหรับการพิมพ์วัสดุ PLA/PETG/TPU และกรองอากาศในห้อง" -msgid "" -"Heating mode is suitable for printing ABS/ASA/PC/PA materials and circulates " -"filters the chamber air." -msgstr "" -"โหมดทำความร้อนเหมาะสำหรับการพิมพ์วัสดุ ABS/ASA/PC/PA และหมุนเวียนตัวกรองอากาศในห้อง" +msgid "Heating mode is suitable for printing ABS/ASA/PC/PA materials and circulates filters the chamber air." +msgstr "โหมดทำความร้อนเหมาะสำหรับการพิมพ์วัสดุ ABS/ASA/PC/PA และหมุนเวียนตัวกรองอากาศในห้อง" -msgid "" -"Strong cooling mode is suitable for printing PLA/TPU materials. In this " -"mode, the printouts will be fully cooled." -msgstr "" -"โหมดระบายความร้อนสูงเหมาะสำหรับการพิมพ์วัสดุ PLA/TPU ในโหมดนี้ " -"งานพิมพ์จะถูกระบายความร้อนเต็มที่" +msgid "Strong cooling mode is suitable for printing PLA/TPU materials. In this mode, the printouts will be fully cooled." +msgstr "โหมดระบายความร้อนสูงเหมาะสำหรับการพิมพ์วัสดุ PLA/TPU ในโหมดนี้ งานพิมพ์จะถูกระบายความร้อนเต็มที่" msgid "Cooling mode is suitable for printing PLA/PETG/TPU materials." msgstr "โหมดทำความเย็นเหมาะสำหรับการพิมพ์วัสดุ PLA/PETG/TPU" @@ -3243,22 +3108,18 @@ msgstr "การจัด" msgid "Arranging canceled." msgstr "ยกเลิกการจัดเตรียมแล้ว" -msgid "" -"Arranging complete, but some items were not able to be arranged. Reduce " -"spacing and try again." +msgid "Arranging complete, but some items were not able to be arranged. Reduce spacing and try again." msgstr "จัดเรียงเสร็จแล้ว แต่มีบางรายการที่จัดวางไม่ได้ โปรดลดระยะห่างแล้วลองอีกครั้ง" msgid "Arranging done." msgstr "จัดการเรียบร้อยแล้ว." -msgid "" -"Arrange failed. Found some exceptions when processing object geometries." +msgid "Arrange failed. Found some exceptions when processing object geometries." msgstr "การจัดเรียงล้มเหลว พบข้อยกเว้นบางประการเมื่อประมวลผลรูปทรงของวัตถุ" #, c-format, boost-format msgid "" -"Arrangement ignored the following objects which can't fit into a single " -"bed:\n" +"Arrangement ignored the following objects which can't fit into a single bed:\n" "%s" msgstr "" "การจัดวางจะละเลยวัตถุต่อไปนี้ซึ่งไม่สามารถบรรจุลงในฐานพิมพ์เดี่ยวได้:\n" @@ -3329,9 +3190,7 @@ msgstr "การเชื่อมต่อบริการคลาวด์ msgid "Print file not found; please slice again." msgstr "ไม่พบไฟล์พิมพ์ โปรดสไลซ์อีกครั้ง" -msgid "" -"The print file exceeds the maximum allowable size (1GB). Please simplify the " -"model and slice again." +msgid "The print file exceeds the maximum allowable size (1GB). Please simplify the model and slice again." msgstr "ไฟล์พิมพ์เกินขนาดสูงสุดที่อนุญาต (1GB) โปรดทำให้โมเดลง่ายขึ้นและแบ่งส่วนอีกครั้ง" msgid "Failed to send the print job. Please try again." @@ -3340,22 +3199,16 @@ msgstr "ไม่สามารถส่งงานพิมพ์ได้ msgid "Failed to upload file to ftp. Please try again." msgstr "ไม่สามารถอัปโหลดไฟล์ไปยัง ftp โปรดลองอีกครั้ง" -msgid "" -"Check the current status of the Bambu Lab server by clicking on the link " -"above." +msgid "Check the current status of the Bambu Lab server by clicking on the link above." msgstr "ตรวจสอบสถานะปัจจุบันของเซิร์ฟเวอร์ Bambu Lab โดยคลิกลิงก์ด้านบน" -msgid "" -"The size of the print file is too large. Please adjust the file size and try " -"again." +msgid "The size of the print file is too large. Please adjust the file size and try again." msgstr "ขนาดของไฟล์การพิมพ์ใหญ่เกินไป โปรดปรับขนาดไฟล์แล้วลองอีกครั้ง" msgid "Print file not found; please slice it again and send it for printing." msgstr "ไม่พบไฟล์พิมพ์ โปรดสไลซ์อีกครั้งและส่งไปพิมพ์" -msgid "" -"Failed to upload print file via FTP. Please check the network status and try " -"again." +msgid "Failed to upload print file via FTP. Please check the network status and try again." msgstr "อัปโหลดไฟล์พิมพ์ผ่าน FTP ไม่สำเร็จ โปรดตรวจสอบสถานะเครือข่ายแล้วลองอีกครั้ง" msgid "Sending print job over LAN" @@ -3391,24 +3244,14 @@ msgstr "รหัสเข้าถึง:%s ที่อยู่ IP:%s" msgid "A Storage needs to be inserted before printing via LAN." msgstr "ต้องใส่ที่เก็บข้อมูลก่อนที่จะพิมพ์ผ่าน LAN" -msgid "" -"Sending print job over LAN, but the Storage in the printer is abnormal and " -"print-issues may be caused by this." -msgstr "" -"การส่งงานพิมพ์ผ่าน LAN แต่ที่เก็บข้อมูลในเครื่องพิมพ์ผิดปกติและปัญหาการพิมพ์อาจมีสาเหตุจากสิ่งนี้" +msgid "Sending print job over LAN, but the Storage in the printer is abnormal and print-issues may be caused by this." +msgstr "การส่งงานพิมพ์ผ่าน LAN แต่ที่เก็บข้อมูลในเครื่องพิมพ์ผิดปกติและปัญหาการพิมพ์อาจมีสาเหตุจากสิ่งนี้" -msgid "" -"The Storage in the printer is abnormal. Please replace it with a normal " -"Storage before sending print job to printer." -msgstr "" -"พื้นที่จัดเก็บข้อมูลในเครื่องพิมพ์ผิดปกติ โปรดแทนที่ด้วยที่เก็บข้อมูลปกติก่อนที่จะส่งงานพิมพ์ไปยังเครื่องพิมพ์" +msgid "The Storage in the printer is abnormal. Please replace it with a normal Storage before sending print job to printer." +msgstr "พื้นที่จัดเก็บข้อมูลในเครื่องพิมพ์ผิดปกติ โปรดแทนที่ด้วยที่เก็บข้อมูลปกติก่อนที่จะส่งงานพิมพ์ไปยังเครื่องพิมพ์" -msgid "" -"The Storage in the printer is read-only. Please replace it with a normal " -"Storage before sending print job to printer." -msgstr "" -"ที่เก็บข้อมูลในเครื่องพิมพ์เป็นแบบอ่านอย่างเดียว " -"โปรดแทนที่ด้วยที่เก็บข้อมูลปกติก่อนที่จะส่งงานพิมพ์ไปยังเครื่องพิมพ์" +msgid "The Storage in the printer is read-only. Please replace it with a normal Storage before sending print job to printer." +msgstr "ที่เก็บข้อมูลในเครื่องพิมพ์เป็นแบบอ่านอย่างเดียว โปรดแทนที่ด้วยที่เก็บข้อมูลปกติก่อนที่จะส่งงานพิมพ์ไปยังเครื่องพิมพ์" msgid "Encountered an unknown error with the Storage status. Please try again." msgstr "พบข้อผิดพลาดที่ไม่ทราบสาเหตุเกี่ยวกับสถานะการจัดเก็บ โปรดลองอีกครั้ง" @@ -3426,24 +3269,14 @@ msgstr "ส่งสำเร็จ หน้านี้จะปิดใน % msgid "Storage needs to be inserted before sending to printer." msgstr "ต้องใส่ที่เก็บข้อมูลก่อนที่จะส่งไปยังเครื่องพิมพ์" -msgid "" -"Sending G-code file over LAN, but the Storage in the printer is abnormal and " -"print-issues may be caused by this." -msgstr "" -"กำลังส่งไฟล์ G-code ผ่าน LAN " -"แต่ที่เก็บข้อมูลในเครื่องพิมพ์ผิดปกติและปัญหาการพิมพ์อาจมีสาเหตุจากสิ่งนี้" +msgid "Sending G-code file over LAN, but the Storage in the printer is abnormal and print-issues may be caused by this." +msgstr "กำลังส่งไฟล์ G-code ผ่าน LAN แต่ที่เก็บข้อมูลในเครื่องพิมพ์ผิดปกติและปัญหาการพิมพ์อาจมีสาเหตุจากสิ่งนี้" -msgid "" -"The Storage in the printer is abnormal. Please replace it with a normal " -"Storage before sending to printer." +msgid "The Storage in the printer is abnormal. Please replace it with a normal Storage before sending to printer." msgstr "พื้นที่จัดเก็บข้อมูลในเครื่องพิมพ์ผิดปกติ โปรดแทนที่ด้วยที่เก็บข้อมูลปกติก่อนที่จะส่งไปยังเครื่องพิมพ์" -msgid "" -"The Storage in the printer is read-only. Please replace it with a normal " -"Storage before sending to printer." -msgstr "" -"ที่เก็บข้อมูลในเครื่องพิมพ์เป็นแบบอ่านอย่างเดียว " -"โปรดแทนที่ด้วยที่เก็บข้อมูลปกติก่อนที่จะส่งไปยังเครื่องพิมพ์" +msgid "The Storage in the printer is read-only. Please replace it with a normal Storage before sending to printer." +msgstr "ที่เก็บข้อมูลในเครื่องพิมพ์เป็นแบบอ่านอย่างเดียว โปรดแทนที่ด้วยที่เก็บข้อมูลปกติก่อนที่จะส่งไปยังเครื่องพิมพ์" msgid "Bad input data for EmbossCreateObjectJob." msgstr "ข้อมูลอินพุตไม่ถูกต้องสำหรับ EmbossCreateObjectJob" @@ -3486,12 +3319,8 @@ msgstr "การปรับสภาพล่วงหน้าด้วยค msgid "Remaining time: Calculating..." msgstr "เวลาที่เหลืออยู่: กำลังคำนวณ..." -msgid "" -"The heated bed's thermal preconditioning helps optimize the first layer " -"print quality. Printing will start once preconditioning is complete." -msgstr "" -"การปรับสภาพความร้อนล่วงหน้าของฐานพิมพ์อุ่นช่วยปรับคุณภาพการพิมพ์ชั้นแรกให้เหมาะสมที่สุด " -"การพิมพ์จะเริ่มขึ้นเมื่อการปรับสภาพเบื้องต้นเสร็จสิ้น" +msgid "The heated bed's thermal preconditioning helps optimize the first layer print quality. Printing will start once preconditioning is complete." +msgstr "การปรับสภาพความร้อนล่วงหน้าของฐานพิมพ์อุ่นช่วยปรับคุณภาพการพิมพ์ชั้นแรกให้เหมาะสมที่สุด การพิมพ์จะเริ่มขึ้นเมื่อการปรับสภาพเบื้องต้นเสร็จสิ้น" #, c-format, boost-format msgid "Remaining time: %dmin%ds" @@ -3500,12 +3329,8 @@ msgstr "เวลาที่เหลือ: %d นาที %d วินาท msgid "Importing SLA archive" msgstr "การนำเข้าไฟล์เก็บถาวร SLA" -msgid "" -"The SLA archive doesn't contain any presets. Please activate some SLA " -"printer presets first before importing that SLA archive." -msgstr "" -"ไฟล์เก็บถาวร SLA ไม่มีค่าที่ตั้งไว้ล่วงหน้า โปรดเปิดใช้งานค่าที่ตั้งไว้ล่วงหน้าของเครื่องพิมพ์ SLA " -"ก่อนนำเข้าไฟล์เก็บถาวรนั้น" +msgid "The SLA archive doesn't contain any presets. Please activate some SLA printer presets first before importing that SLA archive." +msgstr "ไฟล์เก็บถาวร SLA ไม่มีค่าที่ตั้งไว้ล่วงหน้า โปรดเปิดใช้งานค่าที่ตั้งไว้ล่วงหน้าของเครื่องพิมพ์ SLA ก่อนนำเข้าไฟล์เก็บถาวรนั้น" msgid "Importing canceled." msgstr "การนำเข้าถูกยกเลิก" @@ -3513,12 +3338,8 @@ msgstr "การนำเข้าถูกยกเลิก" msgid "Importing done." msgstr "นำเข้าเรียบร้อยแล้ว" -msgid "" -"The imported SLA archive did not contain any presets. The current SLA " -"presets were used as fallback." -msgstr "" -"ไฟล์เก็บถาวร SLA ที่นำเข้าไม่มีการตั้งค่าล่วงหน้าใดๆ ค่าที่ตั้งไว้ล่วงหน้าของ SLA " -"ปัจจุบันถูกใช้เป็นทางเลือก" +msgid "The imported SLA archive did not contain any presets. The current SLA presets were used as fallback." +msgstr "ไฟล์เก็บถาวร SLA ที่นำเข้าไม่มีการตั้งค่าล่วงหน้าใดๆ ค่าที่ตั้งไว้ล่วงหน้าของ SLA ปัจจุบันถูกใช้เป็นทางเลือก" msgid "You cannot load an SLA project with a multi-part object on the bed" msgstr "คุณไม่สามารถโหลดโครงการ SLA ที่มีวัตถุหลายส่วนบนฐานพิมพ์ได้" @@ -3568,50 +3389,21 @@ msgstr "Orca Slicer ขึ้นอยู่กับ PrusaSlicer และ Bamb msgid "Libraries" msgstr "ห้องสมุด" -msgid "" -"This software uses open source components whose copyright and other " -"proprietary rights belong to their respective owners" -msgstr "" -"ซอฟต์แวร์นี้ใช้ส่วนประกอบโอเพ่นซอร์สซึ่งมีลิขสิทธิ์และกรรมสิทธิ์อื่น ๆ เป็นของเจ้าของที่เกี่ยวข้อง" +msgid "This software uses open source components whose copyright and other proprietary rights belong to their respective owners" +msgstr "ซอฟต์แวร์นี้ใช้ส่วนประกอบโอเพ่นซอร์สซึ่งมีลิขสิทธิ์และกรรมสิทธิ์อื่น ๆ เป็นของเจ้าของที่เกี่ยวข้อง" #, c-format, boost-format msgid "About %s" msgstr "เกี่ยวกับ %s" -msgid "" -"Open-source slicing stands on a tradition of collaboration and attribution. " -"Slic3r, created by Alessandro Ranellucci and the RepRap community, laid the " -"foundation. PrusaSlicer by Prusa Research built on that work, Bambu Studio " -"forked from PrusaSlicer, and SuperSlicer extended it with community-driven " -"enhancements. Each project carried the work of its predecessors forward, " -"crediting those who came before." -msgstr "" -"การแบ่งส่วนโอเพ่นซอร์สยืนหยัดบนพื้นฐานของการทำงานร่วมกันและการระบุแหล่งที่มา Slic3r " -"ซึ่งสร้างโดย Alessandro Ranellucci และชุมชน RepRap ได้วางรากฐาน PrusaSlicer โดย " -"Prusa Research สร้างขึ้นจากผลงานดังกล่าว Bambu Studio แยกจาก PrusaSlicer และ " -"SuperSlicer ได้ขยายขอบเขตด้วยการปรับปรุงที่ขับเคลื่อนโดยชุมชน " -"แต่ละโครงการได้สานต่องานของรุ่นก่อนไปข้างหน้า โดยให้เครดิตผู้ที่มาก่อนหน้านี้" +msgid "Open-source slicing stands on a tradition of collaboration and attribution. Slic3r, created by Alessandro Ranellucci and the RepRap community, laid the foundation. PrusaSlicer by Prusa Research built on that work, Bambu Studio forked from PrusaSlicer, and SuperSlicer extended it with community-driven enhancements. Each project carried the work of its predecessors forward, crediting those who came before." +msgstr "การแบ่งส่วนโอเพ่นซอร์สยืนหยัดบนพื้นฐานของการทำงานร่วมกันและการระบุแหล่งที่มา Slic3r ซึ่งสร้างโดย Alessandro Ranellucci และชุมชน RepRap ได้วางรากฐาน PrusaSlicer โดย Prusa Research สร้างขึ้นจากผลงานดังกล่าว Bambu Studio แยกจาก PrusaSlicer และ SuperSlicer ได้ขยายขอบเขตด้วยการปรับปรุงที่ขับเคลื่อนโดยชุมชน แต่ละโครงการได้สานต่องานของรุ่นก่อนไปข้างหน้า โดยให้เครดิตผู้ที่มาก่อนหน้านี้" -msgid "" -"OrcaSlicer began in that same spirit, drawing from PrusaSlicer, BambuStudio, " -"SuperSlicer, and CuraSlicer. But it has since grown far beyond its origins — " -"introducing advanced calibration tools, precise wall and seam control and " -"hundreds of other features." -msgstr "" -"OrcaSlicer เริ่มต้นด้วยจิตวิญญาณเดียวกัน โดยวาดจาก PrusaSlicer, BambuStudio, " -"SuperSlicer และ CuraSlicer แต่ตั้งแต่นั้นเป็นต้นมามันก็เติบโตไปไกลเกินกว่าต้นกำเนิด " -"โดยนำเสนอเครื่องมือสอบเทียบขั้นสูง การควบคุมผนังและรอยตะเข็บที่แม่นยำ และคุณสมบัติอื่น ๆ " -"หลายร้อยรายการ" +msgid "OrcaSlicer began in that same spirit, drawing from PrusaSlicer, BambuStudio, SuperSlicer, and CuraSlicer. But it has since grown far beyond its origins — introducing advanced calibration tools, precise wall and seam control and hundreds of other features." +msgstr "OrcaSlicer เริ่มต้นด้วยจิตวิญญาณเดียวกัน โดยวาดจาก PrusaSlicer, BambuStudio, SuperSlicer และ CuraSlicer แต่ตั้งแต่นั้นเป็นต้นมามันก็เติบโตไปไกลเกินกว่าต้นกำเนิด โดยนำเสนอเครื่องมือสอบเทียบขั้นสูง การควบคุมผนังและรอยตะเข็บที่แม่นยำ และคุณสมบัติอื่น ๆ หลายร้อยรายการ" -msgid "" -"Today, OrcaSlicer is the most widely used and actively developed open-source " -"slicer in the 3D printing community. Many of its innovations have been " -"adopted by other slicers, making it a driving force for the entire industry." -msgstr "" -"ปัจจุบัน OrcaSlicer " -"เป็นตัวแบ่งส่วนข้อมูลแบบโอเพ่นซอร์สที่ใช้กันอย่างแพร่หลายและได้รับการพัฒนาอย่างแข็งขันที่สุดในชุมชนการพิมพ์ " -"3 มิติ นวัตกรรมหลายอย่างของบริษัทได้ถูกนำไปใช้โดยตัวแบ่งส่วนข้อมูลอื่นๆ " -"ทำให้สิ่งนี้เป็นแรงผลักดันสำหรับอุตสาหกรรมทั้งหมด" +msgid "Today, OrcaSlicer is the most widely used and actively developed open-source slicer in the 3D printing community. Many of its innovations have been adopted by other slicers, making it a driving force for the entire industry." +msgstr "ปัจจุบัน OrcaSlicer เป็นตัวแบ่งส่วนข้อมูลแบบโอเพ่นซอร์สที่ใช้กันอย่างแพร่หลายและได้รับการพัฒนาอย่างแข็งขันที่สุดในชุมชนการพิมพ์ 3 มิติ นวัตกรรมหลายอย่างของบริษัทได้ถูกนำไปใช้โดยตัวแบ่งส่วนข้อมูลอื่นๆ ทำให้สิ่งนี้เป็นแรงผลักดันสำหรับอุตสาหกรรมทั้งหมด" msgid "Version" msgstr "เวอร์ชัน" @@ -3678,8 +3470,7 @@ msgid "Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)" msgstr "โปรดป้อนค่าที่ถูกต้อง (K ใน %.1f~%.1f, N ใน %.1f~%.1f)" msgid "" -"The nozzle flow is not set. Please set the nozzle flow rate before editing " -"the filament.\n" +"The nozzle flow is not set. Please set the nozzle flow rate before editing the filament.\n" "'Device -> Print parts'" msgstr "" "ไม่ได้ตั้งค่าการไหลของหัวฉีด โปรดตั้งค่าอัตราการไหลของหัวฉีดก่อนที่จะแก้ไขเส้นพลาสติก\n" @@ -3697,13 +3488,8 @@ msgstr "สีกำหนดเอง" msgid "Dynamic flow calibration" msgstr "ปรับเทียบการไหลแบบไดนามิก" -msgid "" -"The nozzle temp and max volumetric speed will affect the calibration " -"results. Please fill in the same values as the actual printing. They can be " -"auto-filled by selecting a filament preset." -msgstr "" -"อุณหภูมิหัวฉีดและความเร็วปริมาตรสูงสุดจะส่งผลต่อผลการสอบเทียบ " -"กรุณากรอกค่าเดียวกันกับการพิมพ์จริง สามารถไส้ในเส้นได้อัตโนมัติโดยเลือกค่าเส้นสายที่ตั้งไว้ล่วงหน้า" +msgid "The nozzle temp and max volumetric speed will affect the calibration results. Please fill in the same values as the actual printing. They can be auto-filled by selecting a filament preset." +msgstr "อุณหภูมิหัวฉีดและความเร็วปริมาตรสูงสุดจะส่งผลต่อผลการสอบเทียบ กรุณากรอกค่าเดียวกันกับการพิมพ์จริง สามารถไส้ในเส้นได้อัตโนมัติโดยเลือกค่าเส้นสายที่ตั้งไว้ล่วงหน้า" msgid "Nozzle Diameter" msgstr "เส้นผ่านศูนย์กลางหัวฉีด" @@ -3735,13 +3521,8 @@ msgstr "เริ่ม" msgid "Next" msgstr "ถัดไป" -msgid "" -"Calibration completed. Please find the most uniform extrusion line on your " -"hot bed like the picture below, and fill the value on its left side into the " -"factor K input box." -msgstr "" -"การสอบเทียบเสร็จสิ้น โปรดค้นหาเส้นการอัดรีดที่สม่ำเสมอที่สุดบนฐานพิมพ์ร้อนของคุณดังภาพด้านล่าง " -"และกรอกค่าทางด้านซ้ายลงในกล่องอินพุตแฟคเตอร์ K" +msgid "Calibration completed. Please find the most uniform extrusion line on your hot bed like the picture below, and fill the value on its left side into the factor K input box." +msgstr "การสอบเทียบเสร็จสิ้น โปรดค้นหาเส้นการอัดรีดที่สม่ำเสมอที่สุดบนฐานพิมพ์ร้อนของคุณดังภาพด้านล่าง และกรอกค่าทางด้านซ้ายลงในกล่องอินพุตแฟคเตอร์ K" msgid "Save" msgstr "บันทึก" @@ -3771,8 +3552,7 @@ msgstr "ไม่ได้แมป" msgid "" "Upper half area: Original\n" -"Lower half area: The filament from original project will be used when " -"unmapped.\n" +"Lower half area: The filament from original project will be used when unmapped.\n" "And you can click it to modify" msgstr "" "พื้นที่ครึ่งบน: ต้นฉบับ\n" @@ -3831,21 +3611,12 @@ msgid "Nozzle" msgstr "หัวฉีด" #, c-format, boost-format -msgid "" -"Note: the filament type(%s) does not match with the filament type(%s) in the " -"slicing file. If you want to use this slot, you can install %s instead of %s " -"and change slot information on the 'Device' page." -msgstr "" -"หมายเหตุ: ประเภทเส้นพลาสติก (%s) ไม่ตรงกับประเภทเส้นพลาสติก (%s) ในไฟล์การแบ่งส่วน " -"หากคุณต้องการใช้ช่องนี้ คุณสามารถติดตั้ง %s แทน %s และเปลี่ยนข้อมูลช่องบนหน้า 'อุปกรณ์'" +msgid "Note: the filament type(%s) does not match with the filament type(%s) in the slicing file. If you want to use this slot, you can install %s instead of %s and change slot information on the 'Device' page." +msgstr "หมายเหตุ: ประเภทเส้นพลาสติก (%s) ไม่ตรงกับประเภทเส้นพลาสติก (%s) ในไฟล์การแบ่งส่วน หากคุณต้องการใช้ช่องนี้ คุณสามารถติดตั้ง %s แทน %s และเปลี่ยนข้อมูลช่องบนหน้า 'อุปกรณ์'" #, c-format, boost-format -msgid "" -"Note: the slot is empty or undefined. If you want to use this slot, you can " -"install %s and change slot information on the 'Device' page." -msgstr "" -"หมายเหตุ: ช่องว่างเปล่าหรือไม่ได้กำหนด หากคุณต้องการใช้ช่องนี้ คุณสามารถติดตั้ง %s " -"และเปลี่ยนข้อมูลช่องได้ในหน้า 'อุปกรณ์'" +msgid "Note: the slot is empty or undefined. If you want to use this slot, you can install %s and change slot information on the 'Device' page." +msgstr "หมายเหตุ: ช่องว่างเปล่าหรือไม่ได้กำหนด หากคุณต้องการใช้ช่องนี้ คุณสามารถติดตั้ง %s และเปลี่ยนข้อมูลช่องได้ในหน้า 'อุปกรณ์'" msgid "Note: Only filament-loaded slots can be selected." msgstr "หมายเหตุ: สามารถเลือกได้เฉพาะช่องที่ใส่ฟิลาเมนท์เท่านั้น" @@ -3862,18 +3633,10 @@ msgstr "ปิดใช้ AMS" msgid "Print with filament on external spool" msgstr "พิมพ์ด้วยเส้นพลาสติกจากแกนภายนอก" -msgid "" -"Please change the desiccant when it is too wet. The indicator may not " -"represent accurately in following cases: when the lid is open or the " -"desiccant pack is changed. It takes a few hours to absorb the moisture, and " -"low temperatures also slow down the process." -msgstr "" -"โปรดเปลี่ยนสารดูดความชื้นเมื่อเปียกเกินไป ตัวบ่งชี้อาจแสดงไม่ถูกต้องในกรณีต่อไปนี้: " -"เมื่อเปิดฝาหรือเปลี่ยนชุดดูดความชื้น การดูดซับความชื้นใช้เวลาหลายชั่วโมง " -"และอุณหภูมิที่ต่ำยังทำให้กระบวนการช้าลงอีกด้วย" +msgid "Please change the desiccant when it is too wet. The indicator may not represent accurately in following cases: when the lid is open or the desiccant pack is changed. It takes a few hours to absorb the moisture, and low temperatures also slow down the process." +msgstr "โปรดเปลี่ยนสารดูดความชื้นเมื่อเปียกเกินไป ตัวบ่งชี้อาจแสดงไม่ถูกต้องในกรณีต่อไปนี้: เมื่อเปิดฝาหรือเปลี่ยนชุดดูดความชื้น การดูดซับความชื้นใช้เวลาหลายชั่วโมง และอุณหภูมิที่ต่ำยังทำให้กระบวนการช้าลงอีกด้วย" -msgid "" -"Configure which AMS slot should be used for a filament used in the print job." +msgid "Configure which AMS slot should be used for a filament used in the print job." msgstr "กำหนดค่าช่อง AMS ที่จะใช้สำหรับเส้นพลาสติกที่ใช้ในงานพิมพ์" msgid "Filament used in this print job" @@ -3900,9 +3663,7 @@ msgstr "ซ้าย" msgid "Right" msgstr "ขวา" -msgid "" -"When the current material run out, the printer will continue to print in the " -"following order." +msgid "When the current material run out, the printer will continue to print in the following order." msgstr "เมื่อวัสดุปัจจุบันหมด เครื่องพิมพ์จะพิมพ์ต่อตามลำดับต่อไปนี้" msgid "Identical filament: same brand, type and color." @@ -3911,21 +3672,17 @@ msgstr "เส้นพลาสติกเหมือนกัน: ยี่ msgid "Group" msgstr "กลุ่ม" -msgid "" -"When the current material runs out, the printer would use identical filament " -"to continue printing." +msgid "When the current material runs out, the printer would use identical filament to continue printing." msgstr "เมื่อวัสดุที่ใช้ในปัจจุบันหมด เครื่องพิมพ์จะใช้เส้นพลาสติกที่เหมือนกันเพื่อพิมพ์ต่อ" msgid "The printer does not currently support auto refill." msgstr "ขณะนี้เครื่องพิมพ์ไม่รองรับการโหลดเส้นอัตโนมัติ" -msgid "" -"AMS filament backup is not enabled; please enable it in the AMS settings." +msgid "AMS filament backup is not enabled; please enable it in the AMS settings." msgstr "ไม่ได้เปิดใช้งานการสำรองข้อมูลเส้นพลาสติก AMS โปรดเปิดใช้งานในการตั้งค่า AMS" msgid "" -"When the current filament runs out, the printer will use identical filament " -"to continue printing.\n" +"When the current filament runs out, the printer will use identical filament to continue printing.\n" "*Identical filament: same brand, type and color." msgstr "" "เมื่อเส้นพลาสติกปัจจุบันหมด เครื่องพิมพ์จะใช้เส้นพลาสติกที่เหมือนกันเพื่อพิมพ์ต่อ\n" @@ -3943,69 +3700,40 @@ msgstr "การตั้งค่า AMS" msgid "Insertion update" msgstr "การอัพเดตการแทรก" -msgid "" -"The AMS will automatically read the filament information when inserting a " -"new Bambu Lab filament spool. This takes about 20 seconds." -msgstr "" -"AMS จะอ่านข้อมูลเส้นพลาสติกโดยอัตโนมัติเมื่อใส่ม้วนเส้นพลาสติก Bambu Lab ใหม่ " -"ขั้นตอนนี้ใช้เวลาประมาณ 20 วินาที" +msgid "The AMS will automatically read the filament information when inserting a new Bambu Lab filament spool. This takes about 20 seconds." +msgstr "AMS จะอ่านข้อมูลเส้นพลาสติกโดยอัตโนมัติเมื่อใส่ม้วนเส้นพลาสติก Bambu Lab ใหม่ ขั้นตอนนี้ใช้เวลาประมาณ 20 วินาที" -msgid "" -"Note: if a new filament is inserted during printing, the AMS will not " -"automatically read any information until printing is completed." -msgstr "" -"หมายเหตุ: หากใส่เส้นพลาสติกใหม่ระหว่างการพิมพ์ AMS จะไม่อ่านข้อมูลใดๆ " -"โดยอัตโนมัติจนกว่าการพิมพ์จะเสร็จสิ้น" +msgid "Note: if a new filament is inserted during printing, the AMS will not automatically read any information until printing is completed." +msgstr "หมายเหตุ: หากใส่เส้นพลาสติกใหม่ระหว่างการพิมพ์ AMS จะไม่อ่านข้อมูลใดๆ โดยอัตโนมัติจนกว่าการพิมพ์จะเสร็จสิ้น" -msgid "" -"When inserting a new filament, the AMS will not automatically read its " -"information, leaving it blank for you to enter manually." -msgstr "" -"เมื่อใส่เส้นพลาสติกใหม่ AMS จะไม่อ่านข้อมูลโดยอัตโนมัติ ปล่อยให้ว่างไว้เพื่อให้คุณป้อนด้วยตนเอง" +msgid "When inserting a new filament, the AMS will not automatically read its information, leaving it blank for you to enter manually." +msgstr "เมื่อใส่เส้นพลาสติกใหม่ AMS จะไม่อ่านข้อมูลโดยอัตโนมัติ ปล่อยให้ว่างไว้เพื่อให้คุณป้อนด้วยตนเอง" msgid "Update on startup" msgstr "อัปเดตเมื่อเริ่มโปรแกรม" -msgid "" -"The AMS will automatically read the information of inserted filament on " -"start-up. It will take about 1 minute. The reading process will rotate the " -"filament spools." -msgstr "" -"AMS จะอ่านข้อมูลของเส้นพลาสติกที่ใส่ไว้โดยอัตโนมัติเมื่อสตาร์ทเครื่อง จะใช้เวลาประมาณ 1 นาที " -"กระบวนการอ่านจะม้วนแกนเส้นพลาสติก" +msgid "The AMS will automatically read the information of inserted filament on start-up. It will take about 1 minute. The reading process will rotate the filament spools." +msgstr "AMS จะอ่านข้อมูลของเส้นพลาสติกที่ใส่ไว้โดยอัตโนมัติเมื่อสตาร์ทเครื่อง จะใช้เวลาประมาณ 1 นาที กระบวนการอ่านจะม้วนแกนเส้นพลาสติก" -msgid "" -"The AMS will not automatically read information from inserted filament " -"during startup and will continue to use the information recorded before the " -"last shutdown." -msgstr "" -"AMS จะไม่อ่านข้อมูลจากเส้นพลาสติกที่แทรกไว้โดยอัตโนมัติระหว่างการเริ่มต้นระบบ " -"และจะยังคงใช้ข้อมูลที่บันทึกไว้ก่อนการปิดระบบครั้งล่าสุด" +msgid "The AMS will not automatically read information from inserted filament during startup and will continue to use the information recorded before the last shutdown." +msgstr "AMS จะไม่อ่านข้อมูลจากเส้นพลาสติกที่แทรกไว้โดยอัตโนมัติระหว่างการเริ่มต้นระบบ และจะยังคงใช้ข้อมูลที่บันทึกไว้ก่อนการปิดระบบครั้งล่าสุด" msgid "Update remaining capacity" msgstr "อัพเดตความจุคงเหลือ" -msgid "" -"AMS will attempt to estimate the remaining capacity of the Bambu Lab " -"filaments." +msgid "AMS will attempt to estimate the remaining capacity of the Bambu Lab filaments." msgstr "AMS จะพยายามประเมินกำลังการผลิตที่เหลืออยู่ของเส้นพลาสติก Bambu Lab" msgid "AMS filament backup" msgstr "การสำรองข้อมูลเส้นพลาสติก AMS" -msgid "" -"AMS will continue to another spool with matching filament properties " -"automatically when current filament runs out." -msgstr "" -"AMS " -"จะดำเนินการต่อไปยังแกนม้วนอื่นที่มีคุณสมบัติเส้นพลาสติกที่ตรงกันโดยอัตโนมัติเมื่อเส้นพลาสติกปัจจุบันหมด" +msgid "AMS will continue to another spool with matching filament properties automatically when current filament runs out." +msgstr "AMS จะดำเนินการต่อไปยังแกนม้วนอื่นที่มีคุณสมบัติเส้นพลาสติกที่ตรงกันโดยอัตโนมัติเมื่อเส้นพลาสติกปัจจุบันหมด" msgid "Air Printing Detection" msgstr "การตรวจจับการพิมพ์ทางอากาศ" -msgid "" -"Detects clogging and filament grinding, halting printing immediately to " -"conserve time and filament." +msgid "Detects clogging and filament grinding, halting printing immediately to conserve time and filament." msgstr "ตรวจจับการอุดตันและการบดเส้นพลาสติก หยุดการพิมพ์ทันทีเพื่อประหยัดเวลาและเส้นพลาสติก" msgid "AMS Type" @@ -4026,12 +3754,8 @@ msgstr "การสลับประเภท AMS จำเป็นต้อ msgid "Arrange AMS Order" msgstr "จัดเรียงคำสั่งซื้อ AMS" -msgid "" -"AMS ID will be reset. If you want a specific ID sequence, disconnect all AMS " -"before resetting and connect them in the desired order after resetting." -msgstr "" -"รหัส AMS จะถูกรีเซ็ต หากคุณต้องการลำดับ ID เฉพาะ ให้ยกเลิกการเชื่อมต่อ AMS " -"ทั้งหมดก่อนที่จะรีเซ็ต และเชื่อมต่อตามลำดับที่ต้องการหลังจากรีเซ็ต" +msgid "AMS ID will be reset. If you want a specific ID sequence, disconnect all AMS before resetting and connect them in the desired order after resetting." +msgstr "รหัส AMS จะถูกรีเซ็ต หากคุณต้องการลำดับ ID เฉพาะ ให้ยกเลิกการเชื่อมต่อ AMS ทั้งหมดก่อนที่จะรีเซ็ต และเชื่อมต่อตามลำดับที่ต้องการหลังจากรีเซ็ต" msgid "File" msgstr "ไฟล์" @@ -4039,26 +3763,16 @@ msgstr "ไฟล์" msgid "Calibration" msgstr "การปรับเทียบ" -msgid "" -"Failed to download the plug-in. Please check your firewall settings and VPN " -"software and retry." -msgstr "" -"ดาวน์โหลดปลั๊กอินไม่สำเร็จ โปรดตรวจสอบการตั้งค่าไฟร์วอลล์และซอฟต์แวร์ VPN แล้วลองอีกครั้ง" +msgid "Failed to download the plug-in. Please check your firewall settings and VPN software and retry." +msgstr "ดาวน์โหลดปลั๊กอินไม่สำเร็จ โปรดตรวจสอบการตั้งค่าไฟร์วอลล์และซอฟต์แวร์ VPN แล้วลองอีกครั้ง" -msgid "" -"Failed to install the plug-in. The plug-in file may be in use. Please " -"restart OrcaSlicer and try again. Also check whether it is blocked or has " -"been deleted by anti-virus software." -msgstr "" -"ไม่สามารถติดตั้งปลั๊กอินได้ ไฟล์ปลั๊กอินอาจถูกใช้งานอยู่ โปรดรีสตาร์ท OrcaSlicer แล้วลองอีกครั้ง " -"ตรวจสอบด้วยว่าซอฟต์แวร์ป้องกันไวรัสบล็อกหรือลบหรือไม่" +msgid "Failed to install the plug-in. The plug-in file may be in use. Please restart OrcaSlicer and try again. Also check whether it is blocked or has been deleted by anti-virus software." +msgstr "ไม่สามารถติดตั้งปลั๊กอินได้ ไฟล์ปลั๊กอินอาจถูกใช้งานอยู่ โปรดรีสตาร์ท OrcaSlicer แล้วลองอีกครั้ง ตรวจสอบด้วยว่าซอฟต์แวร์ป้องกันไวรัสบล็อกหรือลบหรือไม่" msgid "Click here to see more info" msgstr "คลิกที่นี่เพื่อดูข้อมูลเพิ่มเติม" -msgid "" -"The network plug-in was installed but could not be loaded. Please restart " -"the application." +msgid "The network plug-in was installed but could not be loaded. Please restart the application." msgstr "ติดตั้งปลั๊กอินเครือข่ายแล้ว แต่ไม่สามารถโหลดได้ กรุณารีสตาร์ทแอปพลิเคชัน" msgid "Restart Required" @@ -4067,19 +3781,13 @@ msgstr "ต้องเริ่มใหม่" msgid "Please home all axes (click " msgstr "กรุณากลับบ้านทุกแกน (คลิก" -msgid "" -") to locate the toolhead's position. This prevents device moving beyond the " -"printable boundary and causing equipment wear." -msgstr "" -") เพื่อค้นหาตำแหน่งของหัวเครื่องมือ เพื่อป้องกันไม่ให้อุปกรณ์เคลื่อนที่เกินขอบเขตที่สามารถพิมพ์ได้ " -"และทำให้อุปกรณ์สึกหรอ" +msgid ") to locate the toolhead's position. This prevents device moving beyond the printable boundary and causing equipment wear." +msgstr ") เพื่อค้นหาตำแหน่งของหัวเครื่องมือ เพื่อป้องกันไม่ให้อุปกรณ์เคลื่อนที่เกินขอบเขตที่สามารถพิมพ์ได้ และทำให้อุปกรณ์สึกหรอ" msgid "Go Home" msgstr "กลับจุดอ้างอิง" -msgid "" -"An error occurred. The system may have run out of memory, or a bug may have " -"occurred." +msgid "An error occurred. The system may have run out of memory, or a bug may have occurred." msgstr "เกิดข้อผิดพลาด ระบบอาจมีหน่วยความจำไม่เพียงพอ หรืออาจเกิดบั๊กในโปรแกรม" #, boost-format @@ -4127,45 +3835,27 @@ msgstr "เกิดข้อผิดพลาดที่ไม่รู้จ #, boost-format msgid "" -"Copying of the temporary G-code to the output G-code failed. Maybe the SD " -"card is write locked?\n" +"Copying of the temporary G-code to the output G-code failed. Maybe the SD card is write locked?\n" "Error message: %1%" msgstr "" "การคัดลอก G-code ชั่วคราวไปยังเอาต์พุต G-code ล้มเหลว บางทีการ์ด SD อาจถูกล็อคการเขียน?\n" "ข้อความแสดงข้อผิดพลาด: %1%" #, boost-format -msgid "" -"Copying of the temporary G-code to the output G-code failed. There might be " -"problem with target device, please try exporting again or using different " -"device. The corrupted output G-code is at %1%.tmp." -msgstr "" -"การคัดลอก G-code ชั่วคราวไปยังเอาต์พุต G-code ล้มเหลว อาจมีปัญหากับอุปกรณ์เป้าหมาย " -"โปรดลองส่งออกอีกครั้งหรือใช้อุปกรณ์อื่น G-code เอาต์พุตที่เสียหายอยู่ที่ %1%.tmp" +msgid "Copying of the temporary G-code to the output G-code failed. There might be problem with target device, please try exporting again or using different device. The corrupted output G-code is at %1%.tmp." +msgstr "การคัดลอก G-code ชั่วคราวไปยังเอาต์พุต G-code ล้มเหลว อาจมีปัญหากับอุปกรณ์เป้าหมาย โปรดลองส่งออกอีกครั้งหรือใช้อุปกรณ์อื่น G-code เอาต์พุตที่เสียหายอยู่ที่ %1%.tmp" #, boost-format -msgid "" -"Renaming of the G-code after copying to the selected destination folder has " -"failed. Current path is %1%.tmp. Please try exporting again." -msgstr "" -"การเปลี่ยนชื่อ G-code หลังจากคัดลอกไปยังโฟลเดอร์ปลายทางที่เลือกล้มเหลว เส้นทางปัจจุบันคือ %1%." -"tmp โปรดลองส่งออกอีกครั้ง" +msgid "Renaming of the G-code after copying to the selected destination folder has failed. Current path is %1%.tmp. Please try exporting again." +msgstr "การเปลี่ยนชื่อ G-code หลังจากคัดลอกไปยังโฟลเดอร์ปลายทางที่เลือกล้มเหลว เส้นทางปัจจุบันคือ %1%.tmp โปรดลองส่งออกอีกครั้ง" #, boost-format -msgid "" -"Copying of the temporary G-code has finished but the original code at %1% " -"couldn't be opened during copy check. The output G-code is at %2%.tmp." -msgstr "" -"การคัดลอก G-code ชั่วคราวเสร็จสิ้นแล้ว แต่ไม่สามารถเปิดรหัสต้นฉบับที่ %1% " -"ได้ในระหว่างการตรวจสอบการคัดลอก G-code เอาต์พุตอยู่ที่ %2%.tmp" +msgid "Copying of the temporary G-code has finished but the original code at %1% couldn't be opened during copy check. The output G-code is at %2%.tmp." +msgstr "การคัดลอก G-code ชั่วคราวเสร็จสิ้นแล้ว แต่ไม่สามารถเปิดรหัสต้นฉบับที่ %1% ได้ในระหว่างการตรวจสอบการคัดลอก G-code เอาต์พุตอยู่ที่ %2%.tmp" #, boost-format -msgid "" -"Copying of the temporary G-code has finished but the exported code couldn't " -"be opened during copy check. The output G-code is at %1%.tmp." -msgstr "" -"การคัดลอก G-code ชั่วคราวเสร็จสิ้นแล้ว " -"แต่ไม่สามารถเปิดรหัสที่ส่งออกได้ในระหว่างการตรวจสอบการคัดลอก G-code เอาต์พุตอยู่ที่ %1%.tmp" +msgid "Copying of the temporary G-code has finished but the exported code couldn't be opened during copy check. The output G-code is at %1%.tmp." +msgstr "การคัดลอก G-code ชั่วคราวเสร็จสิ้นแล้ว แต่ไม่สามารถเปิดรหัสที่ส่งออกได้ในระหว่างการตรวจสอบการคัดลอก G-code เอาต์พุตอยู่ที่ %1%.tmp" #, boost-format msgid "G-code file exported to %1%" @@ -4197,14 +3887,10 @@ msgstr "ต้นทาง" msgid "Size in X and Y of the rectangular plate." msgstr "ขนาดของแผ่นสี่เหลี่ยมเป็น X และ Y" -msgid "" -"Distance of the 0,0 G-code coordinate from the front left corner of the " -"rectangle." +msgid "Distance of the 0,0 G-code coordinate from the front left corner of the rectangle." msgstr "ระยะห่างของพิกัด 0,0 G-code จากมุมซ้ายหน้าของสี่เหลี่ยมผืนผ้า" -msgid "" -"Diameter of the print bed. It is assumed that origin (0,0) is located in the " -"center." +msgid "Diameter of the print bed. It is assumed that origin (0,0) is located in the center." msgstr "เส้นผ่านศูนย์กลางของฐานพิมพ์ สันนิษฐานว่าจุดกำเนิด (0,0) อยู่ตรงกลาง" msgid "Rectangular" @@ -4240,8 +3926,7 @@ msgstr "ข้อผิดพลาด! โมเดลไม่ถูกต้ msgid "The selected file contains no geometry." msgstr "ไฟล์ที่เลือกไม่มีรูปทรงเรขาคณิต" -msgid "" -"The selected file contains several disjointed areas. This is not supported." +msgid "The selected file contains several disjointed areas. This is not supported." msgstr "ไฟล์ที่เลือกมีพื้นที่แยกจากกันหลายส่วน ซึ่งยังไม่รองรับ" msgid "Choose a file to import bed texture from (PNG/SVG):" @@ -4261,17 +3946,14 @@ msgstr "แนะนำให้ใช้อุณหภูมิต่ำสุ msgid "A maximum temperature below %d℃ is recommended for %s.\n" msgstr "แนะนำให้ใช้อุณหภูมิสูงสุดต่ำกว่า %d℃ สำหรับ %s\n" -msgid "" -"The recommended minimum temperature cannot be higher than the recommended " -"maximum temperature.\n" +msgid "The recommended minimum temperature cannot be higher than the recommended maximum temperature.\n" msgstr "อุณหภูมิต่ำสุดที่แนะนำต้องไม่สูงกว่าอุณหภูมิสูงสุดที่แนะนำ\n" msgid "Please check.\n" msgstr "กรุณาตรวจสอบ.\n" msgid "" -"The nozzle may become clogged when the temperature is out of the recommended " -"range.\n" +"The nozzle may become clogged when the temperature is out of the recommended range.\n" "Please make sure whether to use this temperature to print.\n" "\n" msgstr "" @@ -4280,11 +3962,12 @@ msgstr "" "\n" #, c-format, boost-format -msgid "" -"The recommended nozzle temperature for this filament type is [%d, %d] " -"degrees Celsius." +msgid "The recommended nozzle temperature for this filament type is [%d, %d] degrees Celsius." msgstr "อุณหภูมิหัวฉีดที่แนะนำสำหรับเส้นพลาสติกประเภทนี้คือ [%d, %d] องศาเซลเซียส" +msgid "Adaptive Pressure Advance model validation failed:\n" +msgstr "" + msgid "" "Too small max volumetric speed.\n" "Value was reset to 0.5" @@ -4293,13 +3976,8 @@ msgstr "" "รีเซ็ตเป็น 0.5" #, c-format, boost-format -msgid "" -"Current chamber temperature is higher than the material's safe temperature; " -"this may result in material softening and nozzle clogs. The maximum safe " -"temperature for the material is %d" -msgstr "" -"อุณหภูมิห้องพิมพ์ปัจจุบันสูงกว่าอุณหภูมิปลอดภัยของวัสดุ ซึ่งอาจทำให้วัสดุนิ่มและหัวฉีดอุดตัน " -"อุณหภูมิปลอดภัยสูงสุดของวัสดุนี้คือ %d" +msgid "Current chamber temperature is higher than the material's safe temperature; this may result in material softening and nozzle clogs. The maximum safe temperature for the material is %d" +msgstr "อุณหภูมิห้องพิมพ์ปัจจุบันสูงกว่าอุณหภูมิปลอดภัยของวัสดุ ซึ่งอาจทำให้วัสดุนิ่มและหัวฉีดอุดตัน อุณหภูมิปลอดภัยสูงสุดของวัสดุนี้คือ %d" msgid "" "Layer height too small\n" @@ -4326,8 +4004,7 @@ msgstr "" msgid "" "This setting is only used for tuning model size by small amounts.\n" -"For example, when the model size has small errors or when tolerances are " -"incorrect. For large adjustments, please use the model scale function.\n" +"For example, when the model size has small errors or when tolerances are incorrect. For large adjustments, please use the model scale function.\n" "\n" "The value will be reset to 0." msgstr "" @@ -4350,15 +4027,12 @@ msgstr "" "\n" "ค่าจะถูกรีเซ็ตเป็น 0" -msgid "" -"Alternate extra wall does't work well when ensure vertical shell thickness " -"is set to All." +msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." msgstr "ผนังเสริมสำรองทำงานได้ไม่ดีเมื่อตั้งค่าความหนาของเปลือกแนวตั้งเป็นทั้งหมด" msgid "" "Change these settings automatically?\n" -"Yes - Change ensure vertical shell thickness to Moderate and enable " -"alternate extra wall\n" +"Yes - Change ensure vertical shell thickness to Moderate and enable alternate extra wall\n" "No - Don't use alternate extra wall" msgstr "" "เปลี่ยนการตั้งค่าเหล่านี้โดยอัตโนมัติหรือไม่\n" @@ -4366,14 +4040,12 @@ msgstr "" "ไม่ - ห้ามใช้ผนังเสริมสำรอง" msgid "" -"Prime tower does not work when Adaptive Layer Height or Independent Support " -"Layer Height is on.\n" +"Prime tower does not work when Adaptive Layer Height or Independent Support Layer Height is on.\n" "Which do you want to keep?\n" "YES - Keep Prime Tower\n" "NO - Keep Adaptive Layer Height and Independent Support Layer Height" msgstr "" -"ไพรม์ทาวเวอร์ไม่ทำงานเมื่อเปิดใช้งาน Adaptive Layer Height หรือ Independent ส่วนรองรับ " -"Layer Height\n" +"ไพรม์ทาวเวอร์ไม่ทำงานเมื่อเปิดใช้งาน Adaptive Layer Height หรือ Independent ส่วนรองรับ Layer Height\n" "คุณต้องการเก็บอันไหน?\n" "ใช่ - เก็บ Prime Tower ไว้\n" "ไม่ - คงความสูงของเลเยอร์แบบปรับได้และความสูงของเลเยอร์รองรับที่เป็นอิสระ" @@ -4415,32 +4087,20 @@ msgstr "" "ความลึกของล็อคควรน้อยกว่าความลึกของผิวหนัง\n" "รีเซ็ตเป็น 50% ของความลึกของผิว" -msgid "" -"Both [Extrusion] and [Combined] modes of Fuzzy Skin require the Arachne Wall " -"Generator to be enabled." -msgstr "" -"ทั้งโหมด [อัดขึ้นรูป] และ [รวม] ของ Fuzzy Skin จำเป็นต้องเปิดใช้งาน Arachne Wall " -"Generator" +msgid "Both [Extrusion] and [Combined] modes of Fuzzy Skin require the Arachne Wall Generator to be enabled." +msgstr "ทั้งโหมด [อัดขึ้นรูป] และ [รวม] ของ Fuzzy Skin จำเป็นต้องเปิดใช้งาน Arachne Wall Generator" msgid "" "Change these settings automatically?\n" "Yes - Enable Arachne Wall Generator\n" -"No - Disable Arachne Wall Generator and set [Displacement] mode of the " -"Fuzzy Skin" +"No - Disable Arachne Wall Generator and set [Displacement] mode of the Fuzzy Skin" msgstr "" "เปลี่ยนการตั้งค่าเหล่านี้โดยอัตโนมัติหรือไม่\n" "ใช่ - เปิดใช้งาน Arachne Wall Generator\n" -"ไม่ - ปิดการใช้งาน Arachne Wall Generator และตั้งค่าโหมด [Displacement] ของ Fuzzy " -"Skin" +"ไม่ - ปิดการใช้งาน Arachne Wall Generator และตั้งค่าโหมด [Displacement] ของ Fuzzy Skin" -msgid "" -"Spiral mode only works when wall loops is 1, support is disabled, clumping " -"detection by probing is disabled, top shell layers is 0, sparse infill " -"density is 0 and timelapse type is traditional." -msgstr "" -"โหมดเกลียวจะทำงานเฉพาะเมื่อลูปติดผนังเป็น 1, ปิดใช้งานส่วนรองรับ, " -"การตรวจจับการจับตัวเป็นก้อนโดยการตรวจวัดถูกปิดใช้งาน, ชั้นเปลือกด้านบนเป็น 0, " -"ความหนาแน่นของไส้ในแบบกระจายเป็น 0 และประเภทไทม์แลปส์เป็นแบบดั้งเดิม" +msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." +msgstr "โหมดเกลียวจะทำงานเฉพาะเมื่อลูปติดผนังเป็น 1, ปิดใช้งานส่วนรองรับ, การตรวจจับการจับตัวเป็นก้อนโดยการตรวจวัดถูกปิดใช้งาน, ชั้นเปลือกด้านบนเป็น 0, ความหนาแน่นของไส้ในแบบกระจายเป็น 0 และประเภทไทม์แลปส์เป็นแบบดั้งเดิม" msgid " But machines with I3 structure will not generate timelapse videos." msgstr "แต่เครื่องที่มีโครงสร้าง I3 จะไม่สร้างวิดีโอแบบไทม์แลปส์" @@ -4658,58 +4318,30 @@ msgstr "ไม่รองรับไทม์แลปส์ในขณะท msgid "Timelapse is not supported while the storage is readonly." msgstr "ไม่รองรับไทม์แลปส์ในขณะที่พื้นที่เก็บข้อมูลเป็นแบบอ่านอย่างเดียว" -msgid "" -"To ensure your safety, certain processing tasks (such as laser) can only be " -"resumed on printer." -msgstr "" -"เพื่อความปลอดภัยของคุณ งานการประมวลผลบางอย่าง (เช่น เลเซอร์) " -"สามารถดำเนินการต่อได้บนเครื่องพิมพ์เท่านั้น" +msgid "To ensure your safety, certain processing tasks (such as laser) can only be resumed on printer." +msgstr "เพื่อความปลอดภัยของคุณ งานการประมวลผลบางอย่าง (เช่น เลเซอร์) สามารถดำเนินการต่อได้บนเครื่องพิมพ์เท่านั้น" #, c-format, boost-format -msgid "" -"The chamber temperature is too high, which may cause the filament to soften. " -"Please wait until the chamber temperature drops below %d℃. You may open the " -"front door or enable fans to cool down." -msgstr "" -"อุณหภูมิในห้องเพาะเลี้ยงสูงเกินไป ซึ่งอาจทำให้เส้นพลาสติกนิ่มลง " -"โปรดรอจนกว่าอุณหภูมิห้องจะลดลงต่ำกว่า %d℃ คุณสามารถเปิดประตูหน้าหรือเปิดพัดลมให้เย็นลงได้" +msgid "The chamber temperature is too high, which may cause the filament to soften. Please wait until the chamber temperature drops below %d℃. You may open the front door or enable fans to cool down." +msgstr "อุณหภูมิในห้องเพาะเลี้ยงสูงเกินไป ซึ่งอาจทำให้เส้นพลาสติกนิ่มลง โปรดรอจนกว่าอุณหภูมิห้องจะลดลงต่ำกว่า %d℃ คุณสามารถเปิดประตูหน้าหรือเปิดพัดลมให้เย็นลงได้" #, c-format, boost-format -msgid "" -"AMS temperature is too high, which may cause the filament to soften. Please " -"wait until the AMS temperature drops below %d℃." -msgstr "" -"อุณหภูมิ AMS สูงเกินไป ซึ่งอาจทำให้เส้นพลาสติกอ่อนตัวลง โปรดรอจนกว่าอุณหภูมิ AMS จะลดลงต่ำกว่า " -"%d℃" +msgid "AMS temperature is too high, which may cause the filament to soften. Please wait until the AMS temperature drops below %d℃." +msgstr "อุณหภูมิ AMS สูงเกินไป ซึ่งอาจทำให้เส้นพลาสติกอ่อนตัวลง โปรดรอจนกว่าอุณหภูมิ AMS จะลดลงต่ำกว่า %d℃" -msgid "" -"The current chamber temperature or the target chamber temperature exceeds " -"45℃. In order to avoid extruder clogging, low temperature filament(PLA/PETG/" -"TPU) is not allowed to be loaded." -msgstr "" -"อุณหภูมิห้องปัจจุบันหรืออุณหภูมิห้องเป้าหมายเกิน 45°C เพื่อหลีกเลี่ยงการอุดตันของชุดดันเส้น " -"ไม่อนุญาตให้โหลดเส้นพลาสติกที่มีอุณหภูมิต่ำ (PLA/PETG/TPU)" +msgid "The current chamber temperature or the target chamber temperature exceeds 45℃. In order to avoid extruder clogging, low temperature filament(PLA/PETG/TPU) is not allowed to be loaded." +msgstr "อุณหภูมิห้องปัจจุบันหรืออุณหภูมิห้องเป้าหมายเกิน 45°C เพื่อหลีกเลี่ยงการอุดตันของชุดดันเส้น ไม่อนุญาตให้โหลดเส้นพลาสติกที่มีอุณหภูมิต่ำ (PLA/PETG/TPU)" -msgid "" -"Low temperature filament(PLA/PETG/TPU) is loaded in the extruder. In order " -"to avoid extruder clogging, it is not allowed to set the chamber temperature." -msgstr "" -"โหลดเส้นพลาสติกอุณหภูมิต่ำ (PLA/PETG/TPU) ลงในชุดดันเส้น เพื่อหลีกเลี่ยงการอุดตันของชุดดันเส้น " -"ไม่อนุญาตให้ตั้งอุณหภูมิห้องเพาะเลี้ยง" +msgid "Low temperature filament(PLA/PETG/TPU) is loaded in the extruder. In order to avoid extruder clogging, it is not allowed to set the chamber temperature." +msgstr "โหลดเส้นพลาสติกอุณหภูมิต่ำ (PLA/PETG/TPU) ลงในชุดดันเส้น เพื่อหลีกเลี่ยงการอุดตันของชุดดันเส้น ไม่อนุญาตให้ตั้งอุณหภูมิห้องเพาะเลี้ยง" -msgid "" -"When you set the chamber temperature below 40℃, the chamber temperature " -"control will not be activated, and the target chamber temperature will " -"automatically be set to 0℃." -msgstr "" -"เมื่อคุณตั้งอุณหภูมิห้องเพาะเลี้ยงให้ต่ำกว่า 40°C การควบคุมอุณหภูมิห้องเพาะเลี้ยงจะไม่ทำงาน " -"และอุณหภูมิห้องเพาะเลี้ยงเป้าหมายจะถูกตั้งค่าเป็น 0°C โดยอัตโนมัติ" +msgid "When you set the chamber temperature below 40℃, the chamber temperature control will not be activated, and the target chamber temperature will automatically be set to 0℃." +msgstr "เมื่อคุณตั้งอุณหภูมิห้องเพาะเลี้ยงให้ต่ำกว่า 40°C การควบคุมอุณหภูมิห้องเพาะเลี้ยงจะไม่ทำงาน และอุณหภูมิห้องเพาะเลี้ยงเป้าหมายจะถูกตั้งค่าเป็น 0°C โดยอัตโนมัติ" msgid "Failed to start print job" msgstr "ไม่สามารถเริ่มงานพิมพ์ได้" -msgid "" -"This calibration does not support the currently selected nozzle diameter" +msgid "This calibration does not support the currently selected nozzle diameter" msgstr "การสอบเทียบนี้ไม่รองรับเส้นผ่านศูนย์กลางหัวฉีดที่เลือกในปัจจุบัน" msgid "Current flowrate cali param is invalid" @@ -4893,9 +4525,7 @@ msgstr "" "ไม่ สำหรับ %s %s" #, boost-format -msgid "" -"Invalid input format. Expected vector of dimensions in the following format: " -"\"%1%\"" +msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" msgstr "รูปแบบการป้อนข้อมูลไม่ถูกต้อง เวกเตอร์ที่คาดหวังของมิติข้อมูลในรูปแบบต่อไปนี้: \"%1%\"" msgid "Input value is out of range" @@ -4907,12 +4537,8 @@ msgstr "ส่วนขยายบางส่วนในอินพุตไ msgid "This parameter expects a valid template." msgstr "พารามิเตอร์นี้ต้องการเทมเพลตที่ถูกต้อง" -msgid "" -"Invalid pattern. Use N, N#K, or a comma-separated list with optional #K per " -"entry. Examples: 5, 5#2, 1,7,9, 5,9#2,18." -msgstr "" -"รูปแบบไม่ถูกต้อง ใช้ N, N#K หรือรายการที่คั่นด้วยเครื่องหมายจุลภาคโดยมีตัวเลือก #K " -"ให้เลือกต่อรายการ ตัวอย่าง: 5, 5#2, 1,7,9, 5,9#2,18" +msgid "Invalid pattern. Use N, N#K, or a comma-separated list with optional #K per entry. Examples: 5, 5#2, 1,7,9, 5,9#2,18." +msgstr "รูปแบบไม่ถูกต้อง ใช้ N, N#K หรือรายการที่คั่นด้วยเครื่องหมายจุลภาคโดยมีตัวเลือก #K ให้เลือกต่อรายการ ตัวอย่าง: 5, 5#2, 1,7,9, 5,9#2,18" #, boost-format msgid "Invalid format. Expected vector format: \"%1%\"" @@ -5122,12 +4748,8 @@ msgstr "เวลาทั้งหมด" msgid "Total cost" msgstr "ค่าใช้จ่ายทั้งหมด" -msgid "" -"Automatically re-slice according to the optimal filament grouping, and the " -"grouping results will be displayed after slicing." -msgstr "" -"สไลซ์ใหม่โดยอัตโนมัติตามการจัดกลุ่มเส้นพลาสติกที่เหมาะสมที่สุด " -"และผลลัพธ์การจัดกลุ่มจะแสดงหลังจากการสไลซ์" +msgid "Automatically re-slice according to the optimal filament grouping, and the grouping results will be displayed after slicing." +msgstr "สไลซ์ใหม่โดยอัตโนมัติตามการจัดกลุ่มเส้นพลาสติกที่เหมาะสมที่สุด และผลลัพธ์การจัดกลุ่มจะแสดงหลังจากการสไลซ์" msgid "Filament Grouping" msgstr "การจัดกลุ่มเส้นพลาสติก" @@ -5152,41 +4774,27 @@ msgstr "การจัดกลุ่มผลลัพธ์การแบ่ #, boost-format msgid "Increase %1%g filament and %2% changes compared to optimal grouping." -msgstr "" -"เพิ่ม %1%g เส้นพลาสติกและ %2% การเปลี่ยนแปลงเมื่อเปรียบเทียบกับการจัดกลุ่มที่เหมาะสมที่สุด" +msgstr "เพิ่ม %1%g เส้นพลาสติกและ %2% การเปลี่ยนแปลงเมื่อเปรียบเทียบกับการจัดกลุ่มที่เหมาะสมที่สุด" #, boost-format -msgid "" -"Increase %1%g filament and save %2% changes compared to optimal grouping." -msgstr "" -"เพิ่มเส้นพลาสติก %1%g และบันทึกการเปลี่ยนแปลง %2% เมื่อเปรียบเทียบกับการจัดกลุ่มที่เหมาะสมที่สุด" +msgid "Increase %1%g filament and save %2% changes compared to optimal grouping." +msgstr "เพิ่มเส้นพลาสติก %1%g และบันทึกการเปลี่ยนแปลง %2% เมื่อเปรียบเทียบกับการจัดกลุ่มที่เหมาะสมที่สุด" #, boost-format -msgid "" -"Save %1%g filament and increase %2% changes compared to optimal grouping." -msgstr "" -"ประหยัดเส้นพลาสติก %1%g และเพิ่มการเปลี่ยนแปลง %2% เมื่อเปรียบเทียบกับการจัดกลุ่มที่เหมาะสมที่สุด" +msgid "Save %1%g filament and increase %2% changes compared to optimal grouping." +msgstr "ประหยัดเส้นพลาสติก %1%g และเพิ่มการเปลี่ยนแปลง %2% เมื่อเปรียบเทียบกับการจัดกลุ่มที่เหมาะสมที่สุด" #, boost-format -msgid "" -"Save %1%g filament and %2% changes compared to a printer with one nozzle." -msgstr "" -"บันทึก %1%g เส้นพลาสติกและ %2% การเปลี่ยนแปลง เมื่อเปรียบเทียบกับเครื่องพิมพ์ที่มีหัวฉีดเดียว" +msgid "Save %1%g filament and %2% changes compared to a printer with one nozzle." +msgstr "บันทึก %1%g เส้นพลาสติกและ %2% การเปลี่ยนแปลง เมื่อเปรียบเทียบกับเครื่องพิมพ์ที่มีหัวฉีดเดียว" #, boost-format -msgid "" -"Save %1%g filament and increase %2% changes compared to a printer with one " -"nozzle." -msgstr "" -"ประหยัดเส้นพลาสติก %1%g และเพิ่มการเปลี่ยนแปลง %2% เมื่อเทียบกับเครื่องพิมพ์ที่มีหัวฉีดเพียงอันเดียว" +msgid "Save %1%g filament and increase %2% changes compared to a printer with one nozzle." +msgstr "ประหยัดเส้นพลาสติก %1%g และเพิ่มการเปลี่ยนแปลง %2% เมื่อเทียบกับเครื่องพิมพ์ที่มีหัวฉีดเพียงอันเดียว" #, boost-format -msgid "" -"Increase %1%g filament and save %2% changes compared to a printer with one " -"nozzle." -msgstr "" -"เพิ่มเส้นพลาสติก %1%g และประหยัดการเปลี่ยนแปลง %2% " -"เมื่อเปรียบเทียบกับเครื่องพิมพ์ที่มีหัวฉีดเพียงอันเดียว" +msgid "Increase %1%g filament and save %2% changes compared to a printer with one nozzle." +msgstr "เพิ่มเส้นพลาสติก %1%g และประหยัดการเปลี่ยนแปลง %2% เมื่อเปรียบเทียบกับเครื่องพิมพ์ที่มีหัวฉีดเพียงอันเดียว" msgid "Set to Optimal" msgstr "ตั้งค่าให้เหมาะสมที่สุด" @@ -5294,22 +4902,18 @@ msgid "Show normal mode" msgstr "แสดงโหมดปกติ" msgid "" -"An object is placed in the left/right nozzle-only area or exceeds the " -"printable height of the left nozzle.\n" -"Please ensure the filaments used by this object are not arranged to other " -"nozzles." +"An object is placed in the left/right nozzle-only area or exceeds the printable height of the left nozzle.\n" +"Please ensure the filaments used by this object are not arranged to other nozzles." msgstr "" "วัตถุถูกวางในพื้นที่เฉพาะหัวฉีดด้านซ้าย/ขวา หรือเกินความสูงที่สามารถพิมพ์ได้ของหัวฉีดด้านซ้าย\n" "โปรดตรวจสอบให้แน่ใจว่าเส้นพลาสติกที่ใช้โดยวัตถุนี้ไม่ได้ถูกจัดเรียงเข้ากับหัวฉีดอื่น" msgid "" "An object is laid over the boundary of plate or exceeds the height limit.\n" -"Please solve the problem by moving it totally on or off the plate, and " -"confirming that the height is within the build volume." +"Please solve the problem by moving it totally on or off the plate, and confirming that the height is within the build volume." msgstr "" "วัตถุวางอยู่เหนือขอบเขตของแผ่นหรือสูงเกินขีดจำกัดความสูง\n" -"โปรดแก้ไขปัญหาด้วยการเลื่อนเข้าหรือออกจากเพลตโดยสิ้นเชิง " -"และยืนยันว่าความสูงอยู่ภายในปริมาตรงานประกอบ" +"โปรดแก้ไขปัญหาด้วยการเลื่อนเข้าหรือออกจากเพลตโดยสิ้นเชิง และยืนยันว่าความสูงอยู่ภายในปริมาตรงานประกอบ" msgid "Variable layer height" msgstr "ความสูงของชั้นตัวแปร" @@ -5356,17 +4960,11 @@ msgstr "ปุ่มตัวเลข" msgid "Number keys can quickly change the color of objects" msgstr "ปุ่มตัวเลขสามารถเปลี่ยนสีของวัตถุได้อย่างรวดเร็ว" -msgid "" -"Following objects are laid over the boundary of plate or exceeds the height " -"limit:\n" +msgid "Following objects are laid over the boundary of plate or exceeds the height limit:\n" msgstr "วัตถุต่อไปนี้วางอยู่เหนือขอบเขตของแผ่นหรือสูงเกินขีดจำกัดความสูง:\n" -msgid "" -"Please solve the problem by moving it totally on or off the plate, and " -"confirming that the height is within the build volume.\n" -msgstr "" -"โปรดแก้ไขปัญหาด้วยการเลื่อนเข้าหรือออกจากเพลตโดยสิ้นเชิง " -"และยืนยันว่าความสูงอยู่ภายในปริมาตรงานประกอบ\n" +msgid "Please solve the problem by moving it totally on or off the plate, and confirming that the height is within the build volume.\n" +msgstr "โปรดแก้ไขปัญหาด้วยการเลื่อนเข้าหรือออกจากเพลตโดยสิ้นเชิง และยืนยันว่าความสูงอยู่ภายในปริมาตรงานประกอบ\n" msgid "left nozzle" msgstr "หัวฉีดซ้าย" @@ -5382,9 +4980,7 @@ msgstr "ตำแหน่งหรือขนาดของบางรุ่ msgid "The position or size of the model %s exceeds the %s's printable range." msgstr "ตำแหน่งหรือขนาดของโมเดล %s เกินช่วงที่สามารถพิมพ์ได้ของ %s" -msgid "" -" Please check and adjust the part's position or size to fit the printable " -"range:\n" +msgid " Please check and adjust the part's position or size to fit the printable range:\n" msgstr "โปรดตรวจสอบและปรับตำแหน่งหรือขนาดของชิ้นส่วนให้พอดีกับช่วงที่สามารถพิมพ์ได้:\n" #, boost-format @@ -5554,12 +5150,8 @@ msgid "Size:" msgstr "ขนาด:" #, c-format, boost-format -msgid "" -"Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please " -"separate the conflicted objects farther (%s <-> %s)." -msgstr "" -"พบความขัดแย้งของเส้นทางรหัส G ที่เลเยอร์ %d, Z = %.2lfmm " -"โปรดแยกวัตถุที่ขัดแย้งกันให้ไกลออกไป (%s <-> %s)" +msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." +msgstr "พบความขัดแย้งของเส้นทางรหัส G ที่เลเยอร์ %d, Z = %.2lfmm โปรดแยกวัตถุที่ขัดแย้งกันให้ไกลออกไป (%s <-> %s)" msgid "An object is laid over the plate boundaries." msgstr "มีวัตถุวางอยู่เหนือขอบเขตของแผ่น" @@ -5578,32 +5170,20 @@ msgid "Tool %d" msgstr "เครื่องมือ %d" #, c-format, boost-format -msgid "" -"Filament %s is placed in the %s, but the generated G-code path exceeds the " -"printable range of the %s." -msgstr "" -"เส้นพลาสติก %s ถูกวางไว้ใน %s แต่เส้นทาง G-code ที่สร้างขึ้นเกินช่วงที่สามารถพิมพ์ได้ของ %s" +msgid "Filament %s is placed in the %s, but the generated G-code path exceeds the printable range of the %s." +msgstr "เส้นพลาสติก %s ถูกวางไว้ใน %s แต่เส้นทาง G-code ที่สร้างขึ้นเกินช่วงที่สามารถพิมพ์ได้ของ %s" #, c-format, boost-format -msgid "" -"Filaments %s are placed in the %s, but the generated G-code path exceeds the " -"printable range of the %s." -msgstr "" -"เส้นพลาสติก %s ถูกวางไว้ใน %s แต่เส้นทาง G-code ที่สร้างขึ้นเกินช่วงที่สามารถพิมพ์ได้ของ %s" +msgid "Filaments %s are placed in the %s, but the generated G-code path exceeds the printable range of the %s." +msgstr "เส้นพลาสติก %s ถูกวางไว้ใน %s แต่เส้นทาง G-code ที่สร้างขึ้นเกินช่วงที่สามารถพิมพ์ได้ของ %s" #, c-format, boost-format -msgid "" -"Filament %s is placed in the %s, but the generated G-code path exceeds the " -"printable height of the %s." -msgstr "" -"เส้นพลาสติก %s ถูกวางไว้ใน %s แต่เส้นทาง G-code ที่สร้างขึ้นเกินความสูงที่สามารถพิมพ์ได้ของ %s" +msgid "Filament %s is placed in the %s, but the generated G-code path exceeds the printable height of the %s." +msgstr "เส้นพลาสติก %s ถูกวางไว้ใน %s แต่เส้นทาง G-code ที่สร้างขึ้นเกินความสูงที่สามารถพิมพ์ได้ของ %s" #, c-format, boost-format -msgid "" -"Filaments %s are placed in the %s, but the generated G-code path exceeds the " -"printable height of the %s." -msgstr "" -"เส้นพลาสติก %s ถูกวางไว้ใน %s แต่เส้นทาง G-code ที่สร้างขึ้นเกินความสูงที่สามารถพิมพ์ได้ของ %s" +msgid "Filaments %s are placed in the %s, but the generated G-code path exceeds the printable height of the %s." +msgstr "เส้นพลาสติก %s ถูกวางไว้ใน %s แต่เส้นทาง G-code ที่สร้างขึ้นเกินความสูงที่สามารถพิมพ์ได้ของ %s" msgid "Open wiki for more information." msgstr "เปิดวิกิเพื่อดูข้อมูลเพิ่มเติม" @@ -5615,22 +5195,14 @@ msgstr "มองเห็นได้เฉพาะวัตถุที่ก msgid "Filaments %s cannot be printed directly on the surface of this plate." msgstr "ไม่สามารถพิมพ์เส้นพลาสติก %s ลงบนพื้นผิวของเพลตนี้ได้โดยตรง" -msgid "" -"PLA and PETG filaments detected in the mixture. Adjust parameters according " -"to the Wiki to ensure print quality." -msgstr "" -"ตรวจพบเส้นพลาสติก PLA และ PETG ในส่วนผสม ปรับพารามิเตอร์ตาม Wiki " -"เพื่อรับรองคุณภาพการพิมพ์" +msgid "PLA and PETG filaments detected in the mixture. Adjust parameters according to the Wiki to ensure print quality." +msgstr "ตรวจพบเส้นพลาสติก PLA และ PETG ในส่วนผสม ปรับพารามิเตอร์ตาม Wiki เพื่อรับรองคุณภาพการพิมพ์" msgid "The prime tower extends beyond the plate boundary." msgstr "หอคอยหลักขยายออกไปเกินขอบเขตแผ่นเปลือกโลก" -msgid "" -"Partial flushing volume set to 0. Multi-color printing may cause color " -"mixing in models. Please readjust flushing settings." -msgstr "" -"ตั้งค่าปริมาณการไล่เส้นบางส่วนเป็น 0 การพิมพ์หลายสีอาจทำให้เกิดการผสมสีในรุ่นต่างๆ " -"โปรดปรับการตั้งค่าการไล่เส้นใหม่" +msgid "Partial flushing volume set to 0. Multi-color printing may cause color mixing in models. Please readjust flushing settings." +msgstr "ตั้งค่าปริมาณการไล่เส้นบางส่วนเป็น 0 การพิมพ์หลายสีอาจทำให้เกิดการผสมสีในรุ่นต่างๆ โปรดปรับการตั้งค่าการไล่เส้นใหม่" msgid "Click Wiki for help." msgstr "คลิก Wiki เพื่อขอความช่วยเหลือ" @@ -5660,8 +5232,7 @@ msgid "Calibration program" msgstr "โปรแกรมสอบเทียบ" msgid "" -"The calibration program detects the status of your device automatically to " -"minimize deviation.\n" +"The calibration program detects the status of your device automatically to minimize deviation.\n" "It keeps the device performing optimally." msgstr "" "โปรแกรมปรับเทียบจะตรวจจับสถานะของอุปกรณ์ของคุณโดยอัตโนมัติเพื่อลดความเบี่ยงเบน\n" @@ -6019,11 +5590,8 @@ msgstr "ใช้มุมมองมุมฉาก" msgid "Auto Perspective" msgstr "มุมมองอัตโนมัติ" -msgid "" -"Automatically switch between orthographic and perspective when changing from " -"top/bottom/side views." -msgstr "" -"สลับระหว่างออร์โธกราฟิกและเปอร์สเปคทีฟโดยอัตโนมัติเมื่อเปลี่ยนจากมุมมองด้านบน/ล่าง/ด้านข้าง" +msgid "Automatically switch between orthographic and perspective when changing from top/bottom/side views." +msgstr "สลับระหว่างออร์โธกราฟิกและเปอร์สเปคทีฟโดยอัตโนมัติเมื่อเปลี่ยนจากมุมมองด้านบน/ล่าง/ด้านข้าง" msgid "Show &G-code Window" msgstr "แสดงหน้าต่าง &G-code" @@ -6194,15 +5762,12 @@ msgstr "เลือกโปรไฟล์ที่จะโหลด:" #, c-format, boost-format msgid "There is %d config imported. (Only non-system and compatible configs)" -msgid_plural "" -"There are %d configs imported. (Only non-system and compatible configs)" -msgstr[0] "" -"There is %d config imported. (Only non-system and compatible configs)" +msgid_plural "There are %d configs imported. (Only non-system and compatible configs)" +msgstr[0] "There is %d config imported. (Only non-system and compatible configs)" msgid "" "\n" -"Hint: Make sure you have added the corresponding printer before importing " -"the configs." +"Hint: Make sure you have added the corresponding printer before importing the configs." msgstr "" "\n" "คำแนะนำ: ตรวจสอบให้แน่ใจว่าคุณได้เพิ่มเครื่องพิมพ์ที่เกี่ยวข้องก่อนที่จะนำเข้าการกำหนดค่า" @@ -6241,18 +5806,13 @@ msgstr "เครื่องเล่นทำงานผิดปกติ msgid "The player is not loaded; please click the \"play\" button to retry." msgstr "โปรแกรมเล่นไม่ได้โหลด กรุณาคลิกปุ่ม \"เล่น\" เพื่อลองอีกครั้ง" -msgid "" -"The player is not loaded because the GStreamer GTK video sink is missing or " -"failed to initialize." -msgstr "" -"ไม่ได้โหลดโปรแกรมเล่นเนื่องจาก GStreamer GTK video sink หายไปหรือไม่สามารถเริ่มต้นได้" +msgid "The player is not loaded because the GStreamer GTK video sink is missing or failed to initialize." +msgstr "ไม่ได้โหลดโปรแกรมเล่นเนื่องจาก GStreamer GTK video sink หายไปหรือไม่สามารถเริ่มต้นได้" msgid "Please confirm if the printer is connected." msgstr "โปรดยืนยันว่าเครื่องพิมพ์เชื่อมต่ออยู่หรือไม่" -msgid "" -"The printer is currently busy downloading. Please try again after it " -"finishes." +msgid "The printer is currently busy downloading. Please try again after it finishes." msgstr "เครื่องพิมพ์กำลังยุ่งอยู่กับการดาวน์โหลด โปรดลองอีกครั้งหลังจากเสร็จสิ้น" msgid "Printer camera is malfunctioning." @@ -6261,8 +5821,7 @@ msgstr "กล้องเครื่องพิมพ์ทำงานผิ msgid "A problem occurred. Please update the printer firmware and try again." msgstr "เกิดปัญหาขึ้น โปรดอัปเดตเฟิร์มแวร์เครื่องพิมพ์แล้วลองอีกครั้ง" -msgid "" -"LAN Only Liveview is off. Please turn on the liveview on printer screen." +msgid "LAN Only Liveview is off. Please turn on the liveview on printer screen." msgstr "LAN Only Liveview ปิดอยู่ กรุณาเปิด liveview บนหน้าจอเครื่องพิมพ์" msgid "Please enter the IP of the printer to connect." @@ -6274,11 +5833,8 @@ msgstr "กำลังเริ่มต้น..." msgid "Connection Failed. Please check the network and try again" msgstr "การเชื่อมต่อล้มเหลว โปรดตรวจสอบเครือข่ายแล้วลองอีกครั้ง" -msgid "" -"Please check the network and try again. You can restart or update the " -"printer if the issue persists." -msgstr "" -"โปรดตรวจสอบเครือข่ายแล้วลองอีกครั้ง คุณสามารถรีสตาร์ทหรืออัปเดตเครื่องพิมพ์ได้หากปัญหายังคงมีอยู่" +msgid "Please check the network and try again. You can restart or update the printer if the issue persists." +msgstr "โปรดตรวจสอบเครือข่ายแล้วลองอีกครั้ง คุณสามารถรีสตาร์ทหรืออัปเดตเครื่องพิมพ์ได้หากปัญหายังคงมีอยู่" msgid "The printer has been logged out and cannot connect." msgstr "เครื่องพิมพ์ออกจากระบบและไม่สามารถเชื่อมต่อได้" @@ -6387,11 +5943,8 @@ msgstr "ไม่มีไฟล์" msgid "Load failed" msgstr "โหลดล้มเหลว" -msgid "" -"Browsing file in storage is not supported in current firmware. Please update " -"the printer firmware." -msgstr "" -"การเรียกดูไฟล์ในที่จัดเก็บข้อมูลไม่ได้รับส่วนรองรับในเฟิร์มแวร์ปัจจุบัน โปรดอัปเดตเฟิร์มแวร์เครื่องพิมพ์" +msgid "Browsing file in storage is not supported in current firmware. Please update the printer firmware." +msgstr "การเรียกดูไฟล์ในที่จัดเก็บข้อมูลไม่ได้รับส่วนรองรับในเฟิร์มแวร์ปัจจุบัน โปรดอัปเดตเฟิร์มแวร์เครื่องพิมพ์" msgid "LAN Connection Failed (Failed to view sdcard)" msgstr "การเชื่อมต่อ LAN ล้มเหลว (ไม่สามารถดู sdcard)" @@ -6401,10 +5954,8 @@ msgstr "ไม่รองรับการเรียกดูไฟล์ใ #, c-format, boost-format msgid "You are going to delete %u file from printer. Are you sure to continue?" -msgid_plural "" -"You are going to delete %u files from printer. Are you sure to continue?" -msgstr[0] "" -"You are going to delete %u file from printer. Are you sure to continue?" +msgid_plural "You are going to delete %u files from printer. Are you sure to continue?" +msgstr[0] "You are going to delete %u file from printer. Are you sure to continue?" msgid "Delete files" msgstr "ไฟล์ลบ" @@ -6425,12 +5976,8 @@ msgstr "ไม่สามารถดึงข้อมูลรุ่นจา msgid "Failed to parse model information." msgstr "ไม่สามารถแยกวิเคราะห์ข้อมูลโมเดล" -msgid "" -"The .gcode.3mf file contains no G-code data. Please slice it with Orca " -"Slicer and export a new .gcode.3mf file." -msgstr "" -"ไฟล์ .gcode.3mf ไม่มีข้อมูล G-code โปรดแบ่งส่วนด้วย Orca Slicer และส่งออกไฟล์ ." -"gcode.3mf ใหม่" +msgid "The .gcode.3mf file contains no G-code data. Please slice it with Orca Slicer and export a new .gcode.3mf file." +msgstr "ไฟล์ .gcode.3mf ไม่มีข้อมูล G-code โปรดแบ่งส่วนด้วย Orca Slicer และส่งออกไฟล์ .gcode.3mf ใหม่" #, c-format, boost-format msgid "File '%s' was lost! Please download it again." @@ -6463,11 +6010,8 @@ msgstr "กำลังดาวน์โหลด %d%%..." msgid "Air Condition" msgstr "ปรับอากาศ" -msgid "" -"Reconnecting the printer, the operation cannot be completed immediately, " -"please try again later." -msgstr "" -"กำลังเชื่อมต่อเครื่องพิมพ์ใหม่ ไม่สามารถดำเนินการให้เสร็จสิ้นได้ในทันที โปรดลองอีกครั้งในภายหลัง" +msgid "Reconnecting the printer, the operation cannot be completed immediately, please try again later." +msgstr "กำลังเชื่อมต่อเครื่องพิมพ์ใหม่ ไม่สามารถดำเนินการให้เสร็จสิ้นได้ในทันที โปรดลองอีกครั้งในภายหลัง" msgid "Timeout, please try again." msgstr "หมดเวลา โปรดลองอีกครั้ง" @@ -6488,9 +6032,7 @@ msgstr "" "โปรดตรวจสอบว่ามีการใส่ที่เก็บข้อมูลเข้าไปในเครื่องพิมพ์หรือไม่\n" "หากยังไม่สามารถอ่านได้ คุณสามารถลองฟอร์แมตที่จัดเก็บข้อมูลได้" -msgid "" -"The firmware version of the printer is too low. Please update the firmware " -"and try again." +msgid "The firmware version of the printer is too low. Please update the firmware and try again." msgstr "เวอร์ชันเฟิร์มแวร์ของเครื่องพิมพ์ต่ำเกินไป โปรดอัปเดตเฟิร์มแวร์แล้วลองอีกครั้ง" msgid "The file already exists, do you want to replace it?" @@ -6661,9 +6203,7 @@ msgstr "" msgid "How do you like this printing file?" msgstr "คุณชอบไฟล์การพิมพ์นี้อย่างไร?" -msgid "" -"(The model has already been rated. Your rating will overwrite the previous " -"rating.)" +msgid "(The model has already been rated. Your rating will overwrite the previous rating.)" msgstr "(แบบจำลองได้รับการจัดอันดับแล้ว การให้คะแนนของคุณจะเขียนทับการให้คะแนนครั้งก่อน)" msgid "Rate" @@ -6720,9 +6260,7 @@ msgstr "คุณแน่ใจหรือไม่ว่าต้องกา msgid "The printer is busy with another print job." msgstr "เครื่องพิมพ์กำลังยุ่งอยู่กับงานพิมพ์อื่น" -msgid "" -"When printing is paused, filament loading and unloading are only supported " -"for external slots." +msgid "When printing is paused, filament loading and unloading are only supported for external slots." msgstr "เมื่อหยุดการพิมพ์ชั่วคราว การโหลดฟิลาเมนท์และการขนถ่ายจะรองรับเฉพาะช่องภายนอกเท่านั้น" msgid "Current extruder is busy changing filament." @@ -6755,29 +6293,20 @@ msgstr "เลเยอร์: %s" msgid "Layer: %d/%d" msgstr "เลเยอร์: %d/%d" -msgid "" -"Please heat the nozzle to above 170℃ before loading or unloading filament." +msgid "Please heat the nozzle to above 170℃ before loading or unloading filament." msgstr "กรุณาตั้งอุณหภูมิหัวฉีดให้สูงกว่า 170°C ก่อนที่จะบรรจุหรือขนถ่ายเส้นพลาสติก" msgid "Chamber temperature cannot be changed in cooling mode while printing." msgstr "อุณหภูมิห้องเพาะเลี้ยงไม่สามารถเปลี่ยนแปลงได้ในโหมดทำความเย็นขณะพิมพ์" -msgid "" -"If the chamber temperature exceeds 40℃, the system will automatically switch " -"to heating mode. Please confirm whether to switch." -msgstr "" -"หากอุณหภูมิห้องเพาะเกิน 40°C ระบบจะเปลี่ยนเป็นโหมดทำความร้อนโดยอัตโนมัติ " -"โปรดยืนยันว่าจะสลับหรือไม่" +msgid "If the chamber temperature exceeds 40℃, the system will automatically switch to heating mode. Please confirm whether to switch." +msgstr "หากอุณหภูมิห้องเพาะเกิน 40°C ระบบจะเปลี่ยนเป็นโหมดทำความร้อนโดยอัตโนมัติ โปรดยืนยันว่าจะสลับหรือไม่" msgid "Please select an AMS slot before calibration" msgstr "โปรดเลือกช่อง AMS ก่อนการสอบเทียบ" -msgid "" -"Cannot read filament info: the filament is loaded to the tool head. Please " -"unload the filament and try again." -msgstr "" -"ไม่สามารถอ่านข้อมูลเส้นพลาสติกได้: โหลดเส้นพลาสติกไปที่หัวเครื่องมือแล้ว " -"โปรดยกเลิกการโหลดเส้นพลาสติกแล้วลองอีกครั้ง" +msgid "Cannot read filament info: the filament is loaded to the tool head. Please unload the filament and try again." +msgstr "ไม่สามารถอ่านข้อมูลเส้นพลาสติกได้: โหลดเส้นพลาสติกไปที่หัวเครื่องมือแล้ว โปรดยกเลิกการโหลดเส้นพลาสติกแล้วลองอีกครั้ง" msgid "This only takes effect during printing" msgstr "ซึ่งจะมีผลเฉพาะระหว่างการพิมพ์เท่านั้น" @@ -6794,12 +6323,8 @@ msgstr "สปอร์ต" msgid "Ludicrous" msgstr "เร็วสุด" -msgid "" -"Turning off the lights during the task will cause the failure of AI " -"monitoring, like spaghetti detection. Please choose carefully." -msgstr "" -"การปิดไฟระหว่างทำงานจะทำให้การตรวจสอบ AI ล้มเหลว เช่น การตรวจจับสปาเก็ตตี้ " -"โปรดเลือกอย่างระมัดระวัง" +msgid "Turning off the lights during the task will cause the failure of AI monitoring, like spaghetti detection. Please choose carefully." +msgstr "การปิดไฟระหว่างทำงานจะทำให้การตรวจสอบ AI ล้มเหลว เช่น การตรวจจับสปาเก็ตตี้ โปรดเลือกอย่างระมัดระวัง" msgid "Keep it On" msgstr "เก็บไว้บน" @@ -6853,11 +6378,9 @@ msgid " cannot be opened\n" msgstr "ไม่สามารถเปิดได้\n" msgid "" -"The following issues occurred during the process of uploading images. Do you " -"want to ignore them?\n" +"The following issues occurred during the process of uploading images. Do you want to ignore them?\n" "\n" -msgstr "" -"ปัญหาต่อไปนี้เกิดขึ้นระหว่างกระบวนการอัพโหลดรูปภาพ คุณต้องการที่จะเพิกเฉยต่อพวกเขาหรือไม่?\n" +msgstr "ปัญหาต่อไปนี้เกิดขึ้นระหว่างกระบวนการอัพโหลดรูปภาพ คุณต้องการที่จะเพิกเฉยต่อพวกเขาหรือไม่?\n" msgid "info" msgstr "ข้อมูล" @@ -6891,11 +6414,8 @@ msgstr "" "\n" "คุณต้องการเปลี่ยนเส้นทางไปยังหน้าเว็บเพื่อให้คะแนนหรือไม่" -msgid "" -"Some of your images failed to upload. Would you like to redirect to the " -"webpage to give a rating?" -msgstr "" -"รูปภาพบางรูปของคุณล้มเหลวในการอัปโหลด คุณต้องการเปลี่ยนเส้นทางไปยังหน้าเว็บเพื่อให้คะแนนหรือไม่" +msgid "Some of your images failed to upload. Would you like to redirect to the webpage to give a rating?" +msgstr "รูปภาพบางรูปของคุณล้มเหลวในการอัปโหลด คุณต้องการเปลี่ยนเส้นทางไปยังหน้าเว็บเพื่อให้คะแนนหรือไม่" msgid "You can select up to 16 images." msgstr "คุณสามารถเลือกได้สูงสุด 16 ภาพ" @@ -6967,9 +6487,7 @@ msgstr "ข้าม" msgid "Newer 3MF version" msgstr "เวอร์ชัน 3MF ที่ใหม่กว่า" -msgid "" -"The 3MF file version is in Beta and it is newer than the current OrcaSlicer " -"version." +msgid "The 3MF file version is in Beta and it is newer than the current OrcaSlicer version." msgstr "เวอร์ชันไฟล์ 3MF อยู่ในรุ่นเบต้าและใหม่กว่าเวอร์ชัน OrcaSlicer ปัจจุบัน" msgid "If you would like to try Orca Slicer Beta, you may click to" @@ -6981,8 +6499,7 @@ msgstr "ดาวน์โหลดเวอร์ชันเบต้า" msgid "The 3MF file version is newer than the current OrcaSlicer version." msgstr "เวอร์ชันไฟล์ 3MF ใหม่กว่าเวอร์ชัน OrcaSlicer ปัจจุบัน" -msgid "" -"Updating your OrcaSlicer could enable all functionality in the 3MF file." +msgid "Updating your OrcaSlicer could enable all functionality in the 3MF file." msgstr "การอัปเดต OrcaSlicer ของคุณสามารถเปิดใช้งานฟังก์ชันทั้งหมดในไฟล์ 3MF" msgid "Current Version: " @@ -7001,17 +6518,11 @@ msgstr "ยังไม่ใช่ตอนนี้" msgid "Server Exception" msgstr "ข้อยกเว้นของเซิร์ฟเวอร์" -msgid "" -"The server is unable to respond. Please click the link below to check the " -"server status." +msgid "The server is unable to respond. Please click the link below to check the server status." msgstr "เซิร์ฟเวอร์ไม่สามารถตอบสนองได้ กรุณาคลิกลิงค์ด้านล่างเพื่อตรวจสอบสถานะเซิร์ฟเวอร์" -msgid "" -"If the server is in a fault state, you can temporarily use offline printing " -"or local network printing." -msgstr "" -"หากเซิร์ฟเวอร์อยู่ในสถานะผิดปกติ " -"คุณสามารถใช้การพิมพ์ออฟไลน์หรือการพิมพ์ผ่านเครือข่ายท้องถิ่นได้ชั่วคราว" +msgid "If the server is in a fault state, you can temporarily use offline printing or local network printing." +msgstr "หากเซิร์ฟเวอร์อยู่ในสถานะผิดปกติ คุณสามารถใช้การพิมพ์ออฟไลน์หรือการพิมพ์ผ่านเครือข่ายท้องถิ่นได้ชั่วคราว" msgid "How to use LAN only mode" msgstr "วิธีใช้โหมด LAN เท่านั้น" @@ -7144,9 +6655,7 @@ msgstr "ตัดขั้วต่อ" msgid "Layers" msgstr "เลเยอร์" -msgid "" -"The application cannot run normally because OpenGL version is lower than " -"3.2.\n" +msgid "The application cannot run normally because OpenGL version is lower than 3.2.\n" msgstr "แอปพลิเคชันไม่สามารถทำงานได้ตามปกติเนื่องจากเวอร์ชัน OpenGL ต่ำกว่า 3.2\n" msgid "Please upgrade your graphics card driver." @@ -7177,28 +6686,19 @@ msgstr "ล่าง" msgid "Enable detection of build plate position" msgstr "เปิดใช้งานการตรวจจับตำแหน่งฐานรองแท่นพิมพ์" -msgid "" -"The localization tag of the build plate will be detected, and printing will " -"be paused if the tag is not in predefined range." -msgstr "" -"ตรวจพบแท็กระบุตำแหน่งของเพลต และการพิมพ์จะหยุดชั่วคราวหากแท็กไม่อยู่ในช่วงที่กำหนดไว้ล่วงหน้า" +msgid "The localization tag of the build plate will be detected, and printing will be paused if the tag is not in predefined range." +msgstr "ตรวจพบแท็กระบุตำแหน่งของเพลต และการพิมพ์จะหยุดชั่วคราวหากแท็กไม่อยู่ในช่วงที่กำหนดไว้ล่วงหน้า" msgid "Build Plate Detection" msgstr "ตรวจจับฐานพิมพ์" -msgid "" -"Identifies the type and position of the build plate on the heatbed. Pausing " -"printing if a mismatch is detected." -msgstr "" -"ระบุประเภทและตำแหน่งของแผ่นฐานรองพิมพ์บนฐานพิมพ์ทำความร้อน " -"หยุดการพิมพ์ชั่วคราวหากตรวจพบความไม่ตรงกัน" +msgid "Identifies the type and position of the build plate on the heatbed. Pausing printing if a mismatch is detected." +msgstr "ระบุประเภทและตำแหน่งของแผ่นฐานรองพิมพ์บนฐานพิมพ์ทำความร้อน หยุดการพิมพ์ชั่วคราวหากตรวจพบความไม่ตรงกัน" msgid "AI Detections" msgstr "การตรวจจับเอไอ" -msgid "" -"Printer will send assistant message or pause printing if any of the " -"following problem is detected." +msgid "Printer will send assistant message or pause printing if any of the following problem is detected." msgstr "เครื่องพิมพ์จะส่งข้อความผู้ช่วยหรือหยุดการพิมพ์ชั่วคราว หากตรวจพบปัญหาใดๆ ต่อไปนี้" msgid "Enable AI monitoring of printing" @@ -7237,12 +6737,8 @@ msgstr "กู้คืนอัตโนมัติจากการสูญ msgid "Store Sent Files on External Storage" msgstr "จัดเก็บไฟล์ที่ส่งไปยังที่จัดเก็บข้อมูลภายนอก" -msgid "" -"Save the printing files initiated from Bambu Studio, Bambu Handy and " -"MakerWorld on External Storage" -msgstr "" -"บันทึกไฟล์การพิมพ์ที่เริ่มต้นจาก Bambu Studio, Bambu Handy และ MakerWorld " -"บนที่จัดเก็บข้อมูลภายนอก" +msgid "Save the printing files initiated from Bambu Studio, Bambu Handy and MakerWorld on External Storage" +msgstr "บันทึกไฟล์การพิมพ์ที่เริ่มต้นจาก Bambu Studio, Bambu Handy และ MakerWorld บนที่จัดเก็บข้อมูลภายนอก" msgid "Allow Prompt Sound" msgstr "อนุญาตเสียงพร้อมท์" @@ -7364,13 +6860,10 @@ msgid " nozzle" msgstr "หัวฉีด" #, boost-format -msgid "" -"It is not recommended to print the following filament(s) with %1%: %2%\n" +msgid "It is not recommended to print the following filament(s) with %1%: %2%\n" msgstr "ไม่แนะนำให้พิมพ์เส้นพลาสติกต่อไปนี้ด้วย %1%: %2%\n" -msgid "" -"It is not recommended to use the following nozzle and filament " -"combinations:\n" +msgid "It is not recommended to use the following nozzle and filament combinations:\n" msgstr "ไม่แนะนำให้ใช้หัวฉีดและเส้นพลาสติกผสมกันดังต่อไปนี้:\n" #, boost-format @@ -7414,15 +6907,8 @@ msgstr "AMS(1 ช่อง)" msgid "Not installed" msgstr "ไม่ได้ติดตั้ง" -msgid "" -"The software does not support using different diameter of nozzles for one " -"print. If the left and right nozzles are inconsistent, we can only proceed " -"with single-head printing. Please confirm which nozzle you would like to use " -"for this project." -msgstr "" -"ซอฟต์แวร์ไม่รองรับการใช้หัวฉีดที่มีเส้นผ่านศูนย์กลางต่างกันในการพิมพ์ครั้งเดียว " -"หากหัวฉีดด้านซ้ายและขวาไม่สอดคล้องกัน เราสามารถดำเนินการพิมพ์แบบหัวเดียวเท่านั้น " -"โปรดยืนยันว่าคุณต้องการใช้หัวฉีดใดสำหรับโปรเจ็กต์นี้" +msgid "The software does not support using different diameter of nozzles for one print. If the left and right nozzles are inconsistent, we can only proceed with single-head printing. Please confirm which nozzle you would like to use for this project." +msgstr "ซอฟต์แวร์ไม่รองรับการใช้หัวฉีดที่มีเส้นผ่านศูนย์กลางต่างกันในการพิมพ์ครั้งเดียว หากหัวฉีดด้านซ้ายและขวาไม่สอดคล้องกัน เราสามารถดำเนินการพิมพ์แบบหัวเดียวเท่านั้น โปรดยืนยันว่าคุณต้องการใช้หัวฉีดใดสำหรับโปรเจ็กต์นี้" msgid "Switch diameter" msgstr "สลับเส้นผ่านศูนย์กลาง" @@ -7434,16 +6920,13 @@ msgid "Sync printer information" msgstr "ซิงค์ข้อมูลเครื่องพิมพ์" msgid "" -"The currently selected machine preset is inconsistent with the connected " -"printer type.\n" +"The currently selected machine preset is inconsistent with the connected printer type.\n" "Are you sure to continue syncing?" msgstr "" "ค่าที่ตั้งล่วงหน้าของเครื่องที่เลือกในปัจจุบันไม่สอดคล้องกับประเภทเครื่องพิมพ์ที่เชื่อมต่อ\n" "คุณแน่ใจหรือไม่ว่าจะดำเนินการซิงค์ต่อ?" -msgid "" -"There are unset nozzle types. Please set the nozzle types of all extruders " -"before synchronizing." +msgid "There are unset nozzle types. Please set the nozzle types of all extruders before synchronizing." msgstr "มีประเภทหัวฉีดที่ไม่ได้ตั้งค่า โปรดตั้งค่าประเภทหัวฉีดของชุดดันเส้นทั้งหมดก่อนทำการซิงโครไนซ์" msgid "Sync extruder infomation" @@ -7483,9 +6966,7 @@ msgid "Pellets" msgstr "เม็ด" #, c-format, boost-format -msgid "" -"After completing your operation, %s project will be closed and create a new " -"project." +msgid "After completing your operation, %s project will be closed and create a new project." msgstr "หลังจากเสร็จสิ้นการดำเนินการของคุณ โครงการ %s จะถูกปิดและสร้างโครงการใหม่" msgid "There are no compatible filaments, and sync is not performed." @@ -7496,19 +6977,15 @@ msgstr "ซิงค์เส้นพลาสติกกับ AMS" msgid "" "There are some unknown or incompatible filaments mapped to generic preset.\n" -"Please update Orca Slicer or restart Orca Slicer to check if there is an " -"update to system presets." +"Please update Orca Slicer or restart Orca Slicer to check if there is an update to system presets." msgstr "" "มีเส้นพลาสติกที่ไม่รู้จักหรือเข้ากันไม่ได้บางส่วนที่แมปกับค่าที่ตั้งล่วงหน้าทั่วไป\n" -"โปรดอัปเดต Orca Slicer หรือรีสตาร์ท Orca Slicer " -"เพื่อตรวจสอบว่ามีการอัปเดตการตั้งค่าล่วงหน้าของระบบหรือไม่" +"โปรดอัปเดต Orca Slicer หรือรีสตาร์ท Orca Slicer เพื่อตรวจสอบว่ามีการอัปเดตการตั้งค่าล่วงหน้าของระบบหรือไม่" msgid "Only filament color information has been synchronized from printer." msgstr "เฉพาะข้อมูลสีของเส้นพลาสติกเท่านั้นที่ได้รับการซิงโครไนซ์จากเครื่องพิมพ์" -msgid "" -"Filament type and color information have been synchronized, but slot " -"information is not included." +msgid "Filament type and color information have been synchronized, but slot information is not included." msgstr "ข้อมูลประเภทเส้นพลาสติกและสีได้รับการซิงโครไนซ์แล้ว แต่ไม่รวมข้อมูลช่อง" #, boost-format @@ -7516,53 +6993,30 @@ msgid "Do you want to save changes to \"%1%\"?" msgstr "คุณต้องการบันทึกการเปลี่ยนแปลงใน \"%1%\" หรือไม่?" #, c-format, boost-format -msgid "" -"Successfully unmounted. The device %s (%s) can now be safely removed from " -"the computer." -msgstr "" -"ยกเลิกการต่อเชื่อมสำเร็จแล้ว ขณะนี้สามารถลบอุปกรณ์ %s (%s) " -"ออกจากคอมพิวเตอร์ได้อย่างปลอดภัยแล้ว" +msgid "Successfully unmounted. The device %s (%s) can now be safely removed from the computer." +msgstr "ยกเลิกการต่อเชื่อมสำเร็จแล้ว ขณะนี้สามารถลบอุปกรณ์ %s (%s) ออกจากคอมพิวเตอร์ได้อย่างปลอดภัยแล้ว" #, c-format, boost-format msgid "Ejecting of device %s (%s) has failed." msgstr "การดีดอุปกรณ์ %s (%s) ล้มเหลว" -msgid "" -"Previously unsaved items have been detected. Do you want to restore them?" +msgid "Previously unsaved items have been detected. Do you want to restore them?" msgstr "ตรวจพบโปรเจ็กต์ที่ยังไม่ได้บันทึกก่อนหน้านี้ คุณต้องการกู้คืนหรือไม่" msgid "Restore" msgstr "คืนค่า" -msgid "" -"The current heatbed temperature is relatively high. The nozzle may clog when " -"printing this filament in a closed environment. Please open the front door " -"and/or remove the upper glass." -msgstr "" -"อุณหภูมิฐานพิมพ์ร้อนในปัจจุบันค่อนข้างสูง หัวฉีดอาจอุดตันเมื่อพิมพ์เส้นพลาสติกนี้ในตู้ปิด " -"กรุณาเปิดประตูหน้าและ/หรือถอดกระจกด้านบนออก" +msgid "The current heatbed temperature is relatively high. The nozzle may clog when printing this filament in a closed environment. Please open the front door and/or remove the upper glass." +msgstr "อุณหภูมิฐานพิมพ์ร้อนในปัจจุบันค่อนข้างสูง หัวฉีดอาจอุดตันเมื่อพิมพ์เส้นพลาสติกนี้ในตู้ปิด กรุณาเปิดประตูหน้าและ/หรือถอดกระจกด้านบนออก" -msgid "" -"The nozzle hardness required by the filament is higher than the default " -"nozzle hardness of the printer. Please replace the hardened nozzle or " -"filament, otherwise, the nozzle will be worn down or damaged." -msgstr "" -"ความแข็งของหัวฉีดที่ต้องการสำหรับเส้นพลาสติกนั้นสูงกว่าความแข็งของหัวฉีดเริ่มต้นของเครื่องพิมพ์ " -"โปรดเปลี่ยนหัวฉีดหรือเส้นพลาสติกที่แข็งแล้ว ไม่เช่นนั้นหัวฉีดจะเสียหายหรือเสียหาย" +msgid "The nozzle hardness required by the filament is higher than the default nozzle hardness of the printer. Please replace the hardened nozzle or filament, otherwise, the nozzle will be worn down or damaged." +msgstr "ความแข็งของหัวฉีดที่ต้องการสำหรับเส้นพลาสติกนั้นสูงกว่าความแข็งของหัวฉีดเริ่มต้นของเครื่องพิมพ์ โปรดเปลี่ยนหัวฉีดหรือเส้นพลาสติกที่แข็งแล้ว ไม่เช่นนั้นหัวฉีดจะเสียหายหรือเสียหาย" -msgid "" -"Enabling traditional timelapse photography may cause surface imperfections. " -"It is recommended to change to smooth mode." -msgstr "" -"การเปิดใช้งานการถ่ายภาพไทม์แลปส์แบบดั้งเดิมอาจทำให้เกิดความไม่สมบูรณ์ของพื้นผิวได้ " -"ขอแนะนำให้เปลี่ยนเป็นโหมดราบรื่น" +msgid "Enabling traditional timelapse photography may cause surface imperfections. It is recommended to change to smooth mode." +msgstr "การเปิดใช้งานการถ่ายภาพไทม์แลปส์แบบดั้งเดิมอาจทำให้เกิดความไม่สมบูรณ์ของพื้นผิวได้ ขอแนะนำให้เปลี่ยนเป็นโหมดราบรื่น" -msgid "" -"Smooth mode for timelapse is enabled, but the prime tower is off, which may " -"cause print defects. Please enable the prime tower, re-slice and print again." -msgstr "" -"เปิดใช้งานโหมด Smooth สำหรับไทม์แลปส์แล้ว แต่ไพรม์ทาวเวอร์ปิดอยู่ " -"ซึ่งอาจทำให้เกิดข้อบกพร่องในการพิมพ์ โปรดเปิดใช้งานไพร์มทาวเวอร์ สไลซ์ใหม่และพิมพ์อีกครั้ง" +msgid "Smooth mode for timelapse is enabled, but the prime tower is off, which may cause print defects. Please enable the prime tower, re-slice and print again." +msgstr "เปิดใช้งานโหมด Smooth สำหรับไทม์แลปส์แล้ว แต่ไพรม์ทาวเวอร์ปิดอยู่ ซึ่งอาจทำให้เกิดข้อบกพร่องในการพิมพ์ โปรดเปิดใช้งานไพร์มทาวเวอร์ สไลซ์ใหม่และพิมพ์อีกครั้ง" msgid "Expand sidebar" msgstr "ขยายแถบด้านข้าง" @@ -7586,67 +7040,41 @@ msgstr "โครงการแบมบูสตูดิโอ" msgid "The 3MF is not supported by OrcaSlicer, loading geometry data only." msgstr "OrcaSlicer ไม่รองรับ 3MF โดยจะโหลดข้อมูลเรขาคณิตเท่านั้น" -msgid "" -"The 3MF file was generated by an old OrcaSlicer version, loading geometry " -"data only." +msgid "The 3MF file was generated by an old OrcaSlicer version, loading geometry data only." msgstr "ไฟล์ 3MF สร้างขึ้นโดย OrcaSlicer เวอร์ชันเก่า โดยโหลดข้อมูลเรขาคณิตเท่านั้น" -msgid "" -"The 3MF file was generated by an older version, loading geometry data only." +msgid "The 3MF file was generated by an older version, loading geometry data only." msgstr "ไฟล์ 3MF ถูกสร้างขึ้นโดยเวอร์ชันเก่า โดยโหลดข้อมูลเรขาคณิตเท่านั้น" msgid "The 3MF file was generated by BambuStudio, loading geometry data only." msgstr "ไฟล์ 3MF สร้างโดย BambuStudio โดยโหลดข้อมูลเรขาคณิตเท่านั้น" -msgid "" -"This project was created with an OrcaSlicer 2.3.1-alpha and uses infill " -"rotation template settings that may not work properly with your current " -"infill pattern. This could result in weak support or print quality issues." -msgstr "" -"โปรเจ็กต์นี้สร้างขึ้นด้วย OrcaSlicer 2.3.1-alpha และใช้การตั้งค่าเทมเพลตการหมุน ไส้ใน " -"ที่อาจทำงานไม่ถูกต้องกับรูปแบบ ไส้ใน ปัจจุบันของคุณ " -"ซึ่งอาจส่งผลให้ส่วนรองรับไม่ดีหรือปัญหาคุณภาพการพิมพ์" +msgid "This project was created with an OrcaSlicer 2.3.1-alpha and uses infill rotation template settings that may not work properly with your current infill pattern. This could result in weak support or print quality issues." +msgstr "โปรเจ็กต์นี้สร้างขึ้นด้วย OrcaSlicer 2.3.1-alpha และใช้การตั้งค่าเทมเพลตการหมุน ไส้ใน ที่อาจทำงานไม่ถูกต้องกับรูปแบบ ไส้ใน ปัจจุบันของคุณ ซึ่งอาจส่งผลให้ส่วนรองรับไม่ดีหรือปัญหาคุณภาพการพิมพ์" -msgid "" -"Would you like OrcaSlicer to automatically fix this by clearing the rotation " -"template settings?" -msgstr "" -"คุณต้องการให้ OrcaSlicer แก้ไขปัญหานี้โดยอัตโนมัติโดยการล้างการตั้งค่าเทมเพลตการหมุนหรือไม่" +msgid "Would you like OrcaSlicer to automatically fix this by clearing the rotation template settings?" +msgstr "คุณต้องการให้ OrcaSlicer แก้ไขปัญหานี้โดยอัตโนมัติโดยการล้างการตั้งค่าเทมเพลตการหมุนหรือไม่" #, c-format, boost-format -msgid "" -"The 3MF file version %s is newer than %s's version %s, found the following " -"unrecognized keys:" +msgid "The 3MF file version %s is newer than %s's version %s, found the following unrecognized keys:" msgstr "ไฟล์ 3MF เวอร์ชัน %s ใหม่กว่าเวอร์ชัน %s ของ %s พบคีย์ที่ไม่รู้จักต่อไปนี้:" msgid "You should update your software.\n" msgstr "คุณควรอัพเกรดซอฟต์แวร์ของคุณดีกว่า\n" #, c-format, boost-format -msgid "" -"The 3MF file version %s is newer than %s's version %s, we suggest to upgrade " -"your software." -msgstr "" -"ไฟล์ 3MF เวอร์ชัน %s ใหม่กว่าเวอร์ชัน %s ของ %s เราขอแนะนำให้อัปเกรดซอฟต์แวร์ของคุณ" +msgid "The 3MF file version %s is newer than %s's version %s, we suggest to upgrade your software." +msgstr "ไฟล์ 3MF เวอร์ชัน %s ใหม่กว่าเวอร์ชัน %s ของ %s เราขอแนะนำให้อัปเกรดซอฟต์แวร์ของคุณ" #, c-format, boost-format -msgid "" -"The 3MF was created by BambuStudio (version %s), which is newer than the " -"compatible version %s. Found unrecognized settings:" -msgstr "" -"3MF ถูกสร้างขึ้นโดย BambuStudio (เวอร์ชัน %s) ซึ่งใหม่กว่าเวอร์ชัน %s ที่เข้ากันได้ " -"พบการตั้งค่าที่ไม่รู้จัก:" +msgid "The 3MF was created by BambuStudio (version %s), which is newer than the compatible version %s. Found unrecognized settings:" +msgstr "3MF ถูกสร้างขึ้นโดย BambuStudio (เวอร์ชัน %s) ซึ่งใหม่กว่าเวอร์ชัน %s ที่เข้ากันได้ พบการตั้งค่าที่ไม่รู้จัก:" #, c-format, boost-format -msgid "" -"The 3MF was created by BambuStudio (version %s), which is newer than the " -"compatible version %s. Some settings may not be fully compatible." -msgstr "" -"3MF ถูกสร้างขึ้นโดย BambuStudio (เวอร์ชัน %s) ซึ่งใหม่กว่าเวอร์ชัน %s ที่เข้ากันได้ " -"การตั้งค่าบางอย่างอาจเข้ากันไม่ได้อย่างสมบูรณ์" +msgid "The 3MF was created by BambuStudio (version %s), which is newer than the compatible version %s. Some settings may not be fully compatible." +msgstr "3MF ถูกสร้างขึ้นโดย BambuStudio (เวอร์ชัน %s) ซึ่งใหม่กว่าเวอร์ชัน %s ที่เข้ากันได้ การตั้งค่าบางอย่างอาจเข้ากันไม่ได้อย่างสมบูรณ์" -msgid "" -"The 3MF was created by BambuStudio. Some settings may differ from OrcaSlicer." +msgid "The 3MF was created by BambuStudio. Some settings may differ from OrcaSlicer." msgstr "3MF ถูกสร้างขึ้นโดย BambuStudio การตั้งค่าบางอย่างอาจแตกต่างจาก OrcaSlicer" msgid "Invalid values found in the 3MF:" @@ -7655,13 +7083,10 @@ msgstr "พบค่าที่ไม่ถูกต้องใน 3MF:" msgid "Please correct them in the Param tabs" msgstr "โปรดแก้ไขให้ถูกต้องในแท็บพารามิเตอร์" -msgid "" -"The 3MF has the following modified G-code in filament or printer presets:" +msgid "The 3MF has the following modified G-code in filament or printer presets:" msgstr "3MF มี G-code ที่แก้ไขแล้วต่อไปนี้ในฟิลาเมนต์หรือพรีเซ็ตเครื่องพิมพ์:" -msgid "" -"Please confirm that all modified G-code is safe to prevent any damage to the " -"machine!" +msgid "Please confirm that all modified G-code is safe to prevent any damage to the machine!" msgstr "โปรดยืนยันว่า G-code ที่แก้ไขทั้งหมดนั้นปลอดภัยเพื่อป้องกันความเสียหายต่อเครื่อง!" msgid "Modified G-code" @@ -7670,9 +7095,7 @@ msgstr "แก้ไขรหัส G" msgid "The 3MF has the following customized filament or printer presets:" msgstr "3MF มีเส้นพลาสติกที่กำหนดเองหรือการตั้งค่าเครื่องพิมพ์ไว้ล่วงหน้าดังต่อไปนี้:" -msgid "" -"Please confirm that the G-code within these presets is safe to prevent any " -"damage to the machine!" +msgid "Please confirm that the G-code within these presets is safe to prevent any damage to the machine!" msgstr "โปรดยืนยันว่ารหัส G ภายในค่าที่ตั้งล่วงหน้าเหล่านี้ปลอดภัยเพื่อป้องกันความเสียหายต่อเครื่อง!" msgid "Customized Preset" @@ -7730,13 +7153,10 @@ msgid "Auto-Drop" msgstr "วางอัตโนมัติ" #, c-format, boost-format -msgid "" -"Connected printer is %s. It must match the project preset for printing.\n" +msgid "Connected printer is %s. It must match the project preset for printing.\n" msgstr "เครื่องพิมพ์ที่เชื่อมต่อคือ %s โดยจะต้องตรงกับค่าที่ตั้งล่วงหน้าของโปรเจ็กต์สำหรับการพิมพ์\n" -msgid "" -"Do you want to sync the printer information and automatically switch the " -"preset?" +msgid "Do you want to sync the printer information and automatically switch the preset?" msgstr "คุณต้องการซิงค์ข้อมูลเครื่องพิมพ์และสลับการตั้งค่าล่วงหน้าโดยอัตโนมัติหรือไม่" msgid "Tips" @@ -7745,9 +7165,7 @@ msgstr "เคล็ดลับ" msgid "The file does not contain any geometry data." msgstr "ไฟล์นี้ไม่มีข้อมูลเรขาคณิตใดๆ" -msgid "" -"Your object appears to be too large, do you want to scale it down to fit the " -"print bed automatically?" +msgid "Your object appears to be too large, do you want to scale it down to fit the print bed automatically?" msgstr "วัตถุของคุณดูใหญ่เกินไป คุณต้องการย่อขนาดให้พอดีกับฐานพิมพ์โดยอัตโนมัติหรือไม่" msgid "Object too large" @@ -7883,9 +7301,7 @@ msgstr "กำลังสไลซ์ฐานพิมพ์ %d" msgid "Please resolve the slicing errors and publish again." msgstr "โปรดแก้ไขข้อผิดพลาดในการแบ่งส่วนและเผยแพร่อีกครั้ง" -msgid "" -"The network plug-in was not detected. Network related features are " -"unavailable." +msgid "The network plug-in was not detected. Network related features are unavailable." msgstr "ตรวจไม่พบปลั๊กอินเครือข่าย คุณสมบัติที่เกี่ยวข้องกับเครือข่ายไม่พร้อมใช้งาน" msgid "" @@ -7896,15 +7312,12 @@ msgstr "" "ไฟล์ที่โหลดมีเฉพาะ G-code ไม่สามารถเข้าหน้าเตรียมความพร้อมได้" msgid "" -"The nozzle type and AMS quantity information has not been synced from the " -"connected printer.\n" -"After syncing, software can optimize printing time and filament usage when " -"slicing.\n" +"The nozzle type and AMS quantity information has not been synced from the connected printer.\n" +"After syncing, software can optimize printing time and filament usage when slicing.\n" "Would you like to sync now?" msgstr "" "ข้อมูลประเภทหัวฉีดและข้อมูลปริมาณ AMS ไม่ได้ถูกซิงค์จากเครื่องพิมพ์ที่เชื่อมต่อ\n" -"หลังจากการซิงค์ " -"ซอฟต์แวร์จะสามารถปรับเวลาการพิมพ์และการใช้เส้นพลาสติกให้เหมาะสมเมื่อทำการตัด\n" +"หลังจากการซิงค์ ซอฟต์แวร์จะสามารถปรับเวลาการพิมพ์และการใช้เส้นพลาสติกให้เหมาะสมเมื่อทำการตัด\n" "คุณต้องการซิงค์ตอนนี้หรือไม่?" msgid "Sync now" @@ -7921,8 +7334,7 @@ msgstr "โหลดโปรเจกต์" msgid "" "Failed to save the project.\n" -"Please check whether the folder exists online or if other programs have the " -"project file open." +"Please check whether the folder exists online or if other programs have the project file open." msgstr "" "ไม่สามารถบันทึกโครงการ\n" "โปรดตรวจสอบว่าโฟลเดอร์ออนไลน์อยู่หรือมีโปรแกรมอื่นเปิดไฟล์โครงการหรือไม่" @@ -7949,16 +7361,13 @@ msgstr "ดาวน์โหลดล้มเหลว มีข้อยก msgid "Project downloaded %d%%" msgstr "ดาวน์โหลดโครงการแล้ว %d%%" -msgid "" -"Importing to Orca Slicer failed. Please download the file and manually " -"import it." +msgid "Importing to Orca Slicer failed. Please download the file and manually import it." msgstr "การนำเข้าไปยัง Orca Slicer ล้มเหลว โปรดดาวน์โหลดไฟล์และนำเข้าด้วยตนเอง" msgid "INFO:" msgstr "ข้อมูล:" -msgid "" -"No accelerations provided for calibration. Use default acceleration value " +msgid "No accelerations provided for calibration. Use default acceleration value " msgstr "ไม่มีการเร่งความเร็วสำหรับการสอบเทียบ ใช้ค่าความเร่งเริ่มต้น" msgid "No speeds provided for calibration. Use default optimal speed " @@ -8017,9 +7426,7 @@ msgstr "ไม่สามารถเพิ่มโมเดลได้เม msgid "All objects will be removed, continue?" msgstr "วัตถุทั้งหมดจะถูกลบ ดำเนินการต่อหรือไม่" -msgid "" -"The current project has unsaved changes. Would you like to save before " -"continuing?" +msgid "The current project has unsaved changes. Would you like to save before continuing?" msgstr "โปรเจ็กต์ปัจจุบันมีการเปลี่ยนแปลงที่ยังไม่ได้บันทึก บันทึกก่อนดำเนินการต่อหรือไม่" msgid "Number of copies:" @@ -8044,9 +7451,7 @@ msgid "Save Sliced file as:" msgstr "บันทึกไฟล์ที่สไลซ์เป็น:" #, c-format, boost-format -msgid "" -"The file %s has been sent to the printer's storage space and can be viewed " -"on the printer." +msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "ไฟล์ %s ถูกส่งไปยังพื้นที่เก็บข้อมูลของเครื่องพิมพ์แล้ว และสามารถดูได้บนเครื่องพิมพ์" msgid "The nozzle type is not set. Please set the nozzle and try again." @@ -8055,12 +7460,8 @@ msgstr "ไม่ได้ตั้งค่าประเภทหัวฉี msgid "The nozzle type is not set. Please check." msgstr "ไม่ได้ตั้งค่าประเภทหัวฉีด กรุณาตรวจสอบ." -msgid "" -"Unable to perform boolean operation on model meshes. Only positive parts " -"will be kept. You may fix the meshes and try again." -msgstr "" -"ไม่สามารถดำเนินการบูลีนบนโมเดลเมชได้ จะเก็บเฉพาะส่วนที่เป็นบวกเท่านั้น " -"คุณอาจแก้ไขตาข่ายแล้วลองอีกครั้ง" +msgid "Unable to perform boolean operation on model meshes. Only positive parts will be kept. You may fix the meshes and try again." +msgstr "ไม่สามารถดำเนินการบูลีนบนโมเดลเมชได้ จะเก็บเฉพาะส่วนที่เป็นบวกเท่านั้น คุณอาจแก้ไขตาข่ายแล้วลองอีกครั้ง" #, boost-format msgid "Reason: part \"%1%\" is empty." @@ -8078,9 +7479,7 @@ msgstr "เหตุผล: ส่วน \"%1%\" มีจุดตัดกั msgid "Reason: \"%1%\" and another part have no intersection." msgstr "เหตุผล: \"%1%\" และอีกส่วนหนึ่งไม่มีจุดตัดกัน" -msgid "" -"Unable to perform boolean operation on model meshes. Only positive parts " -"will be exported." +msgid "Unable to perform boolean operation on model meshes. Only positive parts will be exported." msgstr "ไม่สามารถดำเนินการบูลีนบนโมเดลเมชได้ เฉพาะส่วนที่เป็นบวกเท่านั้นที่จะถูกส่งออก" msgid "Flashforge host is not available." @@ -8118,36 +7517,22 @@ msgid "Optimize Rotation" msgstr "เพิ่มประสิทธิภาพการหมุน" #, c-format, boost-format -msgid "" -"Printer not connected. Please go to the device page to connect %s before " -"syncing." +msgid "Printer not connected. Please go to the device page to connect %s before syncing." msgstr "ไม่ได้เชื่อมต่อเครื่องพิมพ์ โปรดไปที่หน้าอุปกรณ์เพื่อเชื่อมต่อ %s ก่อนที่จะทำการซิงค์" #, c-format, boost-format -msgid "" -"OrcaSlicer can't connect to %s. Please check if the printer is powered on " -"and connected to the network." -msgstr "" -"OrcaSlicer ไม่สามารถเชื่อมต่อกับ %s ได้ " -"โปรดตรวจสอบว่าเครื่องพิมพ์เปิดอยู่และเชื่อมต่อกับเครือข่ายหรือไม่" +msgid "OrcaSlicer can't connect to %s. Please check if the printer is powered on and connected to the network." +msgstr "OrcaSlicer ไม่สามารถเชื่อมต่อกับ %s ได้ โปรดตรวจสอบว่าเครื่องพิมพ์เปิดอยู่และเชื่อมต่อกับเครือข่ายหรือไม่" #, c-format, boost-format -msgid "" -"The currently connected printer on the device page is not %s. Please switch " -"to %s before syncing." +msgid "The currently connected printer on the device page is not %s. Please switch to %s before syncing." msgstr "เครื่องพิมพ์ที่เชื่อมต่ออยู่ในหน้าอุปกรณ์ไม่ใช่ %s โปรดสลับไปที่ %s ก่อนที่จะทำการซิงค์" -msgid "" -"There are no filaments on the printer. Please load the filaments on the " -"printer first." +msgid "There are no filaments on the printer. Please load the filaments on the printer first." msgstr "ไม่มีเส้นพลาสติกบนเครื่องพิมพ์ กรุณาใส่ฟิลาเมนต์บนเครื่องพิมพ์ก่อน" -msgid "" -"The filaments on the printer are all unknown types. Please go to the printer " -"screen or software device page to set the filament type." -msgstr "" -"เส้นพลาสติกบนเครื่องพิมพ์ล้วนเป็นประเภทที่ไม่รู้จัก " -"โปรดไปที่หน้าจอเครื่องพิมพ์หรือหน้าอุปกรณ์ซอฟต์แวร์เพื่อตั้งค่าประเภทเส้นพลาสติก" +msgid "The filaments on the printer are all unknown types. Please go to the printer screen or software device page to set the filament type." +msgstr "เส้นพลาสติกบนเครื่องพิมพ์ล้วนเป็นประเภทที่ไม่รู้จัก โปรดไปที่หน้าจอเครื่องพิมพ์หรือหน้าอุปกรณ์ซอฟต์แวร์เพื่อตั้งค่าประเภทเส้นพลาสติก" msgid "Device Page" msgstr "หน้าอุปกรณ์" @@ -8198,19 +7583,11 @@ msgid "Use \"Fix Model\" to repair the mesh." msgstr "ใช้ \"Fix Model\" เพื่อซ่อมแซมตาข่าย" #, c-format, boost-format -msgid "" -"Plate %d: %s is not suggested for use printing filament %s (%s). If you " -"still want to do this print job, please set this filament's bed temperature " -"to a number that is not zero." -msgstr "" -"เพลต %d: ไม่แนะนำให้ใช้ %s กับเส้นพลาสติก %s (%s) หากยังต้องการพิมพ์งานนี้ " -"โปรดตั้งอุณหภูมิฐานของเส้นพลาสติกให้ไม่เป็นศูนย์" +msgid "Plate %d: %s is not suggested for use printing filament %s (%s). If you still want to do this print job, please set this filament's bed temperature to a number that is not zero." +msgstr "เพลต %d: ไม่แนะนำให้ใช้ %s กับเส้นพลาสติก %s (%s) หากยังต้องการพิมพ์งานนี้ โปรดตั้งอุณหภูมิฐานของเส้นพลาสติกให้ไม่เป็นศูนย์" -msgid "" -"Currently, the object configuration form cannot be used with a multiple-" -"extruder printer." -msgstr "" -"ในปัจจุบัน แบบฟอร์มการกำหนดค่าออบเจ็กต์ไม่สามารถใช้กับเครื่องพิมพ์ที่มีชุดดันเส้นหลายเครื่องได้" +msgid "Currently, the object configuration form cannot be used with a multiple-extruder printer." +msgstr "ในปัจจุบัน แบบฟอร์มการกำหนดค่าออบเจ็กต์ไม่สามารถใช้กับเครื่องพิมพ์ที่มีชุดดันเส้นหลายเครื่องได้" msgid "Not available" msgstr "ไม่พร้อมใช้งาน" @@ -8341,22 +7718,11 @@ msgstr "เปิดใช้งานโหมดมืด" msgid "Allow only one OrcaSlicer instance" msgstr "อนุญาตอินสแตนซ์ OrcaSlicer เพียงอินสแตนซ์เดียวเท่านั้น" -msgid "" -"On OSX there is always only one instance of app running by default. However " -"it is allowed to run multiple instances of same app from the command line. " -"In such case this settings will allow only one instance." -msgstr "" -"บน OSX จะมีแอปเพียงอินสแตนซ์เดียวที่ทำงานตามค่าเริ่มต้นเสมอ อย่างไรก็ตาม " -"อนุญาตให้เรียกใช้แอปเดียวกันหลายอินสแตนซ์จากบรรทัดคำสั่ง ในกรณีเช่นนี้ " -"การตั้งค่านี้จะอนุญาตเพียงอินสแตนซ์เดียวเท่านั้น" +msgid "On OSX there is always only one instance of app running by default. However it is allowed to run multiple instances of same app from the command line. In such case this settings will allow only one instance." +msgstr "บน OSX จะมีแอปเพียงอินสแตนซ์เดียวที่ทำงานตามค่าเริ่มต้นเสมอ อย่างไรก็ตาม อนุญาตให้เรียกใช้แอปเดียวกันหลายอินสแตนซ์จากบรรทัดคำสั่ง ในกรณีเช่นนี้ การตั้งค่านี้จะอนุญาตเพียงอินสแตนซ์เดียวเท่านั้น" -msgid "" -"If this is enabled, when starting OrcaSlicer and another instance of the " -"same OrcaSlicer is already running, that instance will be reactivated " -"instead." -msgstr "" -"หากเปิดใช้งานนี้ เมื่อเริ่มต้น OrcaSlicer และอินสแตนซ์อื่นของ OrcaSlicer " -"เดียวกันกำลังทำงานอยู่แล้ว อินสแตนซ์นั้นจะถูกเปิดใช้งานอีกครั้งแทน" +msgid "If this is enabled, when starting OrcaSlicer and another instance of the same OrcaSlicer is already running, that instance will be reactivated instead." +msgstr "หากเปิดใช้งานนี้ เมื่อเริ่มต้น OrcaSlicer และอินสแตนซ์อื่นของ OrcaSlicer เดียวกันกำลังทำงานอยู่แล้ว อินสแตนซ์นั้นจะถูกเปิดใช้งานอีกครั้งแทน" msgid "Show splash screen" msgstr "แสดงหน้าจอเริ่มต้น" @@ -8367,9 +7733,7 @@ msgstr "แสดงหน้าจอเริ่มต้นระหว่า msgid "Show shared profiles notification" msgstr "แสดงการแจ้งเตือนโปรไฟล์ที่แชร์" -msgid "" -"Show a notification with a link to browse shared profiles when the selected " -"printer is changed." +msgid "Show a notification with a link to browse shared profiles when the selected printer is changed." msgstr "แสดงการแจ้งเตือนพร้อมลิงก์เพื่อเรียกดูโปรไฟล์ที่แชร์เมื่อเครื่องพิมพ์ที่เลือกมีการเปลี่ยนแปลง" msgid "Use window buttons on left side" @@ -8399,8 +7763,7 @@ msgstr "โหลดรูปทรงเท่านั้น" msgid "Load behaviour" msgstr "พฤติกรรมการโหลด" -msgid "" -"Should printer/filament/process settings be loaded when opening a 3MF file?" +msgid "Should printer/filament/process settings be loaded when opening a 3MF file?" msgstr "ควรโหลดการตั้งค่าเครื่องพิมพ์/เส้นพลาสติก/กระบวนการเมื่อเปิดไฟล์ 3MF หรือไม่" msgid "Maximum recent files" @@ -8418,16 +7781,13 @@ msgstr "อย่าเตือนเมื่อโหลด 3MF ด้วย msgid "Show options when importing STEP file" msgstr "แสดงตัวเลือกเมื่อนำเข้าไฟล์ STEP" -msgid "" -"If enabled, a parameter settings dialog will appear during STEP file import." +msgid "If enabled, a parameter settings dialog will appear during STEP file import." msgstr "หากเปิดใช้งาน กล่องโต้ตอบการตั้งค่าพารามิเตอร์จะปรากฏขึ้นระหว่างการนำเข้าไฟล์ STEP" msgid "Auto backup" msgstr "การสำรองข้อมูลอัตโนมัติ" -msgid "" -"Backup your project periodically to help with restoring from an occasional " -"crash." +msgid "Backup your project periodically to help with restoring from an occasional crash." msgstr "สำรองข้อมูลโปรเจ็กต์ของคุณเป็นระยะเพื่อกู้คืนจากข้อขัดข้องเป็นครั้งคราว" msgid "Preset" @@ -8436,12 +7796,8 @@ msgstr "พรีเซ็ต" msgid "Remember printer configuration" msgstr "จำการกำหนดค่าเครื่องพิมพ์" -msgid "" -"If enabled, Orca will remember and switch filament/process configuration for " -"each printer automatically." -msgstr "" -"หากเปิดใช้งาน Orca จะจดจำและสลับการกำหนดค่าเส้นพลาสติก/" -"กระบวนการสำหรับเครื่องพิมพ์แต่ละเครื่องโดยอัตโนมัติ" +msgid "If enabled, Orca will remember and switch filament/process configuration for each printer automatically." +msgstr "หากเปิดใช้งาน Orca จะจดจำและสลับการกำหนดค่าเส้นพลาสติก/กระบวนการสำหรับเครื่องพิมพ์แต่ละเครื่องโดยอัตโนมัติ" msgid "Group user filament presets" msgstr "การตั้งค่าเส้นพลาสติกผู้ใช้แบบกลุ่ม" @@ -8470,12 +7826,8 @@ msgstr "คุณสมบัติ" msgid "Multi device management" msgstr "การจัดการอุปกรณ์หลายเครื่อง" -msgid "" -"With this option enabled, you can send a task to multiple devices at the " -"same time and manage multiple devices." -msgstr "" -"เมื่อเปิดใช้งานตัวเลือกนี้ " -"คุณสามารถส่งงานไปยังอุปกรณ์หลายเครื่องพร้อมกันและจัดการอุปกรณ์หลายเครื่องได้" +msgid "With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices." +msgstr "เมื่อเปิดใช้งานตัวเลือกนี้ คุณสามารถส่งงานไปยังอุปกรณ์หลายเครื่องพร้อมกันและจัดการอุปกรณ์หลายเครื่องได้" msgid "Pop up to select filament grouping mode" msgstr "ปรากฏขึ้นเพื่อเลือกโหมดการจัดกลุ่มเส้นพลาสติก" @@ -8487,18 +7839,13 @@ msgid "bits" msgstr "บิต" msgid "" -"Controls the quantization bit depth used when compressing the mesh to Draco " -"format.\n" -"0 = lossless compression (geometry is preserved at full precision). Valid " -"lossy values range from 8 to 30.\n" -"Lower values produce smaller files but lose more geometric detail; higher " -"values preserve more detail at the cost of larger files." +"Controls the quantization bit depth used when compressing the mesh to Draco format.\n" +"0 = lossless compression (geometry is preserved at full precision). Valid lossy values range from 8 to 30.\n" +"Lower values produce smaller files but lose more geometric detail; higher values preserve more detail at the cost of larger files." msgstr "" "ควบคุมความลึกบิตเชิงปริมาณที่ใช้ในการบีบอัด mesh เป็นรูปแบบ Draco\n" -"0 = การบีบอัดแบบไม่สูญเสีย (รูปทรงเรขาคณิตจะถูกรักษาไว้อย่างแม่นยำเต็มที่) " -"ค่าการสูญเสียที่ถูกต้องมีตั้งแต่ 8 ถึง 30\n" -"ค่าที่ต่ำกว่าจะทำให้ไฟล์มีขนาดเล็กลง แต่สูญเสียรายละเอียดทางเรขาคณิตมากขึ้น " -"ค่าที่สูงกว่าจะรักษารายละเอียดได้มากขึ้นโดยที่ไฟล์มีขนาดใหญ่กว่า" +"0 = การบีบอัดแบบไม่สูญเสีย (รูปทรงเรขาคณิตจะถูกรักษาไว้อย่างแม่นยำเต็มที่) ค่าการสูญเสียที่ถูกต้องมีตั้งแต่ 8 ถึง 30\n" +"ค่าที่ต่ำกว่าจะทำให้ไฟล์มีขนาดเล็กลง แต่สูญเสียรายละเอียดทางเรขาคณิตมากขึ้น ค่าที่สูงกว่าจะรักษารายละเอียดได้มากขึ้นโดยที่ไฟล์มีขนาดใหญ่กว่า" msgid "Behaviour" msgstr "พฤติกรรม" @@ -8515,26 +7862,16 @@ msgstr "จัดเรียงจานอัตโนมัติหลัง msgid "Auto slice after changes" msgstr "แบ่งส่วนอัตโนมัติหลังจากการเปลี่ยนแปลง" -msgid "" -"If enabled, OrcaSlicer will re-slice automatically whenever slicing-related " -"settings change." -msgstr "" -"หากเปิดใช้งาน OrcaSlicer " -"จะแบ่งส่วนใหม่โดยอัตโนมัติทุกครั้งที่การตั้งค่าที่เกี่ยวข้องกับการแบ่งมีการเปลี่ยนแปลง" +msgid "If enabled, OrcaSlicer will re-slice automatically whenever slicing-related settings change." +msgstr "หากเปิดใช้งาน OrcaSlicer จะแบ่งส่วนใหม่โดยอัตโนมัติทุกครั้งที่การตั้งค่าที่เกี่ยวข้องกับการแบ่งมีการเปลี่ยนแปลง" -msgid "" -"Delay in seconds before auto slicing starts, allowing multiple edits to be " -"grouped. Use 0 to slice immediately." -msgstr "" -"หน่วงเวลาเป็นวินาทีก่อนที่การแบ่งส่วนอัตโนมัติจะเริ่มขึ้น ทำให้สามารถจัดกลุ่มการแก้ไขได้หลายรายการ " -"ใช้ 0 เพื่อสไลซ์ทันที" +msgid "Delay in seconds before auto slicing starts, allowing multiple edits to be grouped. Use 0 to slice immediately." +msgstr "หน่วงเวลาเป็นวินาทีก่อนที่การแบ่งส่วนอัตโนมัติจะเริ่มขึ้น ทำให้สามารถจัดกลุ่มการแก้ไขได้หลายรายการ ใช้ 0 เพื่อสไลซ์ทันที" msgid "Remove mixed temperature restriction" msgstr "ลบข้อจำกัดอุณหภูมิแบบผสม" -msgid "" -"With this option enabled, you can print materials with a large temperature " -"difference together." +msgid "With this option enabled, you can print materials with a large temperature difference together." msgstr "เมื่อเปิดใช้งานตัวเลือกนี้ คุณสามารถพิมพ์วัสดุที่มีอุณหภูมิแตกต่างกันมากร่วมกันได้" msgid "Touchpad" @@ -8561,9 +7898,7 @@ msgstr "คูณความเร็ววงโคจรเพื่อกา msgid "Zoom to mouse position" msgstr "ซูมไปที่ตำแหน่งเมาส์" -msgid "" -"Zoom in towards the mouse pointer's position in the 3D view, rather than the " -"2D window center." +msgid "Zoom in towards the mouse pointer's position in the 3D view, rather than the 2D window center." msgstr "ซูมเข้าไปยังตำแหน่งของตัวชี้เมาส์ในมุมมอง 3 มิติ แทนที่จะซูมไปที่กึ่งกลางหน้าต่าง 2 มิติ" msgid "Use free camera" @@ -8617,8 +7952,7 @@ msgstr "ล้างตัวเลือกของฉันในการต msgid "Synchronizing printer preset" msgstr "กำลังซิงโครไนซ์การตั้งค่าเครื่องพิมพ์ล่วงหน้า" -msgid "" -"Clear my choice for synchronizing printer preset after loading the file." +msgid "Clear my choice for synchronizing printer preset after loading the file." msgstr "ล้างตัวเลือกของฉันในการซิงโครไนซ์พรีเซ็ตเครื่องพิมพ์หลังจากโหลดไฟล์" msgid "Graphics" @@ -8648,13 +7982,11 @@ msgstr "นอร์มัลแบบเรียบ" msgid "" "Applies smooth normals to the realistic view.\n" "\n" -"Requires manual scene reload to take effect (right-click on 3D view → " -"\"Reload All\")." +"Requires manual scene reload to take effect (right-click on 3D view → \"Reload All\")." msgstr "" "ใช้นอร์มัลแบบเรียบในมุมมองแบบสมจริง\n" "\n" -"ต้องโหลดฉากใหม่ด้วยตนเองจึงจะมีผล (คลิกขวาที่มุมมอง 3D → " -"\"Reload All\")." +"ต้องโหลดฉากใหม่ด้วยตนเองจึงจะมีผล (คลิกขวาที่มุมมอง 3D → \"Reload All\")." msgid "Anti-aliasing" msgstr "ต่อต้านนามแฝง" @@ -8664,11 +7996,9 @@ msgstr "ตัวคูณ MSAA" msgid "" "Set the Multi-Sample Anti-Aliasing level.\n" -"Higher values result in smoother edges, but the impact on performance is " -"exponential.\n" +"Higher values result in smoother edges, but the impact on performance is exponential.\n" "Lower values improve performance, at the cost of jagged edges.\n" -"If disabled, its recommended to enable FXAA to reduce jagged edges with " -"minimal performance impact.\n" +"If disabled, its recommended to enable FXAA to reduce jagged edges with minimal performance impact.\n" "\n" "Requires application restart." msgstr "" @@ -8725,13 +8055,10 @@ msgid "Stealth mode" msgstr "โหมดซ่อนตัว" msgid "" -"This disables all cloud features, including Orca Cloud profile syncing. " -"Users who prefer to work entirely offline can enable this option.\n" -"Note: When Stealth Mode is enabled, your user profiles will not be backed up " -"to Orca Cloud." +"This disables all cloud features, including Orca Cloud profile syncing. Users who prefer to work entirely offline can enable this option.\n" +"Note: When Stealth Mode is enabled, your user profiles will not be backed up to Orca Cloud." msgstr "" -"ตัวเลือกนี้จะปิดการใช้งานฟีเจอร์คลาวด์ทั้งหมด รวมถึงการซิงก์โปรไฟล์ Orca Cloud " -"ผู้ใช้ที่ต้องการทำงานแบบออฟไลน์ทั้งหมดสามารถเปิดตัวเลือกนี้ได้\n" +"ตัวเลือกนี้จะปิดการใช้งานฟีเจอร์คลาวด์ทั้งหมด รวมถึงการซิงก์โปรไฟล์ Orca Cloud ผู้ใช้ที่ต้องการทำงานแบบออฟไลน์ทั้งหมดสามารถเปิดตัวเลือกนี้ได้\n" "หมายเหตุ: เมื่อเปิดโหมดซ่อนตัว โปรไฟล์ผู้ใช้ของคุณจะไม่ถูกสำรองไปยัง Orca Cloud" msgid "Hide login side panel" @@ -8752,12 +8079,8 @@ msgstr "ผู้ให้บริการคลาวด์" msgid "Enable Bambu Cloud" msgstr "เปิดใช้งาน Bambu Cloud" -msgid "" -"Allow logging into Bambu Cloud alongside Orca Cloud. When enabled, a Bambu " -"login section appears on the homepage." -msgstr "" -"อนุญาตให้เข้าสู่ระบบ Bambu Cloud ควบคู่ไปกับ Orca Cloud เมื่อเปิดใช้งาน ส่วนเข้าสู่ระบบ " -"Bambu จะปรากฏบนหน้าแรก" +msgid "Allow logging into Bambu Cloud alongside Orca Cloud. When enabled, a Bambu login section appears on the homepage." +msgstr "อนุญาตให้เข้าสู่ระบบ Bambu Cloud ควบคู่ไปกับ Orca Cloud เมื่อเปิดใช้งาน ส่วนเข้าสู่ระบบ Bambu จะปรากฏบนหน้าแรก" msgid "Update & sync" msgstr "อัปเดตและซิงค์" @@ -8774,9 +8097,7 @@ msgstr "อัปเดตค่าที่ตั้งล่วงหน้า msgid "Use encrypted file for token storage" msgstr "ใช้ไฟล์ที่เข้ารหัสสำหรับการจัดเก็บโทเค็น" -msgid "" -"Store authentication tokens in an encrypted file instead of the system " -"keychain. (Requires restart)" +msgid "Store authentication tokens in an encrypted file instead of the system keychain. (Requires restart)" msgstr "จัดเก็บโทเค็นการรับรองความถูกต้องในไฟล์ที่เข้ารหัสแทนพวงกุญแจระบบ (ต้องรีสตาร์ท)" msgid "Filament Sync Options" @@ -8785,8 +8106,7 @@ msgstr "ตัวเลือกการซิงค์ฟิลาเมนต msgid "Filament sync mode" msgstr "โหมดการซิงค์ฟิลาเมนต์" -msgid "" -"Choose whether sync updates both filament preset and color, or only color." +msgid "Choose whether sync updates both filament preset and color, or only color." msgstr "เลือกว่าการซิงค์จะอัปเดตทั้งค่าที่ตั้งไว้ล่วงหน้าของเส้นพลาสติกและสี หรือเฉพาะสีเท่านั้น" msgid "Filament & Color" @@ -8839,9 +8159,7 @@ msgstr "ดาวน์โหลดปลั๊กอินเครือข่ msgid "Associate files to OrcaSlicer" msgstr "เชื่อมโยงไฟล์กับ OrcaSlicer" -msgid "" -"File associations for the Microsoft Store version are managed by Windows " -"Settings." +msgid "File associations for the Microsoft Store version are managed by Windows Settings." msgstr "การเชื่อมโยงไฟล์สำหรับเวอร์ชัน Microsoft Store จัดการผ่านการตั้งค่า Windows" msgid "Open Windows Default Apps Settings" @@ -8850,9 +8168,7 @@ msgstr "เปิดการตั้งค่าแอปเริ่มต้ msgid "Associate 3MF files to OrcaSlicer" msgstr "เชื่อมโยงไฟล์ 3MF กับ OrcaSlicer" -msgid "" -"If enabled, this sets OrcaSlicer as the default application to open 3MF " -"files." +msgid "If enabled, this sets OrcaSlicer as the default application to open 3MF files." msgstr "หากเปิดใช้งาน ให้ตั้งค่า OrcaSlicer เป็นแอปพลิเคชันเริ่มต้นเพื่อเปิดไฟล์ 3MF" msgid "Associate DRC files to OrcaSlicer" @@ -8864,17 +8180,13 @@ msgstr "หากเปิดใช้งาน ให้ตั้งค่า O msgid "Associate STL files to OrcaSlicer" msgstr "เชื่อมโยงไฟล์ STL กับ OrcaSlicer" -msgid "" -"If enabled, this sets OrcaSlicer as the default application to open STL " -"files." +msgid "If enabled, this sets OrcaSlicer as the default application to open STL files." msgstr "หากเปิดใช้งาน ให้ตั้งค่า OrcaSlicer เป็นแอปพลิเคชันเริ่มต้นเพื่อเปิดไฟล์ STL" msgid "Associate STEP files to OrcaSlicer" msgstr "เชื่อมโยงไฟล์ STEP กับ OrcaSlicer" -msgid "" -"If enabled, this sets OrcaSlicer as the default application to open STEP " -"files." +msgid "If enabled, this sets OrcaSlicer as the default application to open STEP files." msgstr "หากเปิดใช้งาน ให้ตั้งค่า OrcaSlicer เป็นแอปพลิเคชันเริ่มต้นเพื่อเปิดไฟล์ STEP" msgid "Associate web links to OrcaSlicer" @@ -8890,23 +8202,17 @@ msgid "(Experimental) Keep painted feature after mesh change" msgstr "(ทดลอง) เก็บคุณสมบัติการทาสีไว้หลังจากเปลี่ยนตาข่าย" msgid "" -"Attempt to keep painted features (color/seam/support/fuzzy etc.) after " -"changing the object mesh (such as cut/reload from disk/simplify/fix etc.)\n" +"Attempt to keep painted features (color/seam/support/fuzzy etc.) after changing the object mesh (such as cut/reload from disk/simplify/fix etc.)\n" "Highly experimental! Slow and may create artifact." msgstr "" -"พยายามคงคุณสมบัติการทาสีไว้ (สี/รอยตะเข็บ/ส่วนรองรับ/คลุมเครือ ฯลฯ) หลังจากเปลี่ยนตาข่ายวัตถุ " -"(เช่น ตัด/โหลดซ้ำจากดิสก์/ลดความซับซ้อน/แก้ไข ฯลฯ)\n" +"พยายามคงคุณสมบัติการทาสีไว้ (สี/รอยตะเข็บ/ส่วนรองรับ/คลุมเครือ ฯลฯ) หลังจากเปลี่ยนตาข่ายวัตถุ (เช่น ตัด/โหลดซ้ำจากดิสก์/ลดความซับซ้อน/แก้ไข ฯลฯ)\n" "น่าทดลองมาก! ช้าและอาจสร้างสิ่งประดิษฐ์" msgid "Show unsupported presets" msgstr "แสดงค่าที่ตั้งไว้ล่วงหน้าที่ไม่รองรับ" -msgid "" -"Show incompatible/unsupported presets in the printer and filament dropdown " -"lists. These presets cannot be selected." -msgstr "" -"แสดงค่าที่ตั้งไว้ล่วงหน้าที่ไม่เข้ากันหรือไม่รองรับในรายการเลือกเครื่องพิมพ์และเส้นพลาสติก " -"ไม่สามารถเลือกค่าที่ตั้งไว้ล่วงหน้าเหล่านี้ได้" +msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." +msgstr "แสดงค่าที่ตั้งไว้ล่วงหน้าที่ไม่เข้ากันหรือไม่รองรับในรายการเลือกเครื่องพิมพ์และเส้นพลาสติก ไม่สามารถเลือกค่าที่ตั้งไว้ล่วงหน้าเหล่านี้ได้" msgid "Allow Abnormal Storage" msgstr "อนุญาตให้จัดเก็บผิดปกติ" @@ -9200,8 +8506,7 @@ msgid "Preset \"%1%\" already exists." msgstr "มีพรีเซ็ต \"%1%\" อยู่แล้ว" #, boost-format -msgid "" -"Preset \"%1%\" already exists and is incompatible with the current printer." +msgid "Preset \"%1%\" already exists and is incompatible with the current printer." msgstr "ค่าที่ตั้งไว้ล่วงหน้า \"%1%\" มีอยู่แล้ว และเข้ากันไม่ได้กับเครื่องพิมพ์ปัจจุบัน" msgid "Please note that saving will overwrite the current preset." @@ -9283,10 +8588,8 @@ msgid "Auto Bed Leveling" msgstr "ปรับระดับฐานพิมพ์อัตโนมัติ" msgid "" -"This checks the flatness of heatbed. Leveling makes extruded height " -"uniform.\n" -"*Automatic mode: Run a leveling check(about 10 seconds). Skip if surface is " -"fine." +"This checks the flatness of heatbed. Leveling makes extruded height uniform.\n" +"*Automatic mode: Run a leveling check(about 10 seconds). Skip if surface is fine." msgstr "" "วิธีนี้จะตรวจสอบความเรียบของฐานพิมพ์ การปรับระดับทำให้ความสูงที่ฉีดเส้นออกมาสม่ำเสมอ\n" "*โหมดอัตโนมัติ: เรียกใช้การตรวจสอบการปรับระดับ (ประมาณ 10 วินาที) ข้ามไปหากพื้นผิวดี" @@ -9295,8 +8598,7 @@ msgid "Flow Dynamics Calibration" msgstr "ปรับเทียบไดนามิกการไหล" msgid "" -"This process determines the dynamic flow values to improve overall print " -"quality.\n" +"This process determines the dynamic flow values to improve overall print quality.\n" "*Automatic mode: Skip if the filament was calibrated recently." msgstr "" "กระบวนการนี้จะกำหนดค่าการไหลแบบไดนามิกเพื่อปรับปรุงคุณภาพการพิมพ์โดยรวม\n" @@ -9322,79 +8624,40 @@ msgid "High Flow" msgstr "อัตราไหลสูง" #, c-format, boost-format -msgid "" -"The nozzle flow setting of %s(%s) doesn't match with the slicing file(%s). " -"Please make sure the nozzle installed matches with settings in printer, then " -"set the corresponding printer preset while slicing." -msgstr "" -"การตั้งค่าการไหลของหัวฉีดของ %s(%s) ไม่ตรงกับไฟล์การแบ่งส่วน (%s) " -"โปรดตรวจสอบให้แน่ใจว่าหัวฉีดที่ติดตั้งตรงกับการตั้งค่าในเครื่องพิมพ์ " -"จากนั้นตั้งค่าเครื่องพิมพ์ล่วงหน้าที่เกี่ยวข้องขณะสไลซ์" +msgid "The nozzle flow setting of %s(%s) doesn't match with the slicing file(%s). Please make sure the nozzle installed matches with settings in printer, then set the corresponding printer preset while slicing." +msgstr "การตั้งค่าการไหลของหัวฉีดของ %s(%s) ไม่ตรงกับไฟล์การแบ่งส่วน (%s) โปรดตรวจสอบให้แน่ใจว่าหัวฉีดที่ติดตั้งตรงกับการตั้งค่าในเครื่องพิมพ์ จากนั้นตั้งค่าเครื่องพิมพ์ล่วงหน้าที่เกี่ยวข้องขณะสไลซ์" #, c-format, boost-format -msgid "" -"Filament %s does not match the filament in AMS slot %s. Please update the " -"printer firmware to support AMS slot assignment." -msgstr "" -"เส้นพลาสติก %s ไม่ตรงกับเส้นพลาสติกในช่อง AMS %s " -"โปรดอัปเดตเฟิร์มแวร์เครื่องพิมพ์เพื่อรองรับการกำหนดสล็อต AMS" +msgid "Filament %s does not match the filament in AMS slot %s. Please update the printer firmware to support AMS slot assignment." +msgstr "เส้นพลาสติก %s ไม่ตรงกับเส้นพลาสติกในช่อง AMS %s โปรดอัปเดตเฟิร์มแวร์เครื่องพิมพ์เพื่อรองรับการกำหนดสล็อต AMS" -msgid "" -"Filament does not match the filament in AMS slot. Please update the printer " -"firmware to support AMS slot assignment." -msgstr "" -"เส้นพลาสติกไม่ตรงกับเส้นพลาสติกในช่อง AMS " -"โปรดอัปเดตเฟิร์มแวร์เครื่องพิมพ์เพื่อรองรับการกำหนดสล็อต AMS" +msgid "Filament does not match the filament in AMS slot. Please update the printer firmware to support AMS slot assignment." +msgstr "เส้นพลาสติกไม่ตรงกับเส้นพลาสติกในช่อง AMS โปรดอัปเดตเฟิร์มแวร์เครื่องพิมพ์เพื่อรองรับการกำหนดสล็อต AMS" #, c-format, boost-format -msgid "" -"The selected printer (%s) is incompatible with the print file configuration " -"(%s). Please adjust the printer preset in the prepare page or choose a " -"compatible printer on this page." -msgstr "" -"เครื่องพิมพ์ที่เลือก (%s) เข้ากันไม่ได้กับการกำหนดค่าไฟล์การพิมพ์ (%s) " -"โปรดปรับการตั้งค่าล่วงหน้าของเครื่องพิมพ์ในหน้าจัดเตรียมหรือเลือกเครื่องพิมพ์ที่ใช้งานร่วมกันได้ในหน้านี้" +msgid "The selected printer (%s) is incompatible with the print file configuration (%s). Please adjust the printer preset in the prepare page or choose a compatible printer on this page." +msgstr "เครื่องพิมพ์ที่เลือก (%s) เข้ากันไม่ได้กับการกำหนดค่าไฟล์การพิมพ์ (%s) โปรดปรับการตั้งค่าล่วงหน้าของเครื่องพิมพ์ในหน้าจัดเตรียมหรือเลือกเครื่องพิมพ์ที่ใช้งานร่วมกันได้ในหน้านี้" -msgid "" -"When spiral vase mode is enabled, machines with I3 structure will not " -"generate timelapse videos." +msgid "When spiral vase mode is enabled, machines with I3 structure will not generate timelapse videos." msgstr "เมื่อเปิดใช้งานโหมดแจกันเกลียว เครื่องจักรที่มีโครงสร้าง I3 จะไม่สร้างวิดีโอไทม์แลปส์" -msgid "" -"The current printer does not support timelapse in Traditional Mode when " -"printing By-Object." +msgid "The current printer does not support timelapse in Traditional Mode when printing By-Object." msgstr "เครื่องพิมพ์ปัจจุบันไม่รองรับไทม์แลปส์ในโหมดดั้งเดิมเมื่อพิมพ์ By-Object" msgid "Errors" msgstr "ข้อผิดพลาด" -msgid "" -"More than one filament types have been mapped to the same external spool, " -"which may cause printing issues. The printer won't pause during printing." -msgstr "" -"มีการจับคู่เส้นพลาสติกมากกว่าหนึ่งประเภทกับแกนม้วนภายนอกเดียวกัน ซึ่งอาจทำให้เกิดปัญหาในการพิมพ์ " -"เครื่องพิมพ์จะไม่หยุดชั่วคราวระหว่างการพิมพ์" +msgid "More than one filament types have been mapped to the same external spool, which may cause printing issues. The printer won't pause during printing." +msgstr "มีการจับคู่เส้นพลาสติกมากกว่าหนึ่งประเภทกับแกนม้วนภายนอกเดียวกัน ซึ่งอาจทำให้เกิดปัญหาในการพิมพ์ เครื่องพิมพ์จะไม่หยุดชั่วคราวระหว่างการพิมพ์" -msgid "" -"The filament type setting of external spool is different from the filament " -"in the slicing file." +msgid "The filament type setting of external spool is different from the filament in the slicing file." msgstr "การตั้งค่าประเภทฟิลาเมนต์ของสปูลภายนอกแตกต่างจากฟิลาเมนต์ในไฟล์สไลซ์" -msgid "" -"The printer type selected when generating G-code is not consistent with the " -"currently selected printer. It is recommended that you use the same printer " -"type for slicing." -msgstr "" -"ประเภทเครื่องพิมพ์ที่เลือกเมื่อสร้าง G-code ไม่สอดคล้องกับเครื่องพิมพ์ที่เลือกในปัจจุบัน " -"ขอแนะนำให้คุณใช้เครื่องพิมพ์ประเภทเดียวกันในการตัด" +msgid "The printer type selected when generating G-code is not consistent with the currently selected printer. It is recommended that you use the same printer type for slicing." +msgstr "ประเภทเครื่องพิมพ์ที่เลือกเมื่อสร้าง G-code ไม่สอดคล้องกับเครื่องพิมพ์ที่เลือกในปัจจุบัน ขอแนะนำให้คุณใช้เครื่องพิมพ์ประเภทเดียวกันในการตัด" -msgid "" -"There are some unknown filaments in the AMS mappings. Please check whether " -"they are the required filaments. If they are okay, click \"Confirm\" to " -"start printing." -msgstr "" -"มีเส้นพลาสติกที่ไม่รู้จักบางอย่างในการแมป AMS โปรดตรวจสอบว่าเป็นเส้นพลาสติกที่ต้องการหรือไม่ " -"หากไม่เป็นไร ให้กด \"ยืนยัน\" เพื่อเริ่มพิมพ์" +msgid "There are some unknown filaments in the AMS mappings. Please check whether they are the required filaments. If they are okay, click \"Confirm\" to start printing." +msgstr "มีเส้นพลาสติกที่ไม่รู้จักบางอย่างในการแมป AMS โปรดตรวจสอบว่าเป็นเส้นพลาสติกที่ต้องการหรือไม่ หากไม่เป็นไร ให้กด \"ยืนยัน\" เพื่อเริ่มพิมพ์" msgid "Please check the following:" msgstr "กรุณาตรวจสอบสิ่งต่อไปนี้:" @@ -9402,17 +8665,13 @@ msgstr "กรุณาตรวจสอบสิ่งต่อไปนี้ msgid "Please fix the error above, otherwise printing cannot continue." msgstr "โปรดแก้ไขข้อผิดพลาดด้านบน ไม่เช่นนั้นการพิมพ์จะไม่สามารถดำเนินการต่อได้" -msgid "" -"Please click the confirm button if you still want to proceed with printing." +msgid "Please click the confirm button if you still want to proceed with printing." msgstr "โปรดคลิกปุ่มยืนยันหากคุณยังต้องการดำเนินการพิมพ์ต่อ" -msgid "" -"This checks the flatness of heatbed. Leveling makes extruded height uniform." +msgid "This checks the flatness of heatbed. Leveling makes extruded height uniform." msgstr "วิธีนี้จะตรวจสอบความเรียบของฐานพิมพ์ การปรับระดับทำให้ความสูงที่ฉีดเส้นออกมาสม่ำเสมอ" -msgid "" -"This process determines the dynamic flow values to improve overall print " -"quality." +msgid "This process determines the dynamic flow values to improve overall print quality." msgstr "กระบวนการนี้จะกำหนดค่าการไหลแบบไดนามิกเพื่อปรับปรุงคุณภาพการพิมพ์โดยรวม" msgid "Preparing print job" @@ -9431,45 +8690,23 @@ msgstr "หัวฉีด" msgid "both extruders" msgstr "ชุดดันเส้นทั้งสอง" -msgid "" -"Tips: If you changed your nozzle of your printer lately, Please go to " -"'Device -> Printer parts' to change your nozzle setting." -msgstr "" -"เคล็ดลับ: หากคุณเปลี่ยนหัวฉีดของเครื่องพิมพ์เมื่อเร็วๆ นี้ โปรดไปที่ 'อุปกรณ์ -> ชิ้นส่วนเครื่องพิมพ์' " -"เพื่อเปลี่ยนการตั้งค่าหัวฉีดของคุณ" +msgid "Tips: If you changed your nozzle of your printer lately, Please go to 'Device -> Printer parts' to change your nozzle setting." +msgstr "เคล็ดลับ: หากคุณเปลี่ยนหัวฉีดของเครื่องพิมพ์เมื่อเร็วๆ นี้ โปรดไปที่ 'อุปกรณ์ -> ชิ้นส่วนเครื่องพิมพ์' เพื่อเปลี่ยนการตั้งค่าหัวฉีดของคุณ" #, c-format, boost-format -msgid "" -"The %s diameter(%.1fmm) of current printer doesn't match with the slicing " -"file (%.1fmm). Please make sure the nozzle installed matches with settings " -"in printer, then set the corresponding printer preset when slicing." -msgstr "" -"เส้นผ่านศูนย์กลาง %s(%.1fmm) ของเครื่องพิมพ์ปัจจุบันไม่ตรงกับไฟล์การแบ่งส่วน (%.1fmm) " -"โปรดตรวจสอบให้แน่ใจว่าหัวฉีดที่ติดตั้งตรงกับการตั้งค่าในเครื่องพิมพ์ " -"จากนั้นตั้งค่าเครื่องพิมพ์ล่วงหน้าที่เกี่ยวข้องเมื่อสไลซ์" +msgid "The %s diameter(%.1fmm) of current printer doesn't match with the slicing file (%.1fmm). Please make sure the nozzle installed matches with settings in printer, then set the corresponding printer preset when slicing." +msgstr "เส้นผ่านศูนย์กลาง %s(%.1fmm) ของเครื่องพิมพ์ปัจจุบันไม่ตรงกับไฟล์การแบ่งส่วน (%.1fmm) โปรดตรวจสอบให้แน่ใจว่าหัวฉีดที่ติดตั้งตรงกับการตั้งค่าในเครื่องพิมพ์ จากนั้นตั้งค่าเครื่องพิมพ์ล่วงหน้าที่เกี่ยวข้องเมื่อสไลซ์" #, c-format, boost-format -msgid "" -"The current nozzle diameter (%.1fmm) doesn't match with the slicing file " -"(%.1fmm). Please make sure the nozzle installed matches with settings in " -"printer, then set the corresponding printer preset when slicing." -msgstr "" -"เส้นผ่านศูนย์กลางหัวฉีดปัจจุบัน (%.1fmm) ไม่ตรงกับไฟล์การแบ่งส่วน (%.1fmm) " -"โปรดตรวจสอบให้แน่ใจว่าหัวฉีดที่ติดตั้งตรงกับการตั้งค่าในเครื่องพิมพ์ " -"จากนั้นตั้งค่าเครื่องพิมพ์ล่วงหน้าที่เกี่ยวข้องเมื่อสไลซ์" +msgid "The current nozzle diameter (%.1fmm) doesn't match with the slicing file (%.1fmm). Please make sure the nozzle installed matches with settings in printer, then set the corresponding printer preset when slicing." +msgstr "เส้นผ่านศูนย์กลางหัวฉีดปัจจุบัน (%.1fmm) ไม่ตรงกับไฟล์การแบ่งส่วน (%.1fmm) โปรดตรวจสอบให้แน่ใจว่าหัวฉีดที่ติดตั้งตรงกับการตั้งค่าในเครื่องพิมพ์ จากนั้นตั้งค่าเครื่องพิมพ์ล่วงหน้าที่เกี่ยวข้องเมื่อสไลซ์" #, c-format, boost-format -msgid "" -"The hardness of current material (%s) exceeds the hardness of %s(%s). Please " -"verify the nozzle or material settings and try again." -msgstr "" -"ความแข็งของวัสดุปัจจุบัน (%s) เกินความแข็งของ %s(%s) " -"โปรดตรวจสอบการตั้งค่าหัวฉีดหรือวัสดุแล้วลองอีกครั้ง" +msgid "The hardness of current material (%s) exceeds the hardness of %s(%s). Please verify the nozzle or material settings and try again." +msgstr "ความแข็งของวัสดุปัจจุบัน (%s) เกินความแข็งของ %s(%s) โปรดตรวจสอบการตั้งค่าหัวฉีดหรือวัสดุแล้วลองอีกครั้ง" #, c-format, boost-format -msgid "" -"[ %s ] requires printing in a high-temperature environment. Please close the " -"door." +msgid "[ %s ] requires printing in a high-temperature environment. Please close the door." msgstr "[ %s ] จำเป็นต้องพิมพ์ในสภาพแวดล้อมที่มีอุณหภูมิสูง กรุณาปิดประตู." #, c-format, boost-format @@ -9484,9 +8721,7 @@ msgstr "เส้นพลาสติกบน %s อาจอ่อนตั msgid "The filament on %s is unknown and may soften. Please set filament." msgstr "ไม่ทราบเส้นพลาสติกบน %s และอาจอ่อนตัวลง กรุณาตั้งค่าเส้นพลาสติก" -msgid "" -"Unable to automatically match to suitable filament. Please click to manually " -"match." +msgid "Unable to automatically match to suitable filament. Please click to manually match." msgstr "ไม่สามารถจับคู่เส้นพลาสติกที่เหมาะสมได้โดยอัตโนมัติ กรุณาคลิกเพื่อจับคู่ด้วยตนเอง" msgid "Install toolhead enhanced cooling fan to prevent filament softening." @@ -9528,25 +8763,19 @@ msgstr "ไม่สามารถส่งงานพิมพ์ได้เ msgid "Cannot send a print job while the printer is updating firmware." msgstr "ไม่สามารถส่งงานพิมพ์ในขณะที่เครื่องพิมพ์กำลังอัปเดตเฟิร์มแวร์" -msgid "" -"The printer is executing instructions. Please restart printing after it ends." +msgid "The printer is executing instructions. Please restart printing after it ends." msgstr "เครื่องพิมพ์กำลังดำเนินการตามคำแนะนำ โปรดเริ่มการพิมพ์ใหม่หลังจากสิ้นสุด" msgid "AMS is setting up. Please try again later." msgstr "AMS กำลังจัดตั้ง โปรดลองอีกครั้งในภายหลัง" -msgid "" -"Not all filaments used in slicing are mapped to the printer. Please check " -"the mapping of filaments." -msgstr "" -"เส้นพลาสติกที่ใช้ในการตัดบางเส้นไม่ได้ถูกแมปกับเครื่องพิมพ์ โปรดตรวจสอบการแมปของเส้นพลาสติก" +msgid "Not all filaments used in slicing are mapped to the printer. Please check the mapping of filaments." +msgstr "เส้นพลาสติกที่ใช้ในการตัดบางเส้นไม่ได้ถูกแมปกับเครื่องพิมพ์ โปรดตรวจสอบการแมปของเส้นพลาสติก" msgid "Please do not mix-use the Ext with AMS." msgstr "กรุณาอย่าใช้ Ext ผสมกับ AMS" -msgid "" -"Invalid nozzle information, please refresh or manually set nozzle " -"information." +msgid "Invalid nozzle information, please refresh or manually set nozzle information." msgstr "ข้อมูลหัวฉีดไม่ถูกต้อง โปรดรีเฟรชหรือตั้งค่าข้อมูลหัวฉีดด้วยตนเอง" msgid "Storage needs to be inserted before printing via LAN." @@ -9567,43 +8796,23 @@ msgstr "ไม่สามารถส่งงานพิมพ์จานเ msgid "Storage needs to be inserted to record timelapse." msgstr "ต้องใส่ที่เก็บข้อมูลเพื่อบันทึกไทม์แลปส์" -msgid "" -"You have selected both external and AMS filaments for an extruder. You will " -"need to manually switch the external filament during printing." -msgstr "" -"คุณได้เลือกเส้นพลาสติกทั้งภายนอกและ AMS สำหรับชุดดันเส้น " -"คุณจะต้องสลับเส้นพลาสติกภายนอกด้วยตนเองระหว่างการพิมพ์" +msgid "You have selected both external and AMS filaments for an extruder. You will need to manually switch the external filament during printing." +msgstr "คุณได้เลือกเส้นพลาสติกทั้งภายนอกและ AMS สำหรับชุดดันเส้น คุณจะต้องสลับเส้นพลาสติกภายนอกด้วยตนเองระหว่างการพิมพ์" -msgid "" -"TPU 90A/TPU 85A is too soft and does not support automatic Flow Dynamics " -"calibration." +msgid "TPU 90A/TPU 85A is too soft and does not support automatic Flow Dynamics calibration." msgstr "TPU 90A/TPU 85A อ่อนเกินไปและไม่รองรับการปรับเทียบ Flow Dynamics อัตโนมัติ" -msgid "" -"Set dynamic flow calibration to 'OFF' to enable custom dynamic flow value." -msgstr "" -"ตั้งค่าการสอบเทียบโฟลว์แบบไดนามิกเป็น 'ปิด' เพื่อเปิดใช้งานค่าโฟลว์แบบไดนามิกที่กำหนดเอง" +msgid "Set dynamic flow calibration to 'OFF' to enable custom dynamic flow value." +msgstr "ตั้งค่าการสอบเทียบโฟลว์แบบไดนามิกเป็น 'ปิด' เพื่อเปิดใช้งานค่าโฟลว์แบบไดนามิกที่กำหนดเอง" msgid "This printer does not support printing all plates." msgstr "เครื่องพิมพ์นี้ไม่รองรับการพิมพ์เพลตทั้งหมด" -msgid "" -"The current firmware supports a maximum of 16 materials. You can either " -"reduce the number of materials to 16 or fewer on the Preparation Page, or " -"try updating the firmware. If you are still restricted after the update, " -"please wait for subsequent firmware support." -msgstr "" -"เฟิร์มแวร์ปัจจุบันรองรับวัสดุได้สูงสุด 16 รายการ คุณสามารถลดจำนวนวัสดุเหลือ 16 " -"ชิ้นหรือน้อยกว่านั้นได้ในหน้าการเตรียมการ หรือลองอัปเดตเฟิร์มแวร์ " -"หากคุณยังคงถูกจำกัดหลังจากการอัพเดต โปรดรอส่วนรองรับเฟิร์มแวร์ครั้งต่อไป" +msgid "The current firmware supports a maximum of 16 materials. You can either reduce the number of materials to 16 or fewer on the Preparation Page, or try updating the firmware. If you are still restricted after the update, please wait for subsequent firmware support." +msgstr "เฟิร์มแวร์ปัจจุบันรองรับวัสดุได้สูงสุด 16 รายการ คุณสามารถลดจำนวนวัสดุเหลือ 16 ชิ้นหรือน้อยกว่านั้นได้ในหน้าการเตรียมการ หรือลองอัปเดตเฟิร์มแวร์ หากคุณยังคงถูกจำกัดหลังจากการอัพเดต โปรดรอส่วนรองรับเฟิร์มแวร์ครั้งต่อไป" -msgid "" -"The type of external filament is unknown or does not match with the filament " -"type in the slicing file. Please make sure you have installed the correct " -"filament in the external spool." -msgstr "" -"ไม่ทราบประเภทของเส้นพลาสติกภายนอกหรือไม่ตรงกับประเภทเส้นพลาสติกในไฟล์สไลซ์ " -"โปรดตรวจสอบให้แน่ใจว่าคุณได้ติดตั้งเส้นพลาสติกที่ถูกต้องในแกนม้วนสายภายนอก" +msgid "The type of external filament is unknown or does not match with the filament type in the slicing file. Please make sure you have installed the correct filament in the external spool." +msgstr "ไม่ทราบประเภทของเส้นพลาสติกภายนอกหรือไม่ตรงกับประเภทเส้นพลาสติกในไฟล์สไลซ์ โปรดตรวจสอบให้แน่ใจว่าคุณได้ติดตั้งเส้นพลาสติกที่ถูกต้องในแกนม้วนสายภายนอก" msgid "Please refer to Wiki before use->" msgstr "โปรดดู Wiki ก่อนใช้งาน ->" @@ -9650,12 +8859,8 @@ msgstr "เครื่องพิมพ์ไม่รองรับการ msgid "Sending..." msgstr "กำลังส่ง..." -msgid "" -"File upload timed out. Please check if the firmware version supports this " -"operation or verify if the printer is functioning properly." -msgstr "" -"การอัปโหลดไฟล์หมดเวลา โปรดตรวจสอบว่าเวอร์ชันเฟิร์มแวร์รองรับการทำงานนี้หรือไม่ " -"หรือตรวจสอบว่าเครื่องพิมพ์ทำงานอย่างถูกต้องหรือไม่" +msgid "File upload timed out. Please check if the firmware version supports this operation or verify if the printer is functioning properly." +msgstr "การอัปโหลดไฟล์หมดเวลา โปรดตรวจสอบว่าเวอร์ชันเฟิร์มแวร์รองรับการทำงานนี้หรือไม่ หรือตรวจสอบว่าเครื่องพิมพ์ทำงานอย่างถูกต้องหรือไม่" msgid "Sending failed, please try again!" msgstr "ส่งไม่สำเร็จ กรุณาลองอีกครั้ง!" @@ -9730,18 +8935,8 @@ msgstr "อ่านและยอมรับ" msgid "Terms and Conditions" msgstr "ข้อกำหนดและเงื่อนไข" -msgid "" -"Thank you for purchasing a Bambu Lab device. Before using your Bambu Lab " -"device, please read the terms and conditions. By clicking to agree to use " -"your Bambu Lab device, you agree to abide by the Privacy Policy and Terms of " -"Use (collectively, the \"Terms\"). If you do not comply with or agree to the " -"Bambu Lab Privacy Policy, please do not use Bambu Lab equipment and services." -msgstr "" -"ขอขอบคุณที่ซื้ออุปกรณ์ Bambu Lab ก่อนใช้อุปกรณ์ Bambu Lab โปรดอ่านข้อกำหนดและเงื่อนไข " -"การคลิกเพื่อยอมรับการใช้อุปกรณ์ Bambu Lab ของคุณ " -"แสดงว่าคุณยินยอมที่จะปฏิบัติตามนโยบายความเป็นส่วนตัวและข้อกำหนดการใช้งาน (เรียกรวมกันว่า " -"\"ข้อกำหนด\") หากคุณไม่ปฏิบัติตามหรือยอมรับนโยบายความเป็นส่วนตัวของ Bambu Lab " -"โปรดอย่าใช้อุปกรณ์และบริการของ Bambu Lab" +msgid "Thank you for purchasing a Bambu Lab device. Before using your Bambu Lab device, please read the terms and conditions. By clicking to agree to use your Bambu Lab device, you agree to abide by the Privacy Policy and Terms of Use (collectively, the \"Terms\"). If you do not comply with or agree to the Bambu Lab Privacy Policy, please do not use Bambu Lab equipment and services." +msgstr "ขอขอบคุณที่ซื้ออุปกรณ์ Bambu Lab ก่อนใช้อุปกรณ์ Bambu Lab โปรดอ่านข้อกำหนดและเงื่อนไข การคลิกเพื่อยอมรับการใช้อุปกรณ์ Bambu Lab ของคุณ แสดงว่าคุณยินยอมที่จะปฏิบัติตามนโยบายความเป็นส่วนตัวและข้อกำหนดการใช้งาน (เรียกรวมกันว่า \"ข้อกำหนด\") หากคุณไม่ปฏิบัติตามหรือยอมรับนโยบายความเป็นส่วนตัวของ Bambu Lab โปรดอย่าใช้อุปกรณ์และบริการของ Bambu Lab" msgid "and" msgstr "และ" @@ -9756,29 +8951,8 @@ msgid "Statement about User Experience Improvement Program" msgstr "คำชี้แจงเกี่ยวกับโครงการปรับปรุงประสบการณ์ผู้ใช้" #, c-format, boost-format -msgid "" -"In the 3D Printing community, we learn from each other's successes and " -"failures to adjust our own slicing parameters and settings. %s follows the " -"same principle and uses machine learning to improve its performance from the " -"successes and failures of the vast number of prints by our users. We are " -"training %s to be smarter by feeding them the real-world data. If you are " -"willing, this service will access information from your error logs and usage " -"logs, which may include information described in Privacy Policy. We will not " -"collect any Personal Data by which an individual can be identified directly " -"or indirectly, including without limitation names, addresses, payment " -"information, or phone numbers. By enabling this service, you agree to these " -"terms and the statement about Privacy Policy." -msgstr "" -"ในชุมชนการพิมพ์ 3 มิติ " -"เราเรียนรู้จากความสำเร็จและความล้มเหลวของกันและกันในการปรับพารามิเตอร์และการตั้งค่าการแบ่งส่วนของเราเอง " -"%s " -"ปฏิบัติตามหลักการเดียวกันและใช้การเรียนรู้ของเครื่องเพื่อปรับปรุงประสิทธิภาพจากความสำเร็จและความล้มเหลวของงานพิมพ์จำนวนมากโดยผู้ใช้ของเรา " -"เรากำลังฝึก %s ให้ฉลาดขึ้นโดยป้อนข้อมูลจากโลกแห่งความเป็นจริงให้พวกเขา หากคุณเต็มใจ " -"บริการนี้จะเข้าถึงข้อมูลจากบันทึกข้อผิดพลาดและบันทึกการใช้งานของคุณ " -"ซึ่งอาจรวมถึงข้อมูลที่อธิบายไว้ในนโยบายความเป็นส่วนตัว เราจะไม่เก็บรวบรวมข้อมูลส่วนบุคคลใด ๆ " -"ที่สามารถระบุตัวบุคคลได้โดยตรงหรือโดยอ้อม รวมถึงแต่ไม่จำกัดเพียงชื่อ ที่อยู่ ข้อมูลการชำระเงิน " -"หรือหมายเลขโทรศัพท์ เมื่อเปิดใช้บริการนี้ " -"แสดงว่าคุณยอมรับข้อกำหนดเหล่านี้และคำชี้แจงเกี่ยวกับนโยบายความเป็นส่วนตัว" +msgid "In the 3D Printing community, we learn from each other's successes and failures to adjust our own slicing parameters and settings. %s follows the same principle and uses machine learning to improve its performance from the successes and failures of the vast number of prints by our users. We are training %s to be smarter by feeding them the real-world data. If you are willing, this service will access information from your error logs and usage logs, which may include information described in Privacy Policy. We will not collect any Personal Data by which an individual can be identified directly or indirectly, including without limitation names, addresses, payment information, or phone numbers. By enabling this service, you agree to these terms and the statement about Privacy Policy." +msgstr "ในชุมชนการพิมพ์ 3 มิติ เราเรียนรู้จากความสำเร็จและความล้มเหลวของกันและกันในการปรับพารามิเตอร์และการตั้งค่าการแบ่งส่วนของเราเอง %s ปฏิบัติตามหลักการเดียวกันและใช้การเรียนรู้ของเครื่องเพื่อปรับปรุงประสิทธิภาพจากความสำเร็จและความล้มเหลวของงานพิมพ์จำนวนมากโดยผู้ใช้ของเรา เรากำลังฝึก %s ให้ฉลาดขึ้นโดยป้อนข้อมูลจากโลกแห่งความเป็นจริงให้พวกเขา หากคุณเต็มใจ บริการนี้จะเข้าถึงข้อมูลจากบันทึกข้อผิดพลาดและบันทึกการใช้งานของคุณ ซึ่งอาจรวมถึงข้อมูลที่อธิบายไว้ในนโยบายความเป็นส่วนตัว เราจะไม่เก็บรวบรวมข้อมูลส่วนบุคคลใด ๆ ที่สามารถระบุตัวบุคคลได้โดยตรงหรือโดยอ้อม รวมถึงแต่ไม่จำกัดเพียงชื่อ ที่อยู่ ข้อมูลการชำระเงิน หรือหมายเลขโทรศัพท์ เมื่อเปิดใช้บริการนี้ แสดงว่าคุณยอมรับข้อกำหนดเหล่านี้และคำชี้แจงเกี่ยวกับนโยบายความเป็นส่วนตัว" msgid "Statement on User Experience Improvement Plan" msgstr "คำชี้แจงเกี่ยวกับแผนการปรับปรุงประสบการณ์ผู้ใช้" @@ -9815,48 +8989,23 @@ msgstr "ค้นหาในที่ตั้งไว้ล่วงหน้ msgid "Click to reset all settings to the last saved preset." msgstr "คลิกเพื่อรีเซ็ตการตั้งค่าทั้งหมดเป็นค่าที่ตั้งไว้ล่วงหน้าที่บันทึกไว้ล่าสุด" -msgid "" -"A prime tower is required for smooth timelapse mode. There may be flaws on " -"the model without a prime tower. Are you sure you want to disable the prime " -"tower?" -msgstr "" -"โหมดไทม์แลปส์แบบราบรื่นต้องใช้ไพรม์ทาวเวอร์ หากไม่มีไพรม์ทาวเวอร์อาจเกิดตำหนิบนโมเดลได้ " -"คุณแน่ใจหรือไม่ว่าต้องการปิดไพรม์ทาวเวอร์?" +msgid "A prime tower is required for smooth timelapse mode. There may be flaws on the model without a prime tower. Are you sure you want to disable the prime tower?" +msgstr "โหมดไทม์แลปส์แบบราบรื่นต้องใช้ไพรม์ทาวเวอร์ หากไม่มีไพรม์ทาวเวอร์อาจเกิดตำหนิบนโมเดลได้ คุณแน่ใจหรือไม่ว่าต้องการปิดไพรม์ทาวเวอร์?" -msgid "" -"A prime tower is required for clumping detection. There may be flaws on the " -"model without prime tower. Are you sure you want to disable prime tower?" -msgstr "" -"ต้องใช้ไพรม์ทาวเวอร์ในการตรวจจับการจับกันเป็นก้อน อาจมีตำหนิตรงรุ่นที่ไม่มีไพร์มทาวเวอร์ " -"คุณแน่ใจหรือไม่ว่าต้องการปิดการใช้งานไพร์มทาวเวอร์?" +msgid "A prime tower is required for clumping detection. There may be flaws on the model without prime tower. Are you sure you want to disable prime tower?" +msgstr "ต้องใช้ไพรม์ทาวเวอร์ในการตรวจจับการจับกันเป็นก้อน อาจมีตำหนิตรงรุ่นที่ไม่มีไพร์มทาวเวอร์ คุณแน่ใจหรือไม่ว่าต้องการปิดการใช้งานไพร์มทาวเวอร์?" -msgid "" -"Enabling both precise Z height and the prime tower may cause slicing errors. " -"Do you still want to enable?" -msgstr "" -"การเปิดใช้งานทั้งความสูง Z ที่แม่นยำและหอคอยหลักอาจทำให้เกิดข้อผิดพลาดในการแบ่งส่วน " -"คุณยังต้องการเปิดใช้งานหรือไม่?" +msgid "Enabling both precise Z height and the prime tower may cause slicing errors. Do you still want to enable?" +msgstr "การเปิดใช้งานทั้งความสูง Z ที่แม่นยำและหอคอยหลักอาจทำให้เกิดข้อผิดพลาดในการแบ่งส่วน คุณยังต้องการเปิดใช้งานหรือไม่?" -msgid "" -"A prime tower is required for clumping detection. There may be flaws on the " -"model without prime tower. Do you still want to enable clumping detection?" -msgstr "" -"ต้องใช้ไพรม์ทาวเวอร์ในการตรวจจับการจับกันเป็นก้อน อาจมีตำหนิตรงรุ่นที่ไม่มีไพร์มทาวเวอร์ " -"คุณยังต้องการเปิดใช้งานการตรวจจับการจับกันเป็นก้อนหรือไม่" +msgid "A prime tower is required for clumping detection. There may be flaws on the model without prime tower. Do you still want to enable clumping detection?" +msgstr "ต้องใช้ไพรม์ทาวเวอร์ในการตรวจจับการจับกันเป็นก้อน อาจมีตำหนิตรงรุ่นที่ไม่มีไพร์มทาวเวอร์ คุณยังต้องการเปิดใช้งานการตรวจจับการจับกันเป็นก้อนหรือไม่" -msgid "" -"Enabling both precise Z height and the prime tower may cause slicing errors. " -"Do you still want to enable precise Z height?" -msgstr "" -"การเปิดใช้งานทั้งความสูง Z ที่แม่นยำและหอคอยหลักอาจทำให้เกิดข้อผิดพลาดในการแบ่งส่วน " -"คุณยังต้องการเปิดใช้งานความสูง Z ที่แม่นยำหรือไม่" +msgid "Enabling both precise Z height and the prime tower may cause slicing errors. Do you still want to enable precise Z height?" +msgstr "การเปิดใช้งานทั้งความสูง Z ที่แม่นยำและหอคอยหลักอาจทำให้เกิดข้อผิดพลาดในการแบ่งส่วน คุณยังต้องการเปิดใช้งานความสูง Z ที่แม่นยำหรือไม่" -msgid "" -"A prime tower is required for smooth timelapse mode. There may be flaws on " -"the model without prime tower. Do you want to enable the prime tower?" -msgstr "" -"โหมดไทม์แลปส์แบบราบรื่นต้องใช้ไพรม์ทาวเวอร์ หากไม่มีไพรม์ทาวเวอร์อาจเกิดตำหนิบนโมเดลได้ " -"ต้องการเปิดใช้ไพรม์ทาวเวอร์หรือไม่?" +msgid "A prime tower is required for smooth timelapse mode. There may be flaws on the model without prime tower. Do you want to enable the prime tower?" +msgstr "โหมดไทม์แลปส์แบบราบรื่นต้องใช้ไพรม์ทาวเวอร์ หากไม่มีไพรม์ทาวเวอร์อาจเกิดตำหนิบนโมเดลได้ ต้องการเปิดใช้ไพรม์ทาวเวอร์หรือไม่?" msgid "Still print by object?" msgstr "ยังคงพิมพ์ตามวัตถุใช่ไหม" @@ -9869,14 +9018,11 @@ msgstr "" "คุณแน่ใจหรือว่าจะใช้พวกมันเป็นฐานส่วนรองรับ?\n" msgid "" -"When using support material for the support interface, we recommend the " -"following settings:\n" -"0 top Z distance, 0 interface spacing, interlaced rectilinear pattern and " -"disable independent support layer height." +"When using support material for the support interface, we recommend the following settings:\n" +"0 top Z distance, 0 interface spacing, interlaced rectilinear pattern and disable independent support layer height." msgstr "" "เมื่อใช้สื่อส่วนรองรับสำหรับอินเทอร์เฟซส่วนรองรับ เราขอแนะนำการตั้งค่าต่อไปนี้:\n" -"0 ระยะทาง Z บนสุด, ระยะห่างระหว่างอินเทอร์เฟซ 0, รูปแบบเส้นตรงแบบอินเทอร์เลซ " -"และปิดใช้งานความสูงของเลเยอร์รองรับอิสระ" +"0 ระยะทาง Z บนสุด, ระยะห่างระหว่างอินเทอร์เฟซ 0, รูปแบบเส้นตรงแบบอินเทอร์เลซ และปิดใช้งานความสูงของเลเยอร์รองรับอิสระ" msgid "" "Change these settings automatically?\n" @@ -9888,41 +9034,22 @@ msgstr "" "ไม่ - อย่าเปลี่ยนการตั้งค่าเหล่านี้ให้ฉัน" msgid "" -"When using soluble material for the support interface, we recommend the " -"following settings:\n" -"0 top Z distance, 0 interface spacing, interlaced rectilinear pattern, " -"disable independent support layer height\n" +"When using soluble material for the support interface, we recommend the following settings:\n" +"0 top Z distance, 0 interface spacing, interlaced rectilinear pattern, disable independent support layer height\n" "and use soluble materials for both support interface and support base." msgstr "" "เมื่อใช้วัสดุที่ละลายน้ำได้สำหรับอินเทอร์เฟซส่วนรองรับ เราขอแนะนำการตั้งค่าต่อไปนี้:\n" -"0 ระยะทาง Z บนสุด, ระยะห่างระหว่างอินเทอร์เฟซ 0, รูปแบบเส้นตรงแบบอินเทอร์เลซ, " -"ปิดใช้งานความสูงของเลเยอร์รองรับอิสระ\n" +"0 ระยะทาง Z บนสุด, ระยะห่างระหว่างอินเทอร์เฟซ 0, รูปแบบเส้นตรงแบบอินเทอร์เลซ, ปิดใช้งานความสูงของเลเยอร์รองรับอิสระ\n" "และใช้วัสดุที่ละลายน้ำได้สำหรับทั้งส่วนต่อประสานและฐานรองรับ" -msgid "" -"Enabling this option will modify the model's shape. If your print requires " -"precise dimensions or is part of an assembly, it's important to double-check " -"whether this change in geometry impacts the functionality of your print." -msgstr "" -"การเปิดใช้งานตัวเลือกนี้จะปรับเปลี่ยนรูปร่างของแบบจำลอง " -"หากการพิมพ์ของคุณต้องการขนาดที่แม่นยำหรือเป็นส่วนหนึ่งของชุดประกอบ " -"สิ่งสำคัญคือต้องตรวจสอบอีกครั้งว่าการเปลี่ยนแปลงทางเรขาคณิตนี้ส่งผลต่อการทำงานของการพิมพ์ของคุณหรือไม่" +msgid "Enabling this option will modify the model's shape. If your print requires precise dimensions or is part of an assembly, it's important to double-check whether this change in geometry impacts the functionality of your print." +msgstr "การเปิดใช้งานตัวเลือกนี้จะปรับเปลี่ยนรูปร่างของแบบจำลอง หากการพิมพ์ของคุณต้องการขนาดที่แม่นยำหรือเป็นส่วนหนึ่งของชุดประกอบ สิ่งสำคัญคือต้องตรวจสอบอีกครั้งว่าการเปลี่ยนแปลงทางเรขาคณิตนี้ส่งผลต่อการทำงานของการพิมพ์ของคุณหรือไม่" msgid "Are you sure you want to enable this option?" msgstr "คุณแน่ใจหรือไม่ว่าต้องการเปิดใช้งานตัวเลือกนี้" -msgid "" -"Infill patterns are typically designed to handle rotation automatically to " -"ensure proper printing and achieve their intended effects (e.g., Gyroid, " -"Cubic). Rotating the current sparse infill pattern may lead to insufficient " -"support. Please proceed with caution and thoroughly check for any potential " -"printing issues. Are you sure you want to enable this option?" -msgstr "" -"โดยทั่วไปรูปแบบไส้ในได้รับการออกแบบให้รองรับการหมุนโดยอัตโนมัติเพื่อให้แน่ใจว่าการพิมพ์ถูกต้องและบรรลุผลตามที่ต้องการ " -"(เช่น Gyroid, ลูกบาศก์) การหมุนรูปแบบ ไส้ใน " -"แบบกระจัดกระจายในปัจจุบันอาจทำให้ส่วนรองรับไม่เพียงพอ " -"โปรดดำเนินการด้วยความระมัดระวังและตรวจสอบปัญหาการพิมพ์ที่อาจเกิดขึ้นอย่างละเอียด " -"คุณแน่ใจหรือไม่ว่าต้องการเปิดใช้งานตัวเลือกนี้" +msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" +msgstr "โดยทั่วไปรูปแบบไส้ในได้รับการออกแบบให้รองรับการหมุนโดยอัตโนมัติเพื่อให้แน่ใจว่าการพิมพ์ถูกต้องและบรรลุผลตามที่ต้องการ (เช่น Gyroid, ลูกบาศก์) การหมุนรูปแบบ ไส้ใน แบบกระจัดกระจายในปัจจุบันอาจทำให้ส่วนรองรับไม่เพียงพอ โปรดดำเนินการด้วยความระมัดระวังและตรวจสอบปัญหาการพิมพ์ที่อาจเกิดขึ้นอย่างละเอียด คุณแน่ใจหรือไม่ว่าต้องการเปิดใช้งานตัวเลือกนี้" msgid "" "Layer height is too small.\n" @@ -9931,12 +9058,8 @@ msgstr "" "ความสูงของเลเยอร์น้อยเกินไป\n" "มันจะตั้งค่าเป็น min_layer_height\n" -msgid "" -"Layer height exceeds the limit in Printer Settings -> Extruder -> Layer " -"height limits, this may cause printing quality issues." -msgstr "" -"ความสูงของเลเยอร์เกินขีดจำกัดในการตั้งค่าเครื่องพิมพ์ -> ชุดดันเส้น -> " -"ขีดจำกัดความสูงของเลเยอร์ ซึ่งอาจทำให้เกิดปัญหาคุณภาพการพิมพ์" +msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "ความสูงของเลเยอร์เกินขีดจำกัดในการตั้งค่าเครื่องพิมพ์ -> ชุดดันเส้น -> ขีดจำกัดความสูงของเลเยอร์ ซึ่งอาจทำให้เกิดปัญหาคุณภาพการพิมพ์" msgid "Adjust to the set range automatically?\n" msgstr "ปรับเป็นช่วงที่ตั้งไว้อัตโนมัติ?\n" @@ -9947,47 +9070,23 @@ msgstr "ปรับ" msgid "Ignore" msgstr "ละเว้น" -msgid "" -"Experimental feature: Retracting and cutting off the filament at a greater " -"distance during filament changes to minimize flush. Although it can notably " -"reduce flush, it may also elevate the risk of nozzle clogs or other printing " -"complications." -msgstr "" -"คุณลักษณะการทดลอง: " -"การดึงกลับและตัดเส้นพลาสติกออกในระยะห่างที่มากขึ้นระหว่างการเปลี่ยนเส้นพลาสติกเพื่อลดการไล่เส้น " -"แม้ว่าจะสามารถลดการไล่เส้นได้อย่างเห็นได้ชัด " -"แต่ก็อาจเพิ่มความเสี่ยงของการอุดตันของหัวฉีดหรือภาวะแทรกซ้อนในการพิมพ์อื่นๆ อีกด้วย" +msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." +msgstr "คุณลักษณะการทดลอง: การดึงกลับและตัดเส้นพลาสติกออกในระยะห่างที่มากขึ้นระหว่างการเปลี่ยนเส้นพลาสติกเพื่อลดการไล่เส้น แม้ว่าจะสามารถลดการไล่เส้นได้อย่างเห็นได้ชัด แต่ก็อาจเพิ่มความเสี่ยงของการอุดตันของหัวฉีดหรือภาวะแทรกซ้อนในการพิมพ์อื่นๆ อีกด้วย" + +msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications. Please use with the latest printer firmware." +msgstr "คุณลักษณะการทดลอง: การดึงกลับและตัดเส้นพลาสติกออกในระยะห่างที่มากขึ้นระหว่างการเปลี่ยนเส้นพลาสติกเพื่อลดการไล่เส้น แม้ว่าจะสามารถลดการไล่เส้นได้อย่างเห็นได้ชัด แต่ก็อาจเพิ่มความเสี่ยงของการอุดตันของหัวฉีดหรือภาวะแทรกซ้อนในการพิมพ์อื่นๆ อีกด้วย กรุณาใช้กับเฟิร์มแวร์เครื่องพิมพ์ล่าสุด" msgid "" -"Experimental feature: Retracting and cutting off the filament at a greater " -"distance during filament changes to minimize flush. Although it can notably " -"reduce flush, it may also elevate the risk of nozzle clogs or other printing " -"complications. Please use with the latest printer firmware." -msgstr "" -"คุณลักษณะการทดลอง: " -"การดึงกลับและตัดเส้นพลาสติกออกในระยะห่างที่มากขึ้นระหว่างการเปลี่ยนเส้นพลาสติกเพื่อลดการไล่เส้น " -"แม้ว่าจะสามารถลดการไล่เส้นได้อย่างเห็นได้ชัด " -"แต่ก็อาจเพิ่มความเสี่ยงของการอุดตันของหัวฉีดหรือภาวะแทรกซ้อนในการพิมพ์อื่นๆ อีกด้วย " -"กรุณาใช้กับเฟิร์มแวร์เครื่องพิมพ์ล่าสุด" - -msgid "" -"When recording timelapse without toolhead, it is recommended to add a " -"\"Timelapse Wipe Tower\" \n" -"by right-click the empty position of build plate and choose \"Add Primitive" -"\"->\"Timelapse Wipe Tower\"." +"When recording timelapse without toolhead, it is recommended to add a \"Timelapse Wipe Tower\" \n" +"by right-click the empty position of build plate and choose \"Add Primitive\"->\"Timelapse Wipe Tower\"." msgstr "" "เมื่อบันทึกไทม์แลปส์โดยไม่มีหัวเครื่องมือ แนะนำให้เพิ่ม \"Timelapse Wipe Tower\" \n" -"โดยคลิกขวาที่ตำแหน่งว่างของแผ่นงานแล้วเลือก \"Add Primitive\"->\"Timelapse Wipe Tower" -"\"" +"โดยคลิกขวาที่ตำแหน่งว่างของแผ่นงานแล้วเลือก \"Add Primitive\"->\"Timelapse Wipe Tower\"" -msgid "" -"A copy of the current system preset will be created, which will be detached " -"from the system preset." -msgstr "" -"สำเนาของค่าที่ตั้งล่วงหน้าของระบบปัจจุบันจะถูกสร้างขึ้น ซึ่งจะแยกออกจากค่าที่ตั้งล่วงหน้าของระบบ" +msgid "A copy of the current system preset will be created, which will be detached from the system preset." +msgstr "สำเนาของค่าที่ตั้งล่วงหน้าของระบบปัจจุบันจะถูกสร้างขึ้น ซึ่งจะแยกออกจากค่าที่ตั้งล่วงหน้าของระบบ" -msgid "" -"The current custom preset will be detached from the parent system preset." +msgid "The current custom preset will be detached from the parent system preset." msgstr "ค่าที่ตั้งไว้ล่วงหน้าแบบกำหนดเองปัจจุบันจะแยกออกจากค่าที่ตั้งไว้ล่วงหน้าของระบบหลัก" msgid "Modifications to the current profile will be saved." @@ -10018,8 +9117,7 @@ msgstr "ค่าที่ตั้งล่วงหน้าปัจจุบ msgid "It can't be deleted or modified." msgstr "ไม่สามารถลบหรือแก้ไขได้" -msgid "" -"Any modifications should be saved as a new preset inherited from this one." +msgid "Any modifications should be saved as a new preset inherited from this one." msgstr "การแก้ไขใด ๆ ควรได้รับการบันทึกเป็นค่าที่ตั้งไว้ล่วงหน้าใหม่ซึ่งสืบทอดมาจากค่านี้" msgid "To do that please specify a new name for the preset." @@ -10085,13 +9183,8 @@ msgstr "ความเร็วชั้นอื่นๆ" msgid "Overhang speed" msgstr "ความเร็วส่วนยื่น" -msgid "" -"This is the speed for various overhang degrees. Overhang degrees are " -"expressed as a percentage of line width. 0 speed means no slowing down for " -"the overhang degree range and wall speed is used" -msgstr "" -"นี่คือความเร็วขององศาส่วนยื่นต่างๆ องศาที่ยื่นออกมาจะแสดงเป็นเปอร์เซ็นต์ของความกว้างของเส้น " -"ความเร็ว 0 หมายถึงไม่มีการชะลอความเร็วสำหรับช่วงองศายื่นและใช้ความเร็วของผนัง" +msgid "This is the speed for various overhang degrees. Overhang degrees are expressed as a percentage of line width. 0 speed means no slowing down for the overhang degree range and wall speed is used" +msgstr "นี่คือความเร็วขององศาส่วนยื่นต่างๆ องศาที่ยื่นออกมาจะแสดงเป็นเปอร์เซ็นต์ของความกว้างของเส้น ความเร็ว 0 หมายถึงไม่มีการชะลอความเร็วสำหรับช่วงองศายื่นและใช้ความเร็วของผนัง" msgid "Set speed for external and internal bridges" msgstr "ตั้งค่าความเร็วสำหรับบริดจ์ภายนอกและภายใน" @@ -10147,12 +9240,10 @@ msgstr "บ่อย" #, c-format, boost-format msgid "" "Following line %s contains reserved keywords.\n" -"Please remove it, or G-code visualization and print time estimation will be " -"broken." +"Please remove it, or G-code visualization and print time estimation will be broken." msgid_plural "" "Following lines %s contain reserved keywords.\n" -"Please remove them, or G-code visualization and print time estimation will " -"be broken." +"Please remove them, or G-code visualization and print time estimation will be broken." msgstr[0] "" "บรรทัดถัดไป %s มีคีย์เวิร์ดที่สงวนไว้\n" "กรุณาลบออก มิฉะนั้นการแสดงผล G-code และการประมาณเวลาพิมพ์จะทำงานผิดพลาด" @@ -10184,59 +9275,32 @@ msgstr "อุณหภูมิพิมพ์" msgid "Nozzle temperature when printing" msgstr "อุณหภูมิหัวฉีดเมื่อพิมพ์" -msgid "" -"Bed temperature when the Cool Plate SuperTack is installed. A value of 0 " -"means the filament does not support printing on the Cool Plate SuperTack." -msgstr "" -"อุณหภูมิฐานพิมพ์เมื่อติดตั้ง Cool Plate SuperTack ค่า 0 " -"หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บน Cool Plate SuperTack" +msgid "Bed temperature when the Cool Plate SuperTack is installed. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." +msgstr "อุณหภูมิฐานพิมพ์เมื่อติดตั้ง Cool Plate SuperTack ค่า 0 หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บน Cool Plate SuperTack" msgid "Cool Plate" msgstr "จานเย็น" -msgid "" -"This is the bed temperature when the Cool Plate is installed. A value of 0 " -"means the filament does not support printing on the Cool Plate." -msgstr "" -"อุณหภูมิฐานพิมพ์เมื่อติดตั้ง Cool Plate ค่า 0 หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บน Cool " -"Plate" +msgid "This is the bed temperature when the Cool Plate is installed. A value of 0 means the filament does not support printing on the Cool Plate." +msgstr "อุณหภูมิฐานพิมพ์เมื่อติดตั้ง Cool Plate ค่า 0 หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บน Cool Plate" msgid "Textured Cool Plate" msgstr "เท็กซ์เจอร์ คูลเพลท" -msgid "" -"This is the bed temperature when the Textured Cool Plate is installed. A " -"value of 0 means the filament does not support printing on the Textured Cool " -"Plate." -msgstr "" -"อุณหภูมิฐานพิมพ์เมื่อติดตั้ง Textured Cool Plate ค่า 0 " -"หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บน Textured Cool Plate" +msgid "This is the bed temperature when the Textured Cool Plate is installed. A value of 0 means the filament does not support printing on the Textured Cool Plate." +msgstr "อุณหภูมิฐานพิมพ์เมื่อติดตั้ง Textured Cool Plate ค่า 0 หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บน Textured Cool Plate" -msgid "" -"This is the bed temperature when the engineering plate is installed. A value " -"of 0 means the filament does not support printing on the Engineering Plate." -msgstr "" -"อุณหภูมิฐานพิมพ์เมื่อติดตั้ง Engineering Plate ค่า 0 หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บน " -"Engineering Plate" +msgid "This is the bed temperature when the engineering plate is installed. A value of 0 means the filament does not support printing on the Engineering Plate." +msgstr "อุณหภูมิฐานพิมพ์เมื่อติดตั้ง Engineering Plate ค่า 0 หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บน Engineering Plate" msgid "Smooth PEI Plate / High Temp Plate" msgstr "แผ่น PEI เรียบ / แผ่นอุณหภูมิสูง" -msgid "" -"This is the bed temperature when the Smooth PEI Plate/High Temperature Plate " -"is installed. A value of 0 means the filament does not support printing on " -"the Smooth PEI Plate/High Temp Plate." -msgstr "" -"อุณหภูมิฐานพิมพ์เมื่อติดตั้งแผ่น Smooth PEI/แผ่นอุณหภูมิสูง ค่า 0 " -"หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บนแผ่น PEI แบบเรียบ/แผ่นอุณหภูมิสูง" +msgid "This is the bed temperature when the Smooth PEI Plate/High Temperature Plate is installed. A value of 0 means the filament does not support printing on the Smooth PEI Plate/High Temp Plate." +msgstr "อุณหภูมิฐานพิมพ์เมื่อติดตั้งแผ่น Smooth PEI/แผ่นอุณหภูมิสูง ค่า 0 หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บนแผ่น PEI แบบเรียบ/แผ่นอุณหภูมิสูง" -msgid "" -"This is the bed temperature when the Textured PEI Plate is installed. A " -"value of 0 means the filament does not support printing on the Textured PEI " -"Plate." -msgstr "" -"อุณหภูมิฐานพิมพ์เมื่อติดตั้งเพลต PEI แบบมีพื้นผิว ค่า 0 " -"หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บนเพลต PEI ที่มีพื้นผิว" +msgid "This is the bed temperature when the Textured PEI Plate is installed. A value of 0 means the filament does not support printing on the Textured PEI Plate." +msgstr "อุณหภูมิฐานพิมพ์เมื่อติดตั้งเพลต PEI แบบมีพื้นผิว ค่า 0 หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บนเพลต PEI ที่มีพื้นผิว" msgid "Volumetric speed limitation" msgstr "การจำกัดความเร็วตามปริมาตร" @@ -10250,22 +9314,13 @@ msgstr "พัดลมระบายความร้อนบางส่ว msgid "Min fan speed threshold" msgstr "เกณฑ์ความเร็วพัดลมขั้นต่ำ" -msgid "" -"The part cooling fan will run at the minimum fan speed when the estimated " -"layer time is longer than the threshold value. When the layer time is " -"shorter than the threshold, the fan speed will be interpolated between the " -"minimum and maximum fan speed according to layer printing time." -msgstr "" -"พัดลมระบายความร้อนจะทำงานที่ความเร็วต่ำสุดเมื่อเวลาเลเยอร์โดยประมาณยาวกว่าเกณฑ์ " -"เมื่อเวลาเลเยอร์สั้นกว่าเกณฑ์ " -"ความเร็วพัดลมจะถูกปรับระหว่างค่าต่ำสุดและสูงสุดตามเวลาพิมพ์ของเลเยอร์" +msgid "The part cooling fan will run at the minimum fan speed when the estimated layer time is longer than the threshold value. When the layer time is shorter than the threshold, the fan speed will be interpolated between the minimum and maximum fan speed according to layer printing time." +msgstr "พัดลมระบายความร้อนจะทำงานที่ความเร็วต่ำสุดเมื่อเวลาเลเยอร์โดยประมาณยาวกว่าเกณฑ์ เมื่อเวลาเลเยอร์สั้นกว่าเกณฑ์ ความเร็วพัดลมจะถูกปรับระหว่างค่าต่ำสุดและสูงสุดตามเวลาพิมพ์ของเลเยอร์" msgid "Max fan speed threshold" msgstr "เกณฑ์ความเร็วพัดลมสูงสุด" -msgid "" -"The part cooling fan will run at maximum speed when the estimated layer time " -"is shorter than the threshold value." +msgid "The part cooling fan will run at maximum speed when the estimated layer time is shorter than the threshold value." msgstr "ความเร็วพัดลมระบายความร้อนของชิ้นส่วนจะสูงสุดเมื่อเวลาเลเยอร์โดยประมาณสั้นกว่าค่าที่ตั้งไว้" msgid "Auxiliary part cooling fan" @@ -10387,9 +9442,7 @@ msgstr "ความเร็วการหลีกเลี่ยงการ msgid "Frequency" msgstr "ความถี่" -msgid "" -"The frequency of the anti-vibration signal will correspond to the natural " -"frequency of the frame." +msgid "The frequency of the anti-vibration signal will correspond to the natural frequency of the frame." msgstr "ความถี่ของสัญญาณป้องกันการสั่นสะเทือนจะสอดคล้องกับความถี่ธรรมชาติของเฟรม" msgid "Damping" @@ -10416,8 +9469,7 @@ msgstr "จำนวนชุดดันเส้นของเครื่อ msgid "" "Single Extruder Multi Material is selected,\n" "and all extruders must have the same diameter.\n" -"Do you want to change the diameter for all extruders to first extruder " -"nozzle diameter value?" +"Do you want to change the diameter for all extruders to first extruder nozzle diameter value?" msgstr "" "เลือกวัสดุหลายชุดดันเส้นเดี่ยว\n" "และชุดดันเส้นทั้งหมดจะต้องมีเส้นผ่านศูนย์กลางเท่ากัน\n" @@ -10432,12 +9484,8 @@ msgstr "ทาวเวอร์เช็ดหัวฉีด" msgid "Single extruder multi-material parameters" msgstr "พารามิเตอร์วัสดุหลายชุดดันเส้นเดี่ยว" -msgid "" -"This is a single extruder multi-material printer, diameters of all extruders " -"will be set to the new value. Do you want to proceed?" -msgstr "" -"นี่คือเครื่องพิมพ์หลายวัสดุที่ใช้ชุดดันเส้นเดี่ยว " -"โดยเส้นผ่านศูนย์กลางของชุดดันเส้นทั้งหมดจะถูกตั้งค่าเป็นค่าใหม่ คุณต้องการดำเนินการต่อหรือไม่?" +msgid "This is a single extruder multi-material printer, diameters of all extruders will be set to the new value. Do you want to proceed?" +msgstr "นี่คือเครื่องพิมพ์หลายวัสดุที่ใช้ชุดดันเส้นเดี่ยว โดยเส้นผ่านศูนย์กลางของชุดดันเส้นทั้งหมดจะถูกตั้งค่าเป็นค่าใหม่ คุณต้องการดำเนินการต่อหรือไม่?" msgid "Layer height limits" msgstr "การจำกัดความสูงของเลเยอร์" @@ -10449,8 +9497,7 @@ msgid "Retraction when switching material" msgstr "การร่นกลับเมื่อเปลี่ยนวัสดุ" msgid "" -"The Retract before wipe option could be only 100% when using the Firmware " -"Retraction mode.\n" +"The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" "Shall I set it to 100% in order to enable Firmware Retraction?" msgstr "" @@ -10461,9 +9508,7 @@ msgstr "" msgid "Firmware Retraction" msgstr "การเพิกถอนเฟิร์มแวร์" -msgid "" -"Switching to a printer with different extruder types or numbers will discard " -"or reset changes to extruder or multi-nozzle-related parameters." +msgid "Switching to a printer with different extruder types or numbers will discard or reset changes to extruder or multi-nozzle-related parameters." msgstr "การเปลี่ยนไปใช้เครื่องพิมพ์ที่มีประเภทหรือหมายเลขชุดดันเส้นที่แตกต่างกันจะยกเลิกหรือรีเซ็ตการเปลี่ยนแปลงในชุดดันเส้นหรือพารามิเตอร์ที่เกี่ยวข้องกับหัวฉีดหลายตัว" msgid "Use Modified Value" @@ -10473,12 +9518,8 @@ msgid "Detached" msgstr "เดี่ยว" #, c-format, boost-format -msgid "" -"%d Filament Preset and %d Process Preset is attached to this printer. Those " -"presets would be deleted if the printer is deleted." -msgstr "" -"%d เส้นพลาสติก Preset และ %d Process Preset ถูกแนบไปกับเครื่องพิมพ์นี้ " -"ค่าที่ตั้งล่วงหน้าเหล่านั้นจะถูกลบหากเครื่องพิมพ์ถูกลบ" +msgid "%d Filament Preset and %d Process Preset is attached to this printer. Those presets would be deleted if the printer is deleted." +msgstr "%d เส้นพลาสติก Preset และ %d Process Preset ถูกแนบไปกับเครื่องพิมพ์นี้ ค่าที่ตั้งล่วงหน้าเหล่านั้นจะถูกลบหากเครื่องพิมพ์ถูกลบ" msgid "Presets inherited by other presets cannot be deleted!" msgstr "ค่าที่ตั้งล่วงหน้าที่สืบทอดมาจากค่าที่ตั้งล่วงหน้าอื่นไม่สามารถลบได้!" @@ -10498,12 +9539,10 @@ msgstr[0] "พรีเซ็ตต่อไปนี้จะถูกลบด msgid "" "Are you sure to delete the selected preset?\n" -"If the preset corresponds to a filament currently in use on your printer, " -"please reset the filament information for that slot." +"If the preset corresponds to a filament currently in use on your printer, please reset the filament information for that slot." msgstr "" "คุณแน่ใจหรือไม่ว่าต้องการลบค่าที่ตั้งล่วงหน้าที่เลือกไว้\n" -"หากค่าที่ตั้งไว้ล่วงหน้าสอดคล้องกับเส้นพลาสติกที่ใช้งานอยู่ในเครื่องพิมพ์ของคุณ " -"โปรดรีเซ็ตข้อมูลเส้นพลาสติกสำหรับช่องนั้น" +"หากค่าที่ตั้งไว้ล่วงหน้าสอดคล้องกับเส้นพลาสติกที่ใช้งานอยู่ในเครื่องพิมพ์ของคุณ โปรดรีเซ็ตข้อมูลเส้นพลาสติกสำหรับช่องนั้น" #, boost-format msgid "Are you sure you want to %1% the selected preset?" @@ -10519,8 +9558,7 @@ msgstr "" " %s ชั้นแรก %d %s, ชั้นอื่นๆ %d %s\n" " %s เดลต้าสูงสุด %d %s เดลต้าปัจจุบัน %d %s\n" -msgid "" -"Some first-layer and other-layer temperature pairs exceed safety limits.\n" +msgid "Some first-layer and other-layer temperature pairs exceed safety limits.\n" msgstr "คู่อุณหภูมิชั้นหนึ่งและชั้นอื่นๆ บางคู่เกินขีดจำกัดความปลอดภัย\n" msgid "" @@ -10668,20 +9706,12 @@ msgid "Preset \"%1%\" contains the following unsaved changes:" msgstr "ค่าที่ตั้งไว้ล่วงหน้า \"%1%\" มีการเปลี่ยนแปลงที่ยังไม่ได้บันทึกต่อไปนี้:" #, boost-format -msgid "" -"Preset \"%1%\" is not compatible with the new printer profile and it " -"contains the following unsaved changes:" -msgstr "" -"ค่าที่ตั้งไว้ล่วงหน้า \"%1%\" เข้ากันไม่ได้กับโปรไฟล์เครื่องพิมพ์ใหม่ " -"และประกอบด้วยการเปลี่ยนแปลงที่ยังไม่ได้บันทึกต่อไปนี้:" +msgid "Preset \"%1%\" is not compatible with the new printer profile and it contains the following unsaved changes:" +msgstr "ค่าที่ตั้งไว้ล่วงหน้า \"%1%\" เข้ากันไม่ได้กับโปรไฟล์เครื่องพิมพ์ใหม่ และประกอบด้วยการเปลี่ยนแปลงที่ยังไม่ได้บันทึกต่อไปนี้:" #, boost-format -msgid "" -"Preset \"%1%\" is not compatible with the new process profile and it " -"contains the following unsaved changes:" -msgstr "" -"ค่าที่ตั้งไว้ล่วงหน้า \"%1%\" เข้ากันไม่ได้กับโปรไฟล์กระบวนการใหม่ " -"และประกอบด้วยการเปลี่ยนแปลงที่ยังไม่ได้บันทึกต่อไปนี้:" +msgid "Preset \"%1%\" is not compatible with the new process profile and it contains the following unsaved changes:" +msgstr "ค่าที่ตั้งไว้ล่วงหน้า \"%1%\" เข้ากันไม่ได้กับโปรไฟล์กระบวนการใหม่ และประกอบด้วยการเปลี่ยนแปลงที่ยังไม่ได้บันทึกต่อไปนี้:" #, boost-format msgid "You have changed some settings of preset \"%1%\"." @@ -10696,20 +9726,17 @@ msgstr "" msgid "" "\n" -"You can save or discard the preset values you have modified, or choose to " -"transfer the values you have modified to the new preset." +"You can save or discard the preset values you have modified, or choose to transfer the values you have modified to the new preset." msgstr "" "\n" -"คุณสามารถบันทึกหรือละทิ้งค่าที่ตั้งไว้ล่วงหน้าที่คุณได้แก้ไข " -"หรือเลือกที่จะถ่ายโอนค่าที่คุณได้แก้ไขไปยังค่าที่ตั้งไว้ล่วงหน้าใหม่" +"คุณสามารถบันทึกหรือละทิ้งค่าที่ตั้งไว้ล่วงหน้าที่คุณได้แก้ไข หรือเลือกที่จะถ่ายโอนค่าที่คุณได้แก้ไขไปยังค่าที่ตั้งไว้ล่วงหน้าใหม่" msgid "You have previously modified your settings." msgstr "คุณได้แก้ไขการตั้งค่าของคุณก่อนหน้านี้" msgid "" "\n" -"You can discard the preset values you have modified, or choose to transfer " -"the modified values to the new project" +"You can discard the preset values you have modified, or choose to transfer the modified values to the new project" msgstr "" "\n" "คุณสามารถละทิ้งค่าที่ตั้งไว้ล่วงหน้าที่คุณได้แก้ไข หรือเลือกที่จะถ่ายโอนค่าที่แก้ไขไปยังโปรเจ็กต์ใหม่" @@ -10732,14 +9759,12 @@ msgstr "ค่าที่ตั้งไว้ล่วงหน้าด้า msgid "Right Preset Value" msgstr "ค่าที่ตั้งไว้ทางขวา" -msgid "" -"You can only transfer to current active profile because it has been modified." +msgid "You can only transfer to current active profile because it has been modified." msgstr "คุณสามารถถ่ายโอนไปยังโปรไฟล์ที่ใช้งานปัจจุบันได้เท่านั้นเนื่องจากมีการแก้ไข" msgid "" "Transfer the selected options from left preset to the right.\n" -"Note: New modified presets will be selected in settings tabs after close " -"this dialog." +"Note: New modified presets will be selected in settings tabs after close this dialog." msgstr "" "ถ่ายโอนตัวเลือกที่เลือกจากค่าที่ตั้งล่วงหน้าด้านซ้ายไปทางด้านขวา\n" "หมายเหตุ: ค่าที่ตั้งล่วงหน้าที่แก้ไขใหม่จะถูกเลือกในแท็บการตั้งค่าหลังจากปิดกล่องโต้ตอบนี้" @@ -10747,11 +9772,8 @@ msgstr "" msgid "Transfer values from left to right" msgstr "โอนค่าจากซ้ายไปขวา" -msgid "" -"If enabled, this dialog can be used for transfer selected values from left " -"to right preset." -msgstr "" -"หากเปิดใช้งาน กล่องโต้ตอบนี้สามารถใช้เพื่อโอนค่าที่เลือกจากค่าที่ตั้งไว้ล่วงหน้าจากซ้ายไปขวาได้" +msgid "If enabled, this dialog can be used for transfer selected values from left to right preset." +msgstr "หากเปิดใช้งาน กล่องโต้ตอบนี้สามารถใช้เพื่อโอนค่าที่เลือกจากค่าที่ตั้งไว้ล่วงหน้าจากซ้ายไปขวาได้" msgid "Add File" msgstr "เพิ่มไฟล์" @@ -10886,12 +9908,10 @@ msgid "—> " msgstr "—> " msgid "" -"Synchronizing AMS filaments will discard your modified but unsaved filament " -"presets.\n" +"Synchronizing AMS filaments will discard your modified but unsaved filament presets.\n" "Are you sure you want to continue?" msgstr "" -"การซิงโครไนซ์ฟิลาเมนต์ AMS " -"จะละทิ้งค่าที่ตั้งไว้ล่วงหน้าของฟิลาเมนต์ที่ได้รับการแก้ไขแต่ไม่ได้บันทึกไว้\n" +"การซิงโครไนซ์ฟิลาเมนต์ AMS จะละทิ้งค่าที่ตั้งไว้ล่วงหน้าของฟิลาเมนต์ที่ได้รับการแก้ไขแต่ไม่ได้บันทึกไว้\n" "คุณแน่ใจหรือไม่ว่าต้องการดำเนินการต่อ?" msgctxt "Sync_AMS" @@ -10908,9 +9928,7 @@ msgctxt "Sync_AMS" msgid "Plate" msgstr "ฐานพิมพ์" -msgid "" -"The connected printer does not match the currently selected printer. Please " -"change the selected printer." +msgid "The connected printer does not match the currently selected printer. Please change the selected printer." msgstr "เครื่องพิมพ์ที่เชื่อมต่อไม่ตรงกับเครื่องพิมพ์ที่เลือกในปัจจุบัน กรุณาเปลี่ยนเครื่องพิมพ์ที่เลือก" msgid "Mapping" @@ -10954,17 +9972,11 @@ msgstr "ใช้ AMS" msgid "Tip" msgstr "เคล็ดลับ" -msgid "" -"Only synchronize filament type and color, not including AMS slot information." +msgid "Only synchronize filament type and color, not including AMS slot information." msgstr "ซิงโครไนซ์ประเภทเส้นพลาสติกและสีเท่านั้น ไม่รวมข้อมูลสล็อต AMS" -msgid "" -"Replace the project filaments list sequentially based on printer filaments. " -"And unused printer filaments will be automatically added to the end of the " -"list." -msgstr "" -"แทนที่รายการเส้นพลาสติกของโครงการตามลำดับตามเส้นพลาสติกของเครื่องพิมพ์ " -"และเส้นพลาสติกเครื่องพิมพ์ที่ไม่ได้ใช้จะถูกเพิ่มเข้าที่ส่วนท้ายของรายการโดยอัตโนมัติ" +msgid "Replace the project filaments list sequentially based on printer filaments. And unused printer filaments will be automatically added to the end of the list." +msgstr "แทนที่รายการเส้นพลาสติกของโครงการตามลำดับตามเส้นพลาสติกของเครื่องพิมพ์ และเส้นพลาสติกเครื่องพิมพ์ที่ไม่ได้ใช้จะถูกเพิ่มเข้าที่ส่วนท้ายของรายการโดยอัตโนมัติ" msgid "Add unused AMS filaments to filaments list." msgstr "เพิ่มเส้นพลาสติก AMS ที่ไม่ได้ใช้ลงในรายการเส้นพลาสติก" @@ -10975,14 +9987,8 @@ msgstr "ผสานสีเดียวกันในโมเดลโดย msgid "After being synced, this action cannot be undone." msgstr "หลังจากซิงค์แล้ว การดำเนินการนี้ไม่สามารถยกเลิกได้" -msgid "" -"After being synced, the project's filament presets and colors will be " -"replaced with the mapped filament types and colors. This action cannot be " -"undone." -msgstr "" -"หลังจากที่ซิงค์แล้ว " -"ค่าที่ตั้งไว้ล่วงหน้าของเส้นพลาสติกและสีของโปรเจ็กต์จะถูกแทนที่ด้วยประเภทและสีของเส้นพลาสติกที่แมปไว้ " -"การดำเนินการนี้ไม่สามารถยกเลิกได้" +msgid "After being synced, the project's filament presets and colors will be replaced with the mapped filament types and colors. This action cannot be undone." +msgstr "หลังจากที่ซิงค์แล้ว ค่าที่ตั้งไว้ล่วงหน้าของเส้นพลาสติกและสีของโปรเจ็กต์จะถูกแทนที่ด้วยประเภทและสีของเส้นพลาสติกที่แมปไว้ การดำเนินการนี้ไม่สามารถยกเลิกได้" msgid "Are you sure to synchronize the filaments?" msgstr "คุณแน่ใจหรือว่าซิงโครไนซ์เส้นพลาสติก?" @@ -10996,37 +10002,27 @@ msgstr "ประสานข้อมูลเส้นพลาสติก" msgid "Add unused filaments to filaments list." msgstr "เพิ่มเส้นพลาสติกที่ไม่ได้ใช้ลงในรายการเส้นพลาสติก" -msgid "" -"Only synchronize filament type and color, not including slot information." +msgid "Only synchronize filament type and color, not including slot information." msgstr "ซิงโครไนซ์ประเภทเส้นพลาสติกและสีเท่านั้น ไม่รวมข้อมูลช่อง" msgid "Ext spool" msgstr "สปูลต่อ" -msgid "" -"Please check whether the nozzle type of the device is the same as the preset " -"nozzle type." +msgid "Please check whether the nozzle type of the device is the same as the preset nozzle type." msgstr "โปรดตรวจสอบว่าประเภทหัวฉีดของอุปกรณ์เหมือนกับประเภทหัวฉีดที่ตั้งไว้ล่วงหน้าหรือไม่" msgid "Storage is not available or is in read-only mode." msgstr "พื้นที่เก็บข้อมูลไม่พร้อมใช้งานหรืออยู่ในโหมดอ่านอย่างเดียว" #, c-format, boost-format -msgid "" -"The selected printer (%s) is incompatible with the chosen printer profile in " -"the slicer (%s)." +msgid "The selected printer (%s) is incompatible with the chosen printer profile in the slicer (%s)." msgstr "เครื่องพิมพ์ที่เลือก (%s) เข้ากันไม่ได้กับโปรไฟล์เครื่องพิมพ์ที่เลือกในตัวแบ่งส่วนข้อมูล (%s)" -msgid "" -"Timelapse is not supported because Print sequence is set to \"By object\"." +msgid "Timelapse is not supported because Print sequence is set to \"By object\"." msgstr "ไม่รองรับไทม์แลปส์เนื่องจากลำดับการพิมพ์ถูกตั้งค่าเป็น \"ตามวัตถุ\"" -msgid "" -"You selected external and AMS filament at the same time in an extruder, you " -"will need manually change external filament." -msgstr "" -"คุณเลือกเส้นพลาสติกภายนอกและเส้นพลาสติก AMS พร้อมกันในชุดดันเส้น " -"คุณจะต้องเปลี่ยนเส้นพลาสติกภายนอกด้วยตนเอง" +msgid "You selected external and AMS filament at the same time in an extruder, you will need manually change external filament." +msgstr "คุณเลือกเส้นพลาสติกภายนอกและเส้นพลาสติก AMS พร้อมกันในชุดดันเส้น คุณจะต้องเปลี่ยนเส้นพลาสติกภายนอกด้วยตนเอง" msgid "Successfully synchronized nozzle information." msgstr "ซิงโครไนซ์ข้อมูลหัวฉีดสำเร็จแล้ว" @@ -11055,25 +10051,13 @@ msgid "Ramming customization" msgstr "การปรับแต่งการกระแทก" msgid "" -"Ramming denotes the rapid extrusion just before a tool change in a single-" -"extruder MM printer. Its purpose is to properly shape the end of the " -"unloaded filament so it does not prevent insertion of the new filament and " -"can itself be reinserted later. This phase is important and different " -"materials can require different extrusion speeds to get the good shape. For " -"this reason, the extrusion rates during ramming are adjustable.\n" +"Ramming denotes the rapid extrusion just before a tool change in a single-extruder MM printer. Its purpose is to properly shape the end of the unloaded filament so it does not prevent insertion of the new filament and can itself be reinserted later. This phase is important and different materials can require different extrusion speeds to get the good shape. For this reason, the extrusion rates during ramming are adjustable.\n" "\n" -"This is an expert-level setting, incorrect adjustment will likely lead to " -"jams, extruder wheel grinding into filament etc." +"This is an expert-level setting, incorrect adjustment will likely lead to jams, extruder wheel grinding into filament etc." msgstr "" -"การชนหมายถึงการอัดขึ้นรูปอย่างรวดเร็วก่อนการเปลี่ยนเครื่องมือในเครื่องพิมพ์ MM แบบชุดดันเส้นเดี่ยว " -"จุดประสงค์คือเพื่อจัดรูปทรงส่วนปลายของเส้นพลาสติกที่ไม่ได้บรรจุอย่างเหมาะสม " -"เพื่อไม่ให้ป้องกันการใส่เส้นพลาสติกใหม่และสามารถใส่กลับเข้าไปใหม่ได้ในภายหลัง " -"ขั้นตอนนี้มีความสำคัญ " -"และวัสดุที่แตกต่างกันอาจต้องใช้ความเร็วการอัดขึ้นรูปที่แตกต่างกันเพื่อให้ได้รูปร่างที่ดี ด้วยเหตุนี้ " -"อัตราการอัดขึ้นรูประหว่างการชนจึงสามารถปรับได้\n" +"การชนหมายถึงการอัดขึ้นรูปอย่างรวดเร็วก่อนการเปลี่ยนเครื่องมือในเครื่องพิมพ์ MM แบบชุดดันเส้นเดี่ยว จุดประสงค์คือเพื่อจัดรูปทรงส่วนปลายของเส้นพลาสติกที่ไม่ได้บรรจุอย่างเหมาะสม เพื่อไม่ให้ป้องกันการใส่เส้นพลาสติกใหม่และสามารถใส่กลับเข้าไปใหม่ได้ในภายหลัง ขั้นตอนนี้มีความสำคัญ และวัสดุที่แตกต่างกันอาจต้องใช้ความเร็วการอัดขึ้นรูปที่แตกต่างกันเพื่อให้ได้รูปร่างที่ดี ด้วยเหตุนี้ อัตราการอัดขึ้นรูประหว่างการชนจึงสามารถปรับได้\n" "\n" -"นี่เป็นการตั้งค่าระดับผู้เชี่ยวชาญ การปรับที่ไม่ถูกต้องอาจทำให้เกิดการติดขัด " -"ล้อชุดดันเส้นบดเป็นเส้นพลาสติก ฯลฯ" +"นี่เป็นการตั้งค่าระดับผู้เชี่ยวชาญ การปรับที่ไม่ถูกต้องอาจทำให้เกิดการติดขัด ล้อชุดดันเส้นบดเป็นเส้นพลาสติก ฯลฯ" #, boost-format msgid "For constant flow rate, hold %1% while dragging." @@ -11091,13 +10075,8 @@ msgstr "ปริมาณ" msgid "Ramming line" msgstr "เส้นราม" -msgid "" -"Orca would re-calculate your flushing volumes everytime the filaments color " -"changed or filaments changed. You could disable the auto-calculate in Orca " -"Slicer > Preferences" -msgstr "" -"Orca จะคำนวณปริมาตรการไล่เส้นของคุณใหม่ทุกครั้งที่สีเส้นพลาสติกเปลี่ยนไปหรือเส้นพลาสติกเปลี่ยนไป " -"คุณสามารถปิดการใช้งานการคำนวณอัตโนมัติใน Orca Slicer > การตั้งค่า" +msgid "Orca would re-calculate your flushing volumes everytime the filaments color changed or filaments changed. You could disable the auto-calculate in Orca Slicer > Preferences" +msgstr "Orca จะคำนวณปริมาตรการไล่เส้นของคุณใหม่ทุกครั้งที่สีเส้นพลาสติกเปลี่ยนไปหรือเส้นพลาสติกเปลี่ยนไป คุณสามารถปิดการใช้งานการคำนวณอัตโนมัติใน Orca Slicer > การตั้งค่า" msgid "Flushing volume (mm³) for each filament pair." msgstr "ปริมาณการไล่เส้นชิ่ง (มม.) สำหรับคู่เส้นพลาสติกแต่ละคู่" @@ -11128,54 +10107,26 @@ msgstr "ปริมาณการไล่เส้นชิ่งสำหร msgid "Please choose the filament colour" msgstr "กรุณาเลือกสีเส้นพลาสติก" -msgid "" -"Native Wayland liveview requires the GStreamer GTK video sink. Please " -"install the gtksink plugin for GStreamer, then restart OrcaSlicer." -msgstr "" -"Native Wayland liveview ต้องใช้ GStreamer GTK video sink โปรดติดตั้งปลั๊กอิน gtksink " -"สำหรับ GStreamer จากนั้นรีสตาร์ท OrcaSlicer" +msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." +msgstr "Native Wayland liveview ต้องใช้ GStreamer GTK video sink โปรดติดตั้งปลั๊กอิน gtksink สำหรับ GStreamer จากนั้นรีสตาร์ท OrcaSlicer" -msgid "" -"Failed to initialize the native Wayland GStreamer video sink. Please check " -"your GStreamer GTK plugin installation." -msgstr "" -"ไม่สามารถเริ่มต้น sink วิดีโอ Wayland GStreamer ดั้งเดิมได้ โปรดตรวจสอบการติดตั้งปลั๊กอิน " -"GStreamer GTK ของคุณ" +msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." +msgstr "ไม่สามารถเริ่มต้น sink วิดีโอ Wayland GStreamer ดั้งเดิมได้ โปรดตรวจสอบการติดตั้งปลั๊กอิน GStreamer GTK ของคุณ" -msgid "" -"Windows Media Player is required for this task! Do you want to enable " -"'Windows Media Player' for your operation system?" -msgstr "" -"งานนี้ต้องใช้ Windows Media Player! คุณต้องการเปิดใช้งาน 'Windows Media Player' " -"สำหรับระบบปฏิบัติการของคุณหรือไม่?" +msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" +msgstr "งานนี้ต้องใช้ Windows Media Player! คุณต้องการเปิดใช้งาน 'Windows Media Player' สำหรับระบบปฏิบัติการของคุณหรือไม่?" -msgid "" -"BambuSource has not correctly been registered for media playing! Press Yes " -"to re-register it. You will be promoted twice" -msgstr "" -"BambuSource ยังไม่ได้รับการลงทะเบียนอย่างถูกต้องสำหรับการเล่นสื่อ! กดใช่เพื่อลงทะเบียนใหม่ " -"คุณจะได้รับการเลื่อนตำแหน่งสองครั้ง" +msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" +msgstr "BambuSource ยังไม่ได้รับการลงทะเบียนอย่างถูกต้องสำหรับการเล่นสื่อ! กดใช่เพื่อลงทะเบียนใหม่ คุณจะได้รับการเลื่อนตำแหน่งสองครั้ง" -msgid "" -"Missing BambuSource component registered for media playing! Please re-" -"install OrcaSlicer or seek community help." -msgstr "" -"ไม่มีส่วนประกอบ BambuSource ที่ลงทะเบียนสำหรับการเล่นสื่อ! โปรดติดตั้ง OrcaSlicer " -"ใหม่หรือขอความช่วยเหลือจากชุมชน" +msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." +msgstr "ไม่มีส่วนประกอบ BambuSource ที่ลงทะเบียนสำหรับการเล่นสื่อ! โปรดติดตั้ง OrcaSlicer ใหม่หรือขอความช่วยเหลือจากชุมชน" -msgid "" -"Using a BambuSource from a different install, video play may not work " -"correctly! Press Yes to fix it." +msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." msgstr "การใช้ BambuSource จากการติดตั้งอื่น การเล่นวิดีโออาจทำงานไม่ถูกต้อง! กดใช่เพื่อแก้ไข" -msgid "" -"Your system is missing H.264 codecs for GStreamer, which are required to " -"play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-" -"libav packages, then restart Orca Slicer?)" -msgstr "" -"ระบบของคุณไม่มีตัวแปลงสัญญาณ H.264 สำหรับ GStreamer ซึ่งจำเป็นในการเล่นวิดีโอ " -"(ลองติดตั้งแพ็คเกจ gstreamer1.0-plugins-bad หรือ gstreamer1.0-libav จากนั้นรีสตาร์ท " -"Orca Slicer หรือไม่)" +msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" +msgstr "ระบบของคุณไม่มีตัวแปลงสัญญาณ H.264 สำหรับ GStreamer ซึ่งจำเป็นในการเล่นวิดีโอ (ลองติดตั้งแพ็คเกจ gstreamer1.0-plugins-bad หรือ gstreamer1.0-libav จากนั้นรีสตาร์ท Orca Slicer หรือไม่)" msgid "Cloud agent is not available. Please restart OrcaSlicer and try again." msgstr "ตัวแทนระบบคลาวด์ไม่พร้อมใช้งาน โปรดรีสตาร์ท OrcaSlicer แล้วลองอีกครั้ง" @@ -11240,13 +10191,8 @@ msgstr "ปุ่มกลางของเมาส์" msgid "Zoom View" msgstr "ซูมดู" -msgid "" -"Auto orients selected objects or all objects. If there are selected objects, " -"it just orients the selected ones. Otherwise, it will orient all objects in " -"the current project." -msgstr "" -"ปรับทิศทางวัตถุที่เลือกหรือวัตถุทั้งหมดโดยอัตโนมัติ หากมีวัตถุที่เลือกไว้ " -"วัตถุนั้นจะปรับทิศทางวัตถุที่เลือกเท่านั้น มิฉะนั้นจะวางแนววัตถุทั้งหมดในโปรเจ็กต์ปัจจุบัน" +msgid "Auto orients selected objects or all objects. If there are selected objects, it just orients the selected ones. Otherwise, it will orient all objects in the current project." +msgstr "ปรับทิศทางวัตถุที่เลือกหรือวัตถุทั้งหมดโดยอัตโนมัติ หากมีวัตถุที่เลือกไว้ วัตถุนั้นจะปรับทิศทางวัตถุที่เลือกเท่านั้น มิฉะนั้นจะวางแนววัตถุทั้งหมดในโปรเจ็กต์ปัจจุบัน" msgid "Auto orients all objects on the active plate." msgstr "ปรับทิศทางวัตถุทั้งหมดบนเพลตที่ใช้งานอยู่โดยอัตโนมัติ" @@ -11444,8 +10390,7 @@ msgstr "ข้อมูลการอัปเดตเวอร์ชัน %s msgid "Network plug-in update" msgstr "การอัปเดตปลั๊กอินเครือข่าย" -msgid "" -"Click OK to update the Network plug-in the next time Orca Slicer launches." +msgid "Click OK to update the Network plug-in the next time Orca Slicer launches." msgstr "คลิกตกลงเพื่ออัปเดตปลั๊กอินเครือข่ายในครั้งถัดไปที่ Orca Slicer เริ่มทำงาน" #, c-format, boost-format @@ -11468,8 +10413,7 @@ msgid "Skip this Version" msgstr "ข้ามเวอร์ชันนี้" #, c-format, boost-format -msgid "" -"New version available: %s. Please update OrcaSlicer from the Microsoft Store." +msgid "New version available: %s. Please update OrcaSlicer from the Microsoft Store." msgstr "มีเวอร์ชันใหม่: %s กรุณาอัปเดต OrcaSlicer จาก Microsoft Store" msgid "Confirm and Update Nozzle" @@ -11478,23 +10422,16 @@ msgstr "ยืนยันและอัปเดตหัวฉีด" msgid "Connect the printer using IP and access code" msgstr "เชื่อมต่อเครื่องพิมพ์โดยใช้ IP และรหัสการเข้าถึง" -msgid "" -"Try the following methods to update the connection parameters and reconnect " -"to the printer." +msgid "Try the following methods to update the connection parameters and reconnect to the printer." msgstr "ลองวิธีการต่อไปนี้เพื่ออัพเดตพารามิเตอร์การเชื่อมต่อและเชื่อมต่อกับเครื่องพิมพ์อีกครั้ง" msgid "1. Please confirm Orca Slicer and your printer are in the same LAN." msgstr "1. โปรดยืนยันว่า Orca Slicer และเครื่องพิมพ์ของคุณอยู่ใน LAN เดียวกัน" -msgid "" -"2. If the IP and Access Code below are different from the actual values on " -"your printer, please correct them." -msgstr "" -"2. หาก IP และรหัสการเข้าถึงด้านล่างแตกต่างจากค่าจริงบนเครื่องพิมพ์ของคุณ โปรดแก้ไขให้ถูกต้อง" +msgid "2. If the IP and Access Code below are different from the actual values on your printer, please correct them." +msgstr "2. หาก IP และรหัสการเข้าถึงด้านล่างแตกต่างจากค่าจริงบนเครื่องพิมพ์ของคุณ โปรดแก้ไขให้ถูกต้อง" -msgid "" -"3. Please obtain the device SN from the printer side; it is usually found in " -"the device information on the printer screen." +msgid "3. Please obtain the device SN from the printer side; it is usually found in the device information on the printer screen." msgstr "3. โปรดรับอุปกรณ์ SN จากฝั่งเครื่องพิมพ์ มักจะพบในข้อมูลอุปกรณ์บนหน้าจอเครื่องพิมพ์" msgid "IP" @@ -11555,11 +10492,8 @@ msgstr "การเชื่อมต่อล้มเหลว! โปรด msgid "sending failed" msgstr "การส่งล้มเหลว" -msgid "" -"Failed to send. Click Retry to attempt sending again. If retrying does not " -"work, please check the reason." -msgstr "" -"ไม่สามารถส่งได้ คลิกลองอีกครั้งเพื่อพยายามส่งอีกครั้ง หากการลองใหม่ไม่ได้ผล โปรดตรวจสอบสาเหตุ" +msgid "Failed to send. Click Retry to attempt sending again. If retrying does not work, please check the reason." +msgstr "ไม่สามารถส่งได้ คลิกลองอีกครั้งเพื่อพยายามส่งอีกครั้ง หากการลองใหม่ไม่ได้ผล โปรดตรวจสอบสาเหตุ" msgid "reconnect" msgstr "เชื่อมต่อใหม่" @@ -11597,28 +10531,14 @@ msgstr "การอัปเดตล้มเหลว" msgid "Update successful" msgstr "อัปเดตสำเร็จ" -msgid "" -"Are you sure you want to update? This will take about 10 minutes. Do not " -"turn off the power while the printer is updating." -msgstr "" -"คุณแน่ใจหรือไม่ว่าต้องการอัปเดต การดำเนินการนี้จะใช้เวลาประมาณ 10 นาที " -"อย่าปิดเครื่องในขณะที่เครื่องพิมพ์กำลังอัปเดต" +msgid "Are you sure you want to update? This will take about 10 minutes. Do not turn off the power while the printer is updating." +msgstr "คุณแน่ใจหรือไม่ว่าต้องการอัปเดต การดำเนินการนี้จะใช้เวลาประมาณ 10 นาที อย่าปิดเครื่องในขณะที่เครื่องพิมพ์กำลังอัปเดต" -msgid "" -"An important update was detected and needs to be run before printing can " -"continue. Do you want to update now? You can also update later from 'Update " -"firmware'." -msgstr "" -"ตรวจพบการอัปเดตที่สำคัญและจำเป็นต้องดำเนินการก่อนจึงจะสามารถพิมพ์ต่อได้ " -"คุณต้องการอัปเดตตอนนี้หรือไม่? คุณสามารถอัปเดตภายหลังได้จาก 'อัปเกรดเฟิร์มแวร์'" +msgid "An important update was detected and needs to be run before printing can continue. Do you want to update now? You can also update later from 'Update firmware'." +msgstr "ตรวจพบการอัปเดตที่สำคัญและจำเป็นต้องดำเนินการก่อนจึงจะสามารถพิมพ์ต่อได้ คุณต้องการอัปเดตตอนนี้หรือไม่? คุณสามารถอัปเดตภายหลังได้จาก 'อัปเกรดเฟิร์มแวร์'" -msgid "" -"The firmware version is abnormal. Repairing and updating are required before " -"printing. Do you want to update now? You can also update later on the " -"printer or update next time you start Orca Slicer." -msgstr "" -"เวอร์ชันเฟิร์มแวร์ผิดปกติ จำเป็นต้องซ่อมแซมและอัปเดตก่อนพิมพ์ คุณต้องการอัปเดตตอนนี้หรือไม่? " -"คุณยังสามารถอัปเดตภายหลังบนเครื่องพิมพ์ หรืออัปเดตในครั้งถัดไปที่เปิด Orca Slicer" +msgid "The firmware version is abnormal. Repairing and updating are required before printing. Do you want to update now? You can also update later on the printer or update next time you start Orca Slicer." +msgstr "เวอร์ชันเฟิร์มแวร์ผิดปกติ จำเป็นต้องซ่อมแซมและอัปเดตก่อนพิมพ์ คุณต้องการอัปเดตตอนนี้หรือไม่? คุณยังสามารถอัปเดตภายหลังบนเครื่องพิมพ์ หรืออัปเดตในครั้งถัดไปที่เปิด Orca Slicer" msgid "Extension Board" msgstr "คณะกรรมการขยาย" @@ -11652,9 +10572,7 @@ msgstr "อัปเดตเป็น" msgid "Open G-code file:" msgstr "เปิดไฟล์ G-code:" -msgid "" -"One object has an empty first layer and can't be printed. Please Cut the " -"bottom or enable supports." +msgid "One object has an empty first layer and can't be printed. Please Cut the bottom or enable supports." msgstr "วัตถุหนึ่งมีชั้นแรกว่างเปล่าและไม่สามารถพิมพ์ได้ กรุณาตัดด้านล่างหรือเปิดใช้งานส่วนรองรับ" #, boost-format @@ -11665,9 +10583,7 @@ msgstr "วัตถุมีชั้นว่างระหว่าง %1% msgid "Object: %1%" msgstr "วัตถุ: %1%" -msgid "" -"Parts of the object at these heights may be too thin or the object may have " -"a faulty mesh." +msgid "Parts of the object at these heights may be too thin or the object may have a faulty mesh." msgstr "บางส่วนของวัตถุที่ความสูงเหล่านี้อาจบางเกินไป หรือวัตถุอาจมี mesh ผิดปกติ" msgid "Process change extrusion role G-code" @@ -11679,9 +10595,7 @@ msgstr "เส้นพลาสติกเปลี่ยนบทบาทก msgid "No object can be printed. It may be too small." msgstr "ไม่สามารถพิมพ์วัตถุได้ อาจจะเล็กเกินไป" -msgid "" -"Your print is very close to the priming regions. Make sure there is no " -"collision." +msgid "Your print is very close to the priming regions. Make sure there is no collision." msgstr "งานพิมพ์ของคุณอยู่ใกล้กับบริเวณรองพื้นมาก ตรวจสอบให้แน่ใจว่าไม่มีการชนกัน" msgid "" @@ -11709,8 +10623,7 @@ msgstr "" "Marlin < 2.1.2 ไม่รองรับการสร้างรูปร่างอินพุต\n" "ตรวจสอบเวอร์ชันเฟิร์มแวร์ของคุณและอัปเดตรสชาติ G-code ของคุณเป็น 'Marlin 2'" -msgid "" -"Input shaping is only supported by Klipper, RepRapFirmware and Marlin 2." +msgid "Input shaping is only supported by Klipper, RepRapFirmware and Marlin 2." msgstr "รูปร่างอินพุตรองรับเฉพาะ Klipper, RepRapFirmware และ Marlin 2 เท่านั้น" msgid "Grouping error: " @@ -11726,12 +10639,8 @@ msgstr "สะพานภายใน" msgid "Failed to calculate line width of %1%. Cannot get value of “%2%” " msgstr "ไม่สามารถคำนวณความกว้างของเส้น %1% ไม่สามารถรับค่า \"%2%\"" -msgid "" -"Invalid spacing supplied to Flow::with_spacing(), check your layer height " -"and extrusion width" -msgstr "" -"การเว้นวรรคที่ให้กับ Flow::with_spacing() ไม่ถูกต้อง " -"โปรดตรวจสอบความสูงของเลเยอร์และความกว้างของการอัดขึ้นรูป" +msgid "Invalid spacing supplied to Flow::with_spacing(), check your layer height and extrusion width" +msgstr "การเว้นวรรคที่ให้กับ Flow::with_spacing() ไม่ถูกต้อง โปรดตรวจสอบความสูงของเลเยอร์และความกว้างของการอัดขึ้นรูป" msgid "undefined error" msgstr "ข้อผิดพลาดที่ไม่ได้กำหนด" @@ -11827,8 +10736,7 @@ msgid "write callback failed" msgstr "การเขียนการโทรกลับล้มเหลว" #, boost-format -msgid "" -"%1% is too close to exclusion area. There may be collisions when printing." +msgid "%1% is too close to exclusion area. There may be collisions when printing." msgstr "%1% อยู่ใกล้พื้นที่แยกมากเกินไป อาจเกิดการชนกันเมื่อพิมพ์" #, boost-format @@ -11842,9 +10750,7 @@ msgstr "%1% สูงเกินไป และจะเกิดการช msgid " is too close to exclusion area, there may be collisions when printing." msgstr "อยู่ใกล้พื้นที่แยกมากเกินไป อาจเกิดการชนกันเมื่อพิมพ์" -msgid "" -" is too close to clumping detection area, there may be collisions when " -"printing." +msgid " is too close to clumping detection area, there may be collisions when printing." msgstr "อยู่ใกล้พื้นที่การตรวจจับการจับตัวกันมากเกินไป อาจเกิดการชนกันเมื่อพิมพ์" msgid "Prime Tower" @@ -11856,156 +10762,94 @@ msgstr "อยู่ใกล้ผู้อื่นมากเกินไป msgid " is too close to an exclusion area, and collisions will be caused.\n" msgstr "อยู่ใกล้เขตหวงห้ามมากเกินไปจะเกิดการชนกัน\n" -msgid "" -" is too close to clumping detection area, and collisions will be caused.\n" +msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr "อยู่ใกล้พื้นที่การตรวจจับการจับตัวกันมากเกินไป และจะเกิดการชนกัน\n" -msgid "" -"Selected nozzle temperatures are incompatible. Each filament's nozzle " -"temperature must fall within the recommended nozzle temperature range of the " -"other filaments. Otherwise, nozzle clogging or printer damage may occur." -msgstr "" -"อุณหภูมิหัวฉีดที่เลือกเข้ากันไม่ได้ " -"อุณหภูมิหัวฉีดของเส้นพลาสติกแต่ละเส้นต้องอยู่ในช่วงอุณหภูมิหัวฉีดที่แนะนำของเส้นพลาสติกอื่นๆ " -"มิฉะนั้นอาจเกิดการอุดตันของหัวฉีดหรือเครื่องพิมพ์เสียหายได้" +msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." +msgstr "อุณหภูมิหัวฉีดที่เลือกเข้ากันไม่ได้ อุณหภูมิหัวฉีดของเส้นพลาสติกแต่ละเส้นต้องอยู่ในช่วงอุณหภูมิหัวฉีดที่แนะนำของเส้นพลาสติกอื่นๆ มิฉะนั้นอาจเกิดการอุดตันของหัวฉีดหรือเครื่องพิมพ์เสียหายได้" -msgid "" -"Invalid recommended nozzle temperature range. The lower bound must be lower " -"than the upper bound." +msgid "Invalid recommended nozzle temperature range. The lower bound must be lower than the upper bound." msgstr "ช่วงอุณหภูมิหัวฉีดที่แนะนำไม่ถูกต้อง ขอบเขตล่างจะต้องต่ำกว่าขอบเขตบน" -msgid "" -"If you still want to print, you can enable the option in Preferences / " -"Control / Slicing / Remove mixed temperature restriction." -msgstr "" -"หากคุณยังต้องการพิมพ์ คุณสามารถเปิดใช้งานตัวเลือกในการตั้งค่า / การควบคุม / การตัด / " -"ลบข้อจำกัดอุณหภูมิแบบผสมได้" +msgid "If you still want to print, you can enable the option in Preferences / Control / Slicing / Remove mixed temperature restriction." +msgstr "หากคุณยังต้องการพิมพ์ คุณสามารถเปิดใช้งานตัวเลือกในการตั้งค่า / การควบคุม / การตัด / ลบข้อจำกัดอุณหภูมิแบบผสมได้" msgid "No extrusions under current settings." msgstr "ไม่มีการอัดขึ้นรูปภายใต้การตั้งค่าปัจจุบัน" -msgid "" -"Smooth mode of timelapse is not supported when \"by object\" sequence is " -"enabled." +msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "ไม่รองรับโหมดไทม์แลปส์แบบราบรื่นเมื่อเปิดใช้งานลำดับ \"ตามวัตถุ\"" -msgid "" -"Clumping detection is not supported when \"by object\" sequence is enabled." +msgid "Clumping detection is not supported when \"by object\" sequence is enabled." msgstr "ไม่รองรับการตรวจจับการจับกันเป็นก้อนเมื่อเปิดใช้งานลำดับ \"ตามวัตถุ\"" -msgid "" -"Enabling both precise Z height and the prime tower may cause slicing errors." +msgid "Enabling both precise Z height and the prime tower may cause slicing errors." msgstr "การเปิดใช้งานทั้งความสูง Z ที่แม่นยำและหอคอยหลักอาจทำให้เกิดข้อผิดพลาดในการแบ่งส่วน" -msgid "" -"A prime tower is required for clumping detection; otherwise, there may be " -"flaws on the model." +msgid "A prime tower is required for clumping detection; otherwise, there may be flaws on the model." msgstr "จำเป็นต้องใช้หอคอยหลักในการตรวจจับการจับกันเป็นก้อน มิฉะนั้นอาจมีข้อบกพร่องในแบบจำลอง" -msgid "" -"Please select \"By object\" print sequence to print multiple objects in " -"spiral vase mode." +msgid "Please select \"By object\" print sequence to print multiple objects in spiral vase mode." msgstr "โปรดเลือกลำดับการพิมพ์ \"ตามวัตถุ\" เพื่อพิมพ์วัตถุหลายชิ้นในโหมดแจกันเกลียว" -msgid "" -"Spiral (vase) mode does not work when an object contains more than one " -"material." +msgid "Spiral (vase) mode does not work when an object contains more than one material." msgstr "โหมดแจกันเกลียวจะไม่ทำงานเมื่อวัตถุมีวัสดุมากกว่าหนึ่งชนิด" #, boost-format -msgid "" -"While the object %1% itself fits the build volume, it exceeds the maximum " -"build volume height because of material shrinkage compensation." -msgstr "" -"แม้ว่าวัตถุ %1% จะพอดีกับปริมาตรการสร้าง " -"แต่วัตถุนั้นเกินความสูงของปริมาตรการสร้างสูงสุดเนื่องจากการชดเชยการหดตัวของวัสดุ" +msgid "While the object %1% itself fits the build volume, it exceeds the maximum build volume height because of material shrinkage compensation." +msgstr "แม้ว่าวัตถุ %1% จะพอดีกับปริมาตรการสร้าง แต่วัตถุนั้นเกินความสูงของปริมาตรการสร้างสูงสุดเนื่องจากการชดเชยการหดตัวของวัสดุ" #, boost-format msgid "The object %1% exceeds the maximum build volume height." msgstr "วัตถุ %1% เกินความสูงของปริมาตรบิลด์สูงสุด" #, boost-format -msgid "" -"While the object %1% itself fits the build volume, its last layer exceeds " -"the maximum build volume height." -msgstr "" -"แม้ว่าออบเจ็กต์ %1% จะพอดีกับปริมาณการสร้าง " -"แต่เลเยอร์สุดท้ายก็เกินความสูงของปริมาตรการสร้างสูงสุด" +msgid "While the object %1% itself fits the build volume, its last layer exceeds the maximum build volume height." +msgstr "แม้ว่าออบเจ็กต์ %1% จะพอดีกับปริมาณการสร้าง แต่เลเยอร์สุดท้ายก็เกินความสูงของปริมาตรการสร้างสูงสุด" -msgid "" -"You might want to reduce the size of your model or change current print " -"settings and retry." +msgid "You might want to reduce the size of your model or change current print settings and retry." msgstr "คุณอาจต้องการลดขนาดแบบจำลองของคุณหรือเปลี่ยนการตั้งค่าการพิมพ์ปัจจุบันแล้วลองอีกครั้ง" msgid "Variable layer height is not supported with Organic supports." msgstr "ไม่รองรับความสูงของเลเยอร์ที่แปรผันได้ด้วยการรองรับแบบออร์แกนิก" -msgid "" -"Different nozzle diameters and different filament diameters may not work " -"well when the prime tower is enabled. It's very experimental, so please " -"proceed with caution." -msgstr "" -"เส้นผ่านศูนย์กลางของหัวฉีดที่แตกต่างกันและเส้นผ่านศูนย์กลางของเส้นพลาสติกที่แตกต่างกันอาจทำงานได้ไม่ดีนักเมื่อเปิดใช้งานไพรม์ทาวเวอร์ " -"ยังเป็นการทดลองอยู่มาก ดังนั้นโปรดดำเนินการด้วยความระมัดระวัง" +msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." +msgstr "เส้นผ่านศูนย์กลางของหัวฉีดที่แตกต่างกันและเส้นผ่านศูนย์กลางของเส้นพลาสติกที่แตกต่างกันอาจทำงานได้ไม่ดีนักเมื่อเปิดใช้งานไพรม์ทาวเวอร์ ยังเป็นการทดลองอยู่มาก ดังนั้นโปรดดำเนินการด้วยความระมัดระวัง" -msgid "" -"The Wipe Tower is currently only supported with the relative extruder " -"addressing (use_relative_e_distances=1)." -msgstr "" -"ขณะนี้ Wipe Tower รองรับการกำหนดที่อยู่ของชุดดันเส้นแบบสัมพันธ์เท่านั้น " -"(use_relative_e_distances=1)" +msgid "The Wipe Tower is currently only supported with the relative extruder addressing (use_relative_e_distances=1)." +msgstr "ขณะนี้ Wipe Tower รองรับการกำหนดที่อยู่ของชุดดันเส้นแบบสัมพันธ์เท่านั้น (use_relative_e_distances=1)" -msgid "" -"Ooze prevention is only supported with the wipe tower when " -"'single_extruder_multi_material' is off." +msgid "Ooze prevention is only supported with the wipe tower when 'single_extruder_multi_material' is off." msgstr "รองรับการป้องกันน้ำซึมด้วยหอเช็ดเมื่อปิด 'single_extruder_multi_material' เท่านั้น" -msgid "" -"The prime tower is currently only supported for the Marlin, RepRap/Sprinter, " -"RepRapFirmware and Repetier G-code flavors." -msgstr "" -"ขณะนี้ไพรม์ทาวเวอร์รองรับเฉพาะรสชาติ Marlin, RepRap/Sprinter, RepRapFirmware และ " -"Repetier G-code เท่านั้น" +msgid "The prime tower is currently only supported for the Marlin, RepRap/Sprinter, RepRapFirmware and Repetier G-code flavors." +msgstr "ขณะนี้ไพรม์ทาวเวอร์รองรับเฉพาะรสชาติ Marlin, RepRap/Sprinter, RepRapFirmware และ Repetier G-code เท่านั้น" msgid "A prime tower is not supported in “By object” print." msgstr "ไม่รองรับไพรม์ทาวเวอร์ในการพิมพ์ \"ตามวัตถุ\"" -msgid "" -"A prime tower is not supported when adaptive layer height is on. It requires " -"that all objects have the same layer height." -msgstr "" -"ไม่รองรับไพรม์ทาวเวอร์เมื่อเปิดความสูงของเลเยอร์แบบปรับได้ " -"กำหนดให้วัตถุทั้งหมดมีความสูงของชั้นเท่ากัน" +msgid "A prime tower is not supported when adaptive layer height is on. It requires that all objects have the same layer height." +msgstr "ไม่รองรับไพรม์ทาวเวอร์เมื่อเปิดความสูงของเลเยอร์แบบปรับได้ กำหนดให้วัตถุทั้งหมดมีความสูงของชั้นเท่ากัน" -msgid "" -"A prime tower requires any “support gap” to be a multiple of layer height." +msgid "A prime tower requires any “support gap” to be a multiple of layer height." msgstr "ไพรม์ทาวเวอร์ต้องการให้ “support gap” เป็นจำนวนเท่าของความสูงชั้น" msgid "A prime tower requires that all objects have the same layer height." msgstr "ไพรม์ทาวเวอร์ต้องการให้วัตถุทั้งหมดมีความสูงชั้นเท่ากัน" -msgid "" -"A prime tower requires that all objects are printed over the same number of " -"raft layers." +msgid "A prime tower requires that all objects are printed over the same number of raft layers." msgstr "ไพรม์ทาวเวอร์ต้องการให้วัตถุทั้งหมดพิมพ์บนจำนวนชั้น raft เท่ากัน" -msgid "" -"The prime tower is only supported for multiple objects if they are printed " -"with the same support_top_z_distance." +msgid "The prime tower is only supported for multiple objects if they are printed with the same support_top_z_distance." msgstr "ไพรม์ทาวเวอร์รองรับวัตถุหลายชิ้นเท่านั้นหากพิมพ์ด้วย support_top_z_distance เท่ากัน" -msgid "" -"A prime tower requires that all objects are sliced with the same layer " -"height." +msgid "A prime tower requires that all objects are sliced with the same layer height." msgstr "ไพรม์ทาวเวอร์ต้องการให้วัตถุทั้งหมดถูกสไลซ์ด้วยความสูงชั้นเท่ากัน" -msgid "" -"The prime tower is only supported if all objects have the same variable " -"layer height." +msgid "The prime tower is only supported if all objects have the same variable layer height." msgstr "ไพรม์ทาวเวอร์ได้รับส่วนรองรับก็ต่อเมื่อวัตถุทั้งหมดมีความสูงของเลเยอร์ที่แปรผันเท่ากัน" -msgid "" -"One or more object were assigned an extruder that the printer does not have." +msgid "One or more object were assigned an extruder that the printer does not have." msgstr "วัตถุอย่างน้อยหนึ่งชิ้นถูกกำหนดให้เป็นชุดดันเส้นที่เครื่องพิมพ์ไม่มี" msgid "Line width too small" @@ -12014,53 +10858,31 @@ msgstr "ความกว้างของเส้นเล็กเกิน msgid "Line width too large" msgstr "ความกว้างของเส้นใหญ่เกินไป" -msgid "" -"Printing with multiple extruders of differing nozzle diameters. If support " -"is to be printed with the current filament (support_filament == 0 or " -"support_interface_filament == 0), all nozzles have to be of the same " -"diameter." -msgstr "" -"การพิมพ์ด้วยชุดดันเส้นหลายเครื่องที่มีเส้นผ่านศูนย์กลางหัวฉีดต่างกัน " -"หากจะพิมพ์ส่วนรองรับด้วยฟิลาเมนต์ปัจจุบัน (support_filament == 0 หรือ " -"support_interface_filament == 0) หัวฉีดทั้งหมดจะต้องมีเส้นผ่านศูนย์กลางเท่ากัน" +msgid "Printing with multiple extruders of differing nozzle diameters. If support is to be printed with the current filament (support_filament == 0 or support_interface_filament == 0), all nozzles have to be of the same diameter." +msgstr "การพิมพ์ด้วยชุดดันเส้นหลายเครื่องที่มีเส้นผ่านศูนย์กลางหัวฉีดต่างกัน หากจะพิมพ์ส่วนรองรับด้วยฟิลาเมนต์ปัจจุบัน (support_filament == 0 หรือ support_interface_filament == 0) หัวฉีดทั้งหมดจะต้องมีเส้นผ่านศูนย์กลางเท่ากัน" -msgid "" -"A prime tower requires that support has the same layer height as the object." +msgid "A prime tower requires that support has the same layer height as the object." msgstr "ไพรม์ทาวเวอร์ต้องการให้ส่วนรองรับมีความสูงชั้นเท่ากับวัตถุ" -msgid "" -"For Organic supports, two walls are supported only with the Hollow/Default " -"base pattern." -msgstr "" -"สำหรับการรองรับแบบออร์แกนิก ผนังทั้งสองได้รับการรองรับด้วยรูปแบบฐานกลวง/ค่าเริ่มต้นเท่านั้น" +msgid "For Organic supports, two walls are supported only with the Hollow/Default base pattern." +msgstr "สำหรับการรองรับแบบออร์แกนิก ผนังทั้งสองได้รับการรองรับด้วยรูปแบบฐานกลวง/ค่าเริ่มต้นเท่านั้น" -msgid "" -"The Lightning base pattern is not supported by this support type; " -"Rectilinear will be used instead." +msgid "The Lightning base pattern is not supported by this support type; Rectilinear will be used instead." msgstr "รูปแบบฐาน Lightning ไม่ได้รับส่วนรองรับโดยประเภทส่วนรองรับนี้ จะใช้เส้นตรงแทน" -msgid "" -"Organic support tree tip diameter must not be smaller than support material " -"extrusion width." +msgid "Organic support tree tip diameter must not be smaller than support material extrusion width." msgstr "เส้นผ่านศูนย์กลางปลายของฐานรองรับแบบออร์แกนิกต้องไม่เล็กกว่าความกว้างของการอัดขึ้นรูปวัสดุรองรับ" -msgid "" -"Organic support branch diameter must not be smaller than 2x support material " -"extrusion width." +msgid "Organic support branch diameter must not be smaller than 2x support material extrusion width." msgstr "เส้นผ่านศูนย์กลางกิ่งรองรับออร์แกนิกต้องไม่เล็กกว่า 2x ความกว้างของการอัดขึ้นรูปวัสดุรองรับ" -msgid "" -"Organic support branch diameter must not be smaller than support tree tip " -"diameter." +msgid "Organic support branch diameter must not be smaller than support tree tip diameter." msgstr "เส้นผ่านศูนย์กลางกิ่งรองรับอินทรีย์ต้องไม่เล็กกว่าเส้นผ่านศูนย์กลางปลายต้นรองรับ" -msgid "" -"The Hollow base pattern is not supported by this support type; Rectilinear " -"will be used instead." +msgid "The Hollow base pattern is not supported by this support type; Rectilinear will be used instead." msgstr "รูปแบบฐานกลวงไม่รองรับประเภทการรองรับนี้ จะใช้เส้นตรงแทน" -msgid "" -"Support enforcers are used but support is not enabled. Please enable support." +msgid "Support enforcers are used but support is not enabled. Please enable support." msgstr "มีการใช้กำหนดส่วนรองรับ แต่ไม่ได้เปิดใช้งานส่วนรองรับ กรุณาเปิดใช้งานส่วนรองรับ" msgid "Layer height cannot exceed nozzle diameter." @@ -12069,115 +10891,71 @@ msgstr "ความสูงของชั้นต้องไม่เกิ msgid "Bridge line width must not exceed nozzle diameter" msgstr "ความกว้างเส้นสะพานต้องไม่เกินเส้นผ่านศูนย์กลางหัวฉีด" -msgid "" -"\"G92 E0\" was found in before_layer_change_gcode, but the G or E are not " -"uppercase. Please change them to the exact uppercase \"G92 E0\"." -msgstr "" -"พบ \"G92 E0\" ใน before_layer_change_gcode แต่ G หรือ E ไม่ได้เป็นตัวพิมพ์ใหญ่ " -"กรุณาเปลี่ยนให้ตรงกับ \"G92 E0\" ตัวพิมพ์ใหญ่ทุกตัว" +msgid "\"G92 E0\" was found in before_layer_change_gcode, but the G or E are not uppercase. Please change them to the exact uppercase \"G92 E0\"." +msgstr "พบ \"G92 E0\" ใน before_layer_change_gcode แต่ G หรือ E ไม่ได้เป็นตัวพิมพ์ใหญ่ กรุณาเปลี่ยนให้ตรงกับ \"G92 E0\" ตัวพิมพ์ใหญ่ทุกตัว" -msgid "" -"\"G92 E0\" was found in layer_change_gcode, but the G or E are not " -"uppercase. Please change them to the exact uppercase \"G92 E0\"." -msgstr "" -"พบ \"G92 E0\" ใน layer_change_gcode แต่ G หรือ E ไม่ได้เป็นตัวพิมพ์ใหญ่ " -"กรุณาเปลี่ยนให้ตรงกับ \"G92 E0\" ตัวพิมพ์ใหญ่ทุกตัว" +msgid "\"G92 E0\" was found in layer_change_gcode, but the G or E are not uppercase. Please change them to the exact uppercase \"G92 E0\"." +msgstr "พบ \"G92 E0\" ใน layer_change_gcode แต่ G หรือ E ไม่ได้เป็นตัวพิมพ์ใหญ่ กรุณาเปลี่ยนให้ตรงกับ \"G92 E0\" ตัวพิมพ์ใหญ่ทุกตัว" -msgid "" -"Relative extruder addressing requires resetting the extruder position at " -"each layer to prevent loss of floating point accuracy. Add \"G92 E0\" to " -"layer_gcode." -msgstr "" -"การระบุตำแหน่งชุดดันเส้นแบบสัมพันธ์จำเป็นต้องรีเซ็ตตำแหน่งชุดดันเส้นในแต่ละชั้น " -"เพื่อป้องกันการสูญเสียความแม่นยำของจุดลอยตัว เพิ่ม \"G92 E0\" ลงใน layer_gcode" +msgid "Relative extruder addressing requires resetting the extruder position at each layer to prevent loss of floating point accuracy. Add \"G92 E0\" to layer_gcode." +msgstr "การระบุตำแหน่งชุดดันเส้นแบบสัมพันธ์จำเป็นต้องรีเซ็ตตำแหน่งชุดดันเส้นในแต่ละชั้น เพื่อป้องกันการสูญเสียความแม่นยำของจุดลอยตัว เพิ่ม \"G92 E0\" ลงใน layer_gcode" -msgid "" -"\"G92 E0\" was found in before_layer_change_gcode, which is incompatible " -"with absolute extruder addressing." -msgstr "" -"พบ \"G92 E0\" ใน before_layer_change_gcode " -"ซึ่งไม่เข้ากันกับการกำหนดที่อยู่ชุดดันเส้นแบบสัมบูรณ์" +msgid "\"G92 E0\" was found in before_layer_change_gcode, which is incompatible with absolute extruder addressing." +msgstr "พบ \"G92 E0\" ใน before_layer_change_gcode ซึ่งไม่เข้ากันกับการกำหนดที่อยู่ชุดดันเส้นแบบสัมบูรณ์" -msgid "" -"\"G92 E0\" was found in layer_change_gcode, which is incompatible with " -"absolute extruder addressing." -msgstr "" -"พบ \"G92 E0\" ใน layer_change_gcode ซึ่งไม่เข้ากันกับการกำหนดที่อยู่ชุดดันเส้นแบบสัมบูรณ์" +msgid "\"G92 E0\" was found in layer_change_gcode, which is incompatible with absolute extruder addressing." +msgstr "พบ \"G92 E0\" ใน layer_change_gcode ซึ่งไม่เข้ากันกับการกำหนดที่อยู่ชุดดันเส้นแบบสัมบูรณ์" #, c-format, boost-format msgid "Plate %d: %s does not support filament %s" msgstr "ฐานพิมพ์ %d: %s ไม่รองรับเส้นพลาสติก %s" -msgid "" -"Setting the jerk speed too low could lead to artifacts on curved surfaces" +msgid "Setting the jerk speed too low could lead to artifacts on curved surfaces" msgstr "การตั้งค่าความเร็วกระตุกต่ำเกินไปอาจทำให้เกิดสิ่งแปลกปลอมบนพื้นผิวโค้งได้" msgid "" -"The jerk setting exceeds the printer's maximum jerk (machine_max_jerk_x/" -"machine_max_jerk_y).\n" -"Orca will automatically cap the jerk speed to ensure it doesn't surpass the " -"printer's capabilities.\n" -"You can adjust the maximum jerk setting in your printer's configuration to " -"get higher speeds." +"The jerk setting exceeds the printer's maximum jerk (machine_max_jerk_x/machine_max_jerk_y).\n" +"Orca will automatically cap the jerk speed to ensure it doesn't surpass the printer's capabilities.\n" +"You can adjust the maximum jerk setting in your printer's configuration to get higher speeds." msgstr "" -"การตั้งค่าการกระตุกเกินการกระตุกสูงสุดของเครื่องพิมพ์ (machine_max_jerk_x/" -"machine_max_jerk_y)\n" +"การตั้งค่าการกระตุกเกินการกระตุกสูงสุดของเครื่องพิมพ์ (machine_max_jerk_x/machine_max_jerk_y)\n" "Orca จะจำกัดความเร็วการกระตุกโดยอัตโนมัติเพื่อให้แน่ใจว่าจะไม่เกินความสามารถของเครื่องพิมพ์\n" "คุณสามารถปรับการตั้งค่าการกระตุกสูงสุดในการกำหนดค่าเครื่องพิมพ์ของคุณเพื่อให้ได้ความเร็วที่สูงขึ้น" msgid "" -"Junction deviation setting exceeds the printer's maximum value " -"(machine_max_junction_deviation).\n" -"Orca will automatically cap the junction deviation to ensure it doesn't " -"surpass the printer's capabilities.\n" -"You can adjust the machine_max_junction_deviation value in your printer's " -"configuration to get higher limits." +"Junction deviation setting exceeds the printer's maximum value (machine_max_junction_deviation).\n" +"Orca will automatically cap the junction deviation to ensure it doesn't surpass the printer's capabilities.\n" +"You can adjust the machine_max_junction_deviation value in your printer's configuration to get higher limits." msgstr "" -"การตั้งค่าส่วนเบี่ยงเบนของจุดเชื่อมต่อเกินค่าสูงสุดของเครื่องพิมพ์ " -"(machine_max_junction_deviation)\n" -"Orca " -"จะจำกัดส่วนเบี่ยงเบนของจุดเชื่อมต่อโดยอัตโนมัติเพื่อให้แน่ใจว่าจะไม่เกินความสามารถของเครื่องพิมพ์\n" -"คุณสามารถปรับค่า machine_max_junction_deviation " -"ในการกำหนดค่าเครื่องพิมพ์ของคุณเพื่อให้ได้ขีดจำกัดที่สูงขึ้น" +"การตั้งค่าส่วนเบี่ยงเบนของจุดเชื่อมต่อเกินค่าสูงสุดของเครื่องพิมพ์ (machine_max_junction_deviation)\n" +"Orca จะจำกัดส่วนเบี่ยงเบนของจุดเชื่อมต่อโดยอัตโนมัติเพื่อให้แน่ใจว่าจะไม่เกินความสามารถของเครื่องพิมพ์\n" +"คุณสามารถปรับค่า machine_max_junction_deviation ในการกำหนดค่าเครื่องพิมพ์ของคุณเพื่อให้ได้ขีดจำกัดที่สูงขึ้น" msgid "" -"The acceleration setting exceeds the printer's maximum acceleration " -"(machine_max_acceleration_extruding).\n" -"Orca will automatically cap the acceleration speed to ensure it doesn't " -"surpass the printer's capabilities.\n" -"You can adjust the machine_max_acceleration_extruding value in your " -"printer's configuration to get higher speeds." +"The acceleration setting exceeds the printer's maximum acceleration (machine_max_acceleration_extruding).\n" +"Orca will automatically cap the acceleration speed to ensure it doesn't surpass the printer's capabilities.\n" +"You can adjust the machine_max_acceleration_extruding value in your printer's configuration to get higher speeds." msgstr "" -"การตั้งค่าความเร่งเกินความเร่งสูงสุดของเครื่องพิมพ์ " -"(machine_max_acceleration_extruding)\n" -"Orca " -"จะจำกัดความเร็วการเร่งความเร็วโดยอัตโนมัติเพื่อให้แน่ใจว่าจะไม่เกินความสามารถของเครื่องพิมพ์\n" -"คุณสามารถปรับค่า machine_max_acceleration_extruding " -"ในการกำหนดค่าเครื่องพิมพ์ของคุณเพื่อให้ได้ความเร็วที่สูงขึ้น" +"การตั้งค่าความเร่งเกินความเร่งสูงสุดของเครื่องพิมพ์ (machine_max_acceleration_extruding)\n" +"Orca จะจำกัดความเร็วการเร่งความเร็วโดยอัตโนมัติเพื่อให้แน่ใจว่าจะไม่เกินความสามารถของเครื่องพิมพ์\n" +"คุณสามารถปรับค่า machine_max_acceleration_extruding ในการกำหนดค่าเครื่องพิมพ์ของคุณเพื่อให้ได้ความเร็วที่สูงขึ้น" msgid "" -"The travel acceleration setting exceeds the printer's maximum travel " -"acceleration (machine_max_acceleration_travel).\n" -"Orca will automatically cap the travel acceleration speed to ensure it " -"doesn't surpass the printer's capabilities.\n" -"You can adjust the machine_max_acceleration_travel value in your printer's " -"configuration to get higher speeds." +"The travel acceleration setting exceeds the printer's maximum travel acceleration (machine_max_acceleration_travel).\n" +"Orca will automatically cap the travel acceleration speed to ensure it doesn't surpass the printer's capabilities.\n" +"You can adjust the machine_max_acceleration_travel value in your printer's configuration to get higher speeds." msgstr "" -"การตั้งค่าการเร่งความเร็วในการเดินทางเกินความเร็วการเคลื่อนที่สูงสุดของเครื่องพิมพ์ " -"(machine_max_acceleration_travel)\n" -"Orca " -"จะจำกัดความเร็วในการเร่งความเร็วโดยอัตโนมัติเพื่อให้แน่ใจว่าจะไม่เกินความสามารถของเครื่องพิมพ์\n" -"คุณสามารถปรับค่า machine_max_acceleration_travel " -"ในการกำหนดค่าเครื่องพิมพ์ของคุณเพื่อให้ได้ความเร็วที่สูงขึ้น" +"การตั้งค่าการเร่งความเร็วในการเดินทางเกินความเร็วการเคลื่อนที่สูงสุดของเครื่องพิมพ์ (machine_max_acceleration_travel)\n" +"Orca จะจำกัดความเร็วในการเร่งความเร็วโดยอัตโนมัติเพื่อให้แน่ใจว่าจะไม่เกินความสามารถของเครื่องพิมพ์\n" +"คุณสามารถปรับค่า machine_max_acceleration_travel ในการกำหนดค่าเครื่องพิมพ์ของคุณเพื่อให้ได้ความเร็วที่สูงขึ้น" -msgid "" -"The precise wall option will be ignored for outer-inner or inner-outer-inner " -"wall sequences." +msgid "The precise wall option will be ignored for outer-inner or inner-outer-inner wall sequences." msgstr "ตัวเลือกผนังที่แม่นยำจะถูกละเว้นสำหรับลำดับผนังด้านนอก-ด้านใน หรือด้านใน-ด้านนอก-ด้านใน" -msgid "" -"Filament shrinkage will not be used because filament shrinkage for the used " -"filaments does not match." +msgid "The Adaptive Pressure Advance model for one or more extruders may contain invalid values." +msgstr "" + +msgid "Filament shrinkage will not be used because filament shrinkage for the used filaments does not match." msgstr "การหดตัวของเส้นพลาสติกจะไม่ถูกนำมาใช้เนื่องจากการหดตัวของเส้นพลาสติกสำหรับเส้นพลาสติกที่ใช้ไม่ตรงกัน" msgid "Generating skirt & brim" @@ -12204,41 +10982,26 @@ msgstr "พื้นที่การพิมพ์ของชุดดัน msgid "Support parallel printheads" msgstr "รองรับหัวพิมพ์แบบขนาน" -msgid "" -"Enable printer settings for machines that can use multiple printheads in " -"parallel." +msgid "Enable printer settings for machines that can use multiple printheads in parallel." msgstr "เปิดการตั้งค่าเครื่องพิมพ์สำหรับเครื่องที่สามารถใช้หัวพิมพ์หลายหัวพร้อมกันแบบขนาน" msgid "Parallel printheads count" msgstr "จำนวนหัวพิมพ์แบบขนาน" -msgid "" -"Set the number of parallel printheads for machines like OrangeStorm Giga " -"printer." +msgid "Set the number of parallel printheads for machines like OrangeStorm Giga printer." msgstr "ตั้งจำนวนหัวพิมพ์แบบขนานสำหรับเครื่องเช่นเครื่องพิมพ์ OrangeStorm Giga" msgid "Parallel printheads bed exclude areas" msgstr "พื้นที่ยกเว้นบนเพลตตามจำนวนหัวพิมพ์แบบขนาน" -msgid "" -"Ordered list of bed exclude areas by parallel printhead count. Item 1 " -"applies to one printhead, item 2 to two printheads, and so on. Leave an item " -"empty for no excluded area." -msgstr "" -"รายการพื้นที่ยกเว้นบนเพลตตามจำนวนหัวพิมพ์แบบขนาน รายการที่ 1 ใช้กับหัวพิมพ์ 1 หัว รายการที่ 2 " -"ใช้กับ 2 หัว และต่อไปเรื่อยๆ ปล่อยรายการว่างหากไม่ต้องการพื้นที่ยกเว้น" +msgid "Ordered list of bed exclude areas by parallel printhead count. Item 1 applies to one printhead, item 2 to two printheads, and so on. Leave an item empty for no excluded area." +msgstr "รายการพื้นที่ยกเว้นบนเพลตตามจำนวนหัวพิมพ์แบบขนาน รายการที่ 1 ใช้กับหัวพิมพ์ 1 หัว รายการที่ 2 ใช้กับ 2 หัว และต่อไปเรื่อยๆ ปล่อยรายการว่างหากไม่ต้องการพื้นที่ยกเว้น" msgid "Excluded bed area" msgstr "พื้นที่ฐานพิมพ์ที่ยกเว้น" -msgid "" -"Unprintable area in XY plane. For example, X1 Series printers use the front " -"left corner to cut filament during filament change. The area is expressed as " -"polygon by points in following format: \"XxY, XxY, ...\"" -msgstr "" -"พื้นที่ที่ไม่สามารถพิมพ์ได้ในระนาบ XY ตัวอย่างเช่น เครื่องพิมพ์ X1 " -"ซีรีส์ใช้มุมซ้ายด้านหน้าเพื่อตัดเส้นพลาสติกระหว่างการเปลี่ยนเส้นพลาสติก " -"พื้นที่จะแสดงเป็นรูปหลายเหลี่ยมตามจุดในรูปแบบต่อไปนี้: \"XxY, XxY, ...\"" +msgid "Unprintable area in XY plane. For example, X1 Series printers use the front left corner to cut filament during filament change. The area is expressed as polygon by points in following format: \"XxY, XxY, ...\"" +msgstr "พื้นที่ที่ไม่สามารถพิมพ์ได้ในระนาบ XY ตัวอย่างเช่น เครื่องพิมพ์ X1 ซีรีส์ใช้มุมซ้ายด้านหน้าเพื่อตัดเส้นพลาสติกระหว่างการเปลี่ยนเส้นพลาสติก พื้นที่จะแสดงเป็นรูปหลายเหลี่ยมตามจุดในรูปแบบต่อไปนี้: \"XxY, XxY, ...\"" msgid "Bed custom texture" msgstr "พื้นผิวแบบกำหนดเองของฐานพิมพ์" @@ -12249,22 +11012,14 @@ msgstr "ฐานพิมพ์รุ่นสั่งทำพิเศษ" msgid "Elephant foot compensation" msgstr "การชดเชยอาการฐานบาน" -msgid "" -"This shrinks the first layer on the build plate to compensate for elephant " -"foot effect." +msgid "This shrinks the first layer on the build plate to compensate for elephant foot effect." msgstr "ย่อชั้นแรกบนฐานรองพิมพ์เพื่อชดเชยผลกระทบจากรอยอาการฐานบาน" msgid "Elephant foot compensation layers" msgstr "ชั้นชดเชยอาการฐานบาน" -msgid "" -"The number of layers on which the elephant foot compensation will be active. " -"The first layer will be shrunk by the elephant foot compensation value, then " -"the next layers will be linearly shrunk less, up to the layer indicated by " -"this value." -msgstr "" -"จำนวนชั้นที่การชดเชยอาการฐานบานจะทำงาน ชั้นแรกจะหดตามค่าการชดเชยอาการฐานบาน " -"จากนั้นชั้นถัดไปจะหดเป็นเส้นตรงน้อยลง จนถึงชั้นที่ระบุด้วยค่านี้" +msgid "The number of layers on which the elephant foot compensation will be active. The first layer will be shrunk by the elephant foot compensation value, then the next layers will be linearly shrunk less, up to the layer indicated by this value." +msgstr "จำนวนชั้นที่การชดเชยอาการฐานบานจะทำงาน ชั้นแรกจะหดตามค่าการชดเชยอาการฐานบาน จากนั้นชั้นถัดไปจะหดเป็นเส้นตรงน้อยลง จนถึงชั้นที่ระบุด้วยค่านี้" msgid "Elephant foot layers density" msgstr "ความหนาแน่นของชั้นอาการฐานบาน" @@ -12272,34 +11027,25 @@ msgstr "ความหนาแน่นของชั้นอาการฐ msgid "" "Density of internal solid infill for Elephant foot layers compensation.\n" "The initial value for the second layer is set.\n" -"Subsequent layers become linearly denser by the height specified in " -"elefant_foot_compensation_layers." +"Subsequent layers become linearly denser by the height specified in elefant_foot_compensation_layers." msgstr "" "ความหนาแน่นของวัสดุไส้ในแบบทึบภายในสำหรับการชดเชยชั้นอาการฐานบาน\n" "มีการตั้งค่าเริ่มต้นสำหรับเลเยอร์ที่สอง\n" -"เลเยอร์ต่อมาจะมีความหนาแน่นเชิงเส้นมากขึ้นตามความสูงที่ระบุใน " -"elefant_foot_compensation_layers" +"เลเยอร์ต่อมาจะมีความหนาแน่นเชิงเส้นมากขึ้นตามความสูงที่ระบุใน elefant_foot_compensation_layers" -msgid "" -"This is the height for each layer. Smaller layer heights give greater " -"accuracy but longer printing time." -msgstr "" -"ความสูงของการตัดแต่ละชั้น ความสูงของชั้นที่เล็กลงหมายถึงความแม่นยำและเวลาในการพิมพ์ที่มากขึ้น" +msgid "This is the height for each layer. Smaller layer heights give greater accuracy but longer printing time." +msgstr "ความสูงของการตัดแต่ละชั้น ความสูงของชั้นที่เล็กลงหมายถึงความแม่นยำและเวลาในการพิมพ์ที่มากขึ้น" msgid "Printable height" msgstr "ความสูงที่สามารถพิมพ์ได้" -msgid "" -"This is the maximum printable height which is limited by the height of the " -"build area." +msgid "This is the maximum printable height which is limited by the height of the build area." msgstr "ความสูงสูงสุดที่สามารถพิมพ์ได้ซึ่งถูกจำกัดโดยกลไกของเครื่องพิมพ์" msgid "Extruder printable height" msgstr "ความสูงที่สามารถพิมพ์ของชุดดันเส้นได้" -msgid "" -"Maximum printable height of this extruder which is limited by mechanism of " -"printer." +msgid "Maximum printable height of this extruder which is limited by mechanism of printer." msgstr "ความสูงสูงสุดที่สามารถพิมพ์ได้ของชุดดันเส้นนี้ซึ่งถูกจำกัดโดยกลไกของเครื่องพิมพ์" msgid "Preferred orientation" @@ -12320,13 +11066,8 @@ msgstr "อนุญาตให้ควบคุมเครื่องพิ msgid "Use 3MF instead of G-code" msgstr "ใช้ 3MF แทน G-code" -msgid "" -"Enable this if the printer accepts a 3MF file as the print job. When " -"enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a " -"plain .gcode file." -msgstr "" -"เปิดใช้งานหากเครื่องพิมพ์รับไฟล์ 3MF เป็นงานพิมพ์ เมื่อเปิดใช้งาน OrcaSlicer " -"จะส่งไฟล์ที่สไลซ์แล้วเป็น .gcode.3mf แทนไฟล์ .gcode ธรรมดา" +msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." +msgstr "เปิดใช้งานหากเครื่องพิมพ์รับไฟล์ 3MF เป็นงานพิมพ์ เมื่อเปิดใช้งาน OrcaSlicer จะส่งไฟล์ที่สไลซ์แล้วเป็น .gcode.3mf แทนไฟล์ .gcode ธรรมดา" msgid "Printer Agent" msgstr "ตัวแทนเครื่องพิมพ์" @@ -12337,34 +11078,20 @@ msgstr "เลือกการใช้งานตัวแทนเครื msgid "Hostname, IP or URL" msgstr "ชื่อโฮสต์, IP หรือ URL" -msgid "" -"Orca Slicer can upload G-code files to a printer host. This field should " -"contain the hostname, IP address or URL of the printer host instance. Print " -"host behind HAProxy with basic auth enabled can be accessed by putting the " -"user name and password into the URL in the following format: https://" -"username:password@your-octopi-address/" -msgstr "" -"Orca Slicer สามารถอัปโหลดไฟล์ G-code ไปยังโฮสต์เครื่องพิมพ์ได้ ฟิลด์นี้ควรมีชื่อโฮสต์ ที่อยู่ IP " -"หรือ URL ของอินสแตนซ์โฮสต์ของเครื่องพิมพ์ โฮสต์การพิมพ์ที่อยู่เบื้องหลัง HAProxy " -"ที่เปิดใช้งานการรับรองความถูกต้องขั้นพื้นฐานสามารถเข้าถึงได้โดยการใส่ชื่อผู้ใช้และรหัสผ่านลงใน URL " -"ในรูปแบบต่อไปนี้: https://username:password@your-octopi-address/" +msgid "Orca Slicer can upload G-code files to a printer host. This field should contain the hostname, IP address or URL of the printer host instance. Print host behind HAProxy with basic auth enabled can be accessed by putting the user name and password into the URL in the following format: https://username:password@your-octopi-address/" +msgstr "Orca Slicer สามารถอัปโหลดไฟล์ G-code ไปยังโฮสต์เครื่องพิมพ์ได้ ฟิลด์นี้ควรมีชื่อโฮสต์ ที่อยู่ IP หรือ URL ของอินสแตนซ์โฮสต์ของเครื่องพิมพ์ โฮสต์การพิมพ์ที่อยู่เบื้องหลัง HAProxy ที่เปิดใช้งานการรับรองความถูกต้องขั้นพื้นฐานสามารถเข้าถึงได้โดยการใส่ชื่อผู้ใช้และรหัสผ่านลงใน URL ในรูปแบบต่อไปนี้: https://username:password@your-octopi-address/" msgid "Device UI" msgstr "UI ของอุปกรณ์" -msgid "" -"Specify the URL of your device user interface if it's not same as print_host." +msgid "Specify the URL of your device user interface if it's not same as print_host." msgstr "ระบุ URL ของอินเทอร์เฟซผู้ใช้อุปกรณ์ของคุณหากไม่เหมือนกับ print_host" msgid "API Key / Password" msgstr "คีย์ API / รหัสผ่าน" -msgid "" -"Orca Slicer can upload G-code files to a printer host. This field should " -"contain the API Key or the password required for authentication." -msgstr "" -"Orca Slicer สามารถอัปโหลดไฟล์ G-code ไปยังโฮสต์เครื่องพิมพ์ได้ ฟิลด์นี้ควรมีคีย์ API " -"หรือรหัสผ่านที่จำเป็นสำหรับการตรวจสอบสิทธิ์" +msgid "Orca Slicer can upload G-code files to a printer host. This field should contain the API Key or the password required for authentication." +msgstr "Orca Slicer สามารถอัปโหลดไฟล์ G-code ไปยังโฮสต์เครื่องพิมพ์ได้ ฟิลด์นี้ควรมีคีย์ API หรือรหัสผ่านที่จำเป็นสำหรับการตรวจสอบสิทธิ์" msgid "Serial Number" msgstr "หมายเลขซีเรียล" @@ -12378,13 +11105,8 @@ msgstr "ชื่อของเครื่องพิมพ์" msgid "HTTPS CA File" msgstr "ไฟล์ HTTPS CA" -msgid "" -"Custom CA certificate file can be specified for HTTPS OctoPrint connections, " -"in crt/pem format. If left blank, the default OS CA certificate repository " -"is used." -msgstr "" -"สามารถระบุไฟล์ใบรับรอง CA แบบกำหนดเองสำหรับการเชื่อมต่อ HTTPS OctoPrint ในรูปแบบ crt/" -"pem หากเว้นว่างไว้ ระบบจะใช้ที่เก็บใบรับรอง OS CA เริ่มต้น" +msgid "Custom CA certificate file can be specified for HTTPS OctoPrint connections, in crt/pem format. If left blank, the default OS CA certificate repository is used." +msgstr "สามารถระบุไฟล์ใบรับรอง CA แบบกำหนดเองสำหรับการเชื่อมต่อ HTTPS OctoPrint ในรูปแบบ crt/pem หากเว้นว่างไว้ ระบบจะใช้ที่เก็บใบรับรอง OS CA เริ่มต้น" msgid "User" msgstr "ผู้ใช้" @@ -12395,13 +11117,8 @@ msgstr "รหัสผ่าน" msgid "Ignore HTTPS certificate revocation checks" msgstr "ละเว้นการตรวจสอบการเพิกถอนใบรับรอง HTTPS" -msgid "" -"Ignore HTTPS certificate revocation checks in the case of missing or offline " -"distribution points. One may want to enable this option for self signed " -"certificates if connection fails." -msgstr "" -"ละเว้นการตรวจสอบการเพิกถอนใบรับรอง HTTPS ในกรณีที่จุดแจกจ่ายหายไปหรือออฟไลน์ " -"อาจต้องการเปิดใช้งานตัวเลือกนี้สำหรับใบรับรองที่ลงนามด้วยตนเองหากการเชื่อมต่อล้มเหลว" +msgid "Ignore HTTPS certificate revocation checks in the case of missing or offline distribution points. One may want to enable this option for self signed certificates if connection fails." +msgstr "ละเว้นการตรวจสอบการเพิกถอนใบรับรอง HTTPS ในกรณีที่จุดแจกจ่ายหายไปหรือออฟไลน์ อาจต้องการเปิดใช้งานตัวเลือกนี้สำหรับใบรับรองที่ลงนามด้วยตนเองหากการเชื่อมต่อล้มเหลว" msgid "Names of presets related to the physical printer." msgstr "ชื่อของค่าที่ตั้งล่วงหน้าที่เกี่ยวข้องกับเครื่องพิมพ์จริง" @@ -12418,23 +11135,14 @@ msgstr "HTTP ไดเจสต์" msgid "Avoid crossing walls" msgstr "หลีกเลี่ยงการข้ามผนัง" -msgid "" -"This detours to avoid traveling across walls, which may cause blobs on the " -"surface." +msgid "This detours to avoid traveling across walls, which may cause blobs on the surface." msgstr "ทางอ้อมเพื่อหลีกเลี่ยงการเดินทางข้ามผนัง ซึ่งอาจทำให้เกิดหยดบนพื้นผิว" msgid "Avoid crossing walls - Max detour length" msgstr "หลีกเลี่ยงการข้ามผนัง - ความยาวทางเบี่ยงสูงสุด" -msgid "" -"Maximum detour distance for avoiding crossing wall: The printer won't detour " -"if the detour distance is larger than this value. Detour length could be " -"specified either as an absolute value or as percentage (for example 50%) of " -"a direct travel path. A value of 0 will disable this." -msgstr "" -"ระยะทางเบี่ยงสูงสุดเพื่อหลีกเลี่ยงการข้ามผนัง เครื่องพิมพ์จะไม่อ้อมหากระยะทางเบี่ยงมากกว่าค่านี้ " -"ความยาวทางเบี่ยงสามารถระบุเป็นค่าสัมบูรณ์หรือเป็นเปอร์เซ็นต์ (เช่น 50%) " -"ของเส้นทางเดินทางโดยตรง ค่า 0 จะปิดการใช้งานตัวเลือกนี้" +msgid "Maximum detour distance for avoiding crossing wall: The printer won't detour if the detour distance is larger than this value. Detour length could be specified either as an absolute value or as percentage (for example 50%) of a direct travel path. A value of 0 will disable this." +msgstr "ระยะทางเบี่ยงสูงสุดเพื่อหลีกเลี่ยงการข้ามผนัง เครื่องพิมพ์จะไม่อ้อมหากระยะทางเบี่ยงมากกว่าค่านี้ ความยาวทางเบี่ยงสามารถระบุเป็นค่าสัมบูรณ์หรือเป็นเปอร์เซ็นต์ (เช่น 50%) ของเส้นทางเดินทางโดยตรง ค่า 0 จะปิดการใช้งานตัวเลือกนี้" msgid "mm or %" msgstr "มม. หรือ %" @@ -12442,47 +11150,23 @@ msgstr "มม. หรือ %" msgid "Other layers" msgstr "ชั้นอื่นๆ" -msgid "" -"Bed temperature for layers except the initial one. A value of 0 means the " -"filament does not support printing on the Cool Plate SuperTack." -msgstr "" -"อุณหภูมิฐานพิมพ์สำหรับชั้นต่างๆ ยกเว้นอุณหภูมิเริ่มต้น ค่า 0 " -"หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บน Cool Plate SuperTack" +msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." +msgstr "อุณหภูมิฐานพิมพ์สำหรับชั้นต่างๆ ยกเว้นอุณหภูมิเริ่มต้น ค่า 0 หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บน Cool Plate SuperTack" -msgid "" -"This is the bed temperature for layers except for the first one. A value of " -"0 means the filament does not support printing on the Cool Plate." -msgstr "" -"อุณหภูมิฐานพิมพ์สำหรับชั้นต่างๆ ยกเว้นอุณหภูมิเริ่มต้น ค่า 0 " -"หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บน Cool Plate" +msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate." +msgstr "อุณหภูมิฐานพิมพ์สำหรับชั้นต่างๆ ยกเว้นอุณหภูมิเริ่มต้น ค่า 0 หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บน Cool Plate" -msgid "" -"This is the bed temperature for layers except for the first one. A value of " -"0 means the filament does not support printing on the Textured Cool Plate." -msgstr "" -"อุณหภูมิฐานพิมพ์สำหรับชั้นต่างๆ ยกเว้นอุณหภูมิเริ่มต้น ค่า 0 " -"หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บน Textured Cool Plate" +msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Textured Cool Plate." +msgstr "อุณหภูมิฐานพิมพ์สำหรับชั้นต่างๆ ยกเว้นอุณหภูมิเริ่มต้น ค่า 0 หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บน Textured Cool Plate" -msgid "" -"This is the bed temperature for layers except for the first one. A value of " -"0 means the filament does not support printing on the Engineering Plate." -msgstr "" -"อุณหภูมิฐานพิมพ์สำหรับชั้นต่างๆ ยกเว้นอุณหภูมิเริ่มต้น ค่า 0 " -"หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บน Engineering Plate" +msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Engineering Plate." +msgstr "อุณหภูมิฐานพิมพ์สำหรับชั้นต่างๆ ยกเว้นอุณหภูมิเริ่มต้น ค่า 0 หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บน Engineering Plate" -msgid "" -"This is the bed temperature for layers except for the first one. A value of " -"0 means the filament does not support printing on the High Temp Plate." -msgstr "" -"อุณหภูมิฐานพิมพ์สำหรับชั้นต่างๆ ยกเว้นอุณหภูมิเริ่มต้น ค่า 0 " -"หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บนแผ่นอุณหภูมิสูง" +msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the High Temp Plate." +msgstr "อุณหภูมิฐานพิมพ์สำหรับชั้นต่างๆ ยกเว้นอุณหภูมิเริ่มต้น ค่า 0 หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บนแผ่นอุณหภูมิสูง" -msgid "" -"This is the bed temperature for layers except for the first one. A value of " -"0 means the filament does not support printing on the Textured PEI Plate." -msgstr "" -"อุณหภูมิฐานพิมพ์สำหรับชั้นต่างๆ ยกเว้นอุณหภูมิเริ่มต้น ค่า 0 " -"หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บนเพลต PEI ที่มีพื้นผิว" +msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Textured PEI Plate." +msgstr "อุณหภูมิฐานพิมพ์สำหรับชั้นต่างๆ ยกเว้นอุณหภูมิเริ่มต้น ค่า 0 หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บนเพลต PEI ที่มีพื้นผิว" msgid "First layer" msgstr "ชั้นแรก" @@ -12490,39 +11174,23 @@ msgstr "ชั้นแรก" msgid "First layer bed temperature" msgstr "อุณหภูมิฐานพิมพ์ชั้นแรก" -msgid "" -"This is the bed temperature of the first layer. A value of 0 means the " -"filament does not support printing on the Cool Plate SuperTack." -msgstr "" -"อุณหภูมิฐานพิมพ์ชั้นแรก ค่า 0 หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บน Cool Plate SuperTack" +msgid "This is the bed temperature of the first layer. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." +msgstr "อุณหภูมิฐานพิมพ์ชั้นแรก ค่า 0 หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บน Cool Plate SuperTack" -msgid "" -"This is the bed temperature of the first layer. A value of 0 means the " -"filament does not support printing on the Cool Plate." +msgid "This is the bed temperature of the first layer. A value of 0 means the filament does not support printing on the Cool Plate." msgstr "อุณหภูมิฐานพิมพ์ชั้นแรก ค่า 0 หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บน Cool Plate" -msgid "" -"This is the bed temperature of the first layer. A value of 0 means the " -"filament does not support printing on the Textured Cool Plate." -msgstr "" -"อุณหภูมิฐานพิมพ์ชั้นแรก ค่า 0 หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บน Textured Cool Plate" +msgid "This is the bed temperature of the first layer. A value of 0 means the filament does not support printing on the Textured Cool Plate." +msgstr "อุณหภูมิฐานพิมพ์ชั้นแรก ค่า 0 หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บน Textured Cool Plate" -msgid "" -"This is the bed temperature of the first layer. A value of 0 means the " -"filament does not support printing on the Engineering Plate." -msgstr "" -"อุณหภูมิฐานพิมพ์ชั้นแรก ค่า 0 หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บน Engineering Plate" +msgid "This is the bed temperature of the first layer. A value of 0 means the filament does not support printing on the Engineering Plate." +msgstr "อุณหภูมิฐานพิมพ์ชั้นแรก ค่า 0 หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บน Engineering Plate" -msgid "" -"This is the bed temperature of the first layer. A value of 0 means the " -"filament does not support printing on the High Temp Plate." +msgid "This is the bed temperature of the first layer. A value of 0 means the filament does not support printing on the High Temp Plate." msgstr "อุณหภูมิฐานพิมพ์ชั้นแรก ค่า 0 หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บนแผ่นอุณหภูมิสูง" -msgid "" -"This is the bed temperature of the first layer. A value of 0 means the " -"filament does not support printing on the Textured PEI Plate." -msgstr "" -"อุณหภูมิฐานพิมพ์ชั้นแรก ค่า 0 หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บนเพลต PEI ที่มีพื้นผิว" +msgid "This is the bed temperature of the first layer. A value of 0 means the filament does not support printing on the Textured PEI Plate." +msgstr "อุณหภูมิฐานพิมพ์ชั้นแรก ค่า 0 หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บนเพลต PEI ที่มีพื้นผิว" msgid "Plate types supported by the printer" msgstr "ประเภทฐานพิมพ์ที่เครื่องพิมพ์รองรับ" @@ -12530,8 +11198,7 @@ msgstr "ประเภทฐานพิมพ์ที่เครื่อง msgid "Default bed type" msgstr "ประเภทฐานพิมพ์เริ่มต้น" -msgid "" -"Default bed type for the printer (supports both numeric and string format)." +msgid "Default bed type for the printer (supports both numeric and string format)." msgstr "ประเภทฐานฐานพิมพ์เริ่มต้นสำหรับเครื่องพิมพ์ (รองรับทั้งรูปแบบตัวเลขและสตริง)" msgid "First layer print sequence" @@ -12552,82 +11219,44 @@ msgstr "G-code นี้จะถูกแทรกทุกครั้งท msgid "Bottom shell layers" msgstr "เลเยอร์ผนังด้านล่าง" -msgid "" -"This is the number of solid layers of bottom shell, including the bottom " -"surface layer. When the thickness calculated by this value is thinner than " -"bottom shell thickness, the bottom shell layers will be increased." -msgstr "" -"นี่คือจำนวนชั้นทึบของเปลือกด้านล่าง รวมถึงชั้นล่างของพื้นผิวด้วย " -"เมื่อความหนาที่คำนวณโดยค่านี้บางกว่าความหนาของเปลือกด้านล่าง ชั้นเปลือกด้านล่างจะเพิ่มขึ้น" +msgid "This is the number of solid layers of bottom shell, including the bottom surface layer. When the thickness calculated by this value is thinner than bottom shell thickness, the bottom shell layers will be increased." +msgstr "นี่คือจำนวนชั้นทึบของเปลือกด้านล่าง รวมถึงชั้นล่างของพื้นผิวด้วย เมื่อความหนาที่คำนวณโดยค่านี้บางกว่าความหนาของเปลือกด้านล่าง ชั้นเปลือกด้านล่างจะเพิ่มขึ้น" msgid "Bottom shell thickness" msgstr "ความหนาผนังด้านล่าง" -msgid "" -"The number of bottom solid layers is increased when slicing if the thickness " -"calculated by bottom shell layers is thinner than this value. This can avoid " -"having too thin a shell when layer height is small. 0 means that this " -"setting is disabled and the thickness of the bottom shell is determined " -"simply by the number of bottom shell layers." -msgstr "" -"จำนวนชั้นทึบด้านล่างจะเพิ่มขึ้นเมื่อสไลซ์หากความหนาที่คำนวณโดยชั้นเปลือกด้านล่างบางกว่าค่านี้ " -"วิธีนี้สามารถหลีกเลี่ยงไม่ให้เปลือกบางเกินไปเมื่อชั้นมีความสูงน้อย 0 " -"หมายความว่าการตั้งค่านี้ถูกปิดใช้งาน " -"และความหนาของเปลือกด้านล่างจะถูกกำหนดโดยชั้นเปลือกด้านล่างอย่างแน่นอน" +msgid "The number of bottom solid layers is increased when slicing if the thickness calculated by bottom shell layers is thinner than this value. This can avoid having too thin a shell when layer height is small. 0 means that this setting is disabled and the thickness of the bottom shell is determined simply by the number of bottom shell layers." +msgstr "จำนวนชั้นทึบด้านล่างจะเพิ่มขึ้นเมื่อสไลซ์หากความหนาที่คำนวณโดยชั้นเปลือกด้านล่างบางกว่าค่านี้ วิธีนี้สามารถหลีกเลี่ยงไม่ให้เปลือกบางเกินไปเมื่อชั้นมีความสูงน้อย 0 หมายความว่าการตั้งค่านี้ถูกปิดใช้งาน และความหนาของเปลือกด้านล่างจะถูกกำหนดโดยชั้นเปลือกด้านล่างอย่างแน่นอน" msgid "Apply gap fill" msgstr "ใช้การเติมช่องว่าง" msgid "" -"Enables gap fill for the selected solid surfaces. The minimum gap length " -"that will be filled can be controlled from the filter out tiny gaps option " -"below.\n" +"Enables gap fill for the selected solid surfaces. The minimum gap length that will be filled can be controlled from the filter out tiny gaps option below.\n" "\n" "Options:\n" -"1. Everywhere: Applies gap fill to top, bottom and internal solid surfaces " -"for maximum strength\n" -"2. Top and Bottom surfaces: Applies gap fill to top and bottom surfaces " -"only, balancing print speed, reducing potential over extrusion in the solid " -"infill and making sure the top and bottom surfaces have no pinhole gaps\n" +"1. Everywhere: Applies gap fill to top, bottom and internal solid surfaces for maximum strength\n" +"2. Top and Bottom surfaces: Applies gap fill to top and bottom surfaces only, balancing print speed, reducing potential over extrusion in the solid infill and making sure the top and bottom surfaces have no pinhole gaps\n" "3. Nowhere: Disables gap fill for all solid infill areas\n" "\n" -"Note that if using the classic perimeter generator, gap fill may also be " -"generated between perimeters, if a full width line cannot fit between them. " -"That perimeter gap fill is not controlled by this setting.\n" +"Note that if using the classic perimeter generator, gap fill may also be generated between perimeters, if a full width line cannot fit between them. That perimeter gap fill is not controlled by this setting.\n" "\n" -"If you would like all gap fill, including the classic perimeter generated " -"one, removed, set the filter out tiny gaps value to a large number, like " -"999999.\n" +"If you would like all gap fill, including the classic perimeter generated one, removed, set the filter out tiny gaps value to a large number, like 999999.\n" "\n" -"However this is not advised, as gap fill between perimeters is contributing " -"to the model's strength. For models where excessive gap fill is generated " -"between perimeters, a better option would be to switch to the arachne wall " -"generator and use this option to control whether the cosmetic top and bottom " -"surface gap fill is generated." +"However this is not advised, as gap fill between perimeters is contributing to the model's strength. For models where excessive gap fill is generated between perimeters, a better option would be to switch to the arachne wall generator and use this option to control whether the cosmetic top and bottom surface gap fill is generated." msgstr "" -"เปิดใช้งานการเติมช่องว่างสำหรับพื้นผิวทึบที่เลือก " -"ความยาวช่องว่างขั้นต่ำที่จะถูกอุดสามารถควบคุมได้จากตัวเลือกกรองช่องว่างเล็กๆ ด้านล่าง\n" +"เปิดใช้งานการเติมช่องว่างสำหรับพื้นผิวทึบที่เลือก ความยาวช่องว่างขั้นต่ำที่จะถูกอุดสามารถควบคุมได้จากตัวเลือกกรองช่องว่างเล็กๆ ด้านล่าง\n" "\n" "ตัวเลือก:\n" "1. ทุกที่: ใช้การเติมช่องว่างกับพื้นผิวแข็งด้านบน ด้านล่าง และภายในเพื่อความแข็งแรงสูงสุด\n" -"2. พื้นผิวด้านบนและด้านล่าง: ใช้การเติมช่องว่างกับพื้นผิวด้านบนและด้านล่างเท่านั้น " -"ปรับสมดุลความเร็วการพิมพ์ ลดโอกาสเกิดการอัดขึ้นรูปมากเกินไปเมื่อเติมช่องว่างในพื้นที่ทึบ " -"และตรวจสอบให้แน่ใจว่าพื้นผิวด้านบนและด้านล่างไม่มีช่องว่างรูเข็ม\n" +"2. พื้นผิวด้านบนและด้านล่าง: ใช้การเติมช่องว่างกับพื้นผิวด้านบนและด้านล่างเท่านั้น ปรับสมดุลความเร็วการพิมพ์ ลดโอกาสเกิดการอัดขึ้นรูปมากเกินไปเมื่อเติมช่องว่างในพื้นที่ทึบ และตรวจสอบให้แน่ใจว่าพื้นผิวด้านบนและด้านล่างไม่มีช่องว่างรูเข็ม\n" "3. ไม่มีที่ไหนเลย: ปิดใช้งานการเติมช่องว่างสำหรับพื้นที่ทึบทั้งหมด\n" "\n" -"โปรดทราบว่าหากใช้เครื่องสร้างเส้นรอบวงแบบคลาสสิก " -"การเติมช่องว่างอาจถูกสร้างขึ้นระหว่างเส้นรอบวงด้วย " -"หากเส้นความกว้างเต็มไม่สามารถพอดีระหว่างเส้นรอบวงได้ " -"การเติมช่องว่างขอบเขตนั้นไม่ได้ถูกควบคุมโดยการตั้งค่านี้\n" +"โปรดทราบว่าหากใช้เครื่องสร้างเส้นรอบวงแบบคลาสสิก การเติมช่องว่างอาจถูกสร้างขึ้นระหว่างเส้นรอบวงด้วย หากเส้นความกว้างเต็มไม่สามารถพอดีระหว่างเส้นรอบวงได้ การเติมช่องว่างขอบเขตนั้นไม่ได้ถูกควบคุมโดยการตั้งค่านี้\n" "\n" -"หากคุณต้องการให้ลบการเติมช่องว่างทั้งหมด รวมถึงเส้นรอบวงแบบคลาสสิกที่สร้างไว้ " -"ให้ตั้งค่าตัวกรองออกค่าช่องว่างเล็กๆ ให้เป็นตัวเลขจำนวนมาก เช่น 999999\n" +"หากคุณต้องการให้ลบการเติมช่องว่างทั้งหมด รวมถึงเส้นรอบวงแบบคลาสสิกที่สร้างไว้ ให้ตั้งค่าตัวกรองออกค่าช่องว่างเล็กๆ ให้เป็นตัวเลขจำนวนมาก เช่น 999999\n" "\n" -"อย่างไรก็ตาม ไม่แนะนำให้ทำเช่นนี้ " -"เนื่องจากการเติมช่องว่างระหว่างเส้นรอบวงมีส่วนทำให้โมเดลมีความแข็งแกร่ง " -"สำหรับโมเดลที่มีการเติมช่องว่างมากเกินไประหว่างเส้นรอบวง " -"ตัวเลือกที่ดีกว่าคือเปลี่ยนไปใช้เครื่องสร้างผนังแบบ Arachne " -"และใช้ตัวเลือกนี้เพื่อควบคุมว่าจะสร้างการเติมช่องว่างบนพื้นผิวด้านบนและด้านล่างที่สวยงามหรือไม่" +"อย่างไรก็ตาม ไม่แนะนำให้ทำเช่นนี้ เนื่องจากการเติมช่องว่างระหว่างเส้นรอบวงมีส่วนทำให้โมเดลมีความแข็งแกร่ง สำหรับโมเดลที่มีการเติมช่องว่างมากเกินไประหว่างเส้นรอบวง ตัวเลือกที่ดีกว่าคือเปลี่ยนไปใช้เครื่องสร้างผนังแบบ Arachne และใช้ตัวเลือกนี้เพื่อควบคุมว่าจะสร้างการเติมช่องว่างบนพื้นผิวด้านบนและด้านล่างที่สวยงามหรือไม่" msgid "Everywhere" msgstr "ทุกที่" @@ -12641,52 +11270,27 @@ msgstr "ไม่มีที่ไหนเลย" msgid "Force cooling for overhangs and bridges" msgstr "บังคับการระบายความร้อนสำหรับส่วนยื่นและสะพาน" -msgid "" -"Enable this option to allow adjustment of the part cooling fan speed for " -"specifically for overhangs, internal and external bridges. Setting the fan " -"speed specifically for these features can improve overall print quality and " -"reduce warping." -msgstr "" -"เปิดใช้งานตัวเลือกนี้เพื่อให้สามารถปรับความเร็วพัดลมระบายความร้อนของชิ้นส่วนสำหรับส่วนยื่น " -"บริดจ์ภายในและภายนอกโดยเฉพาะ " -"การตั้งค่าความเร็วพัดลมสำหรับคุณสมบัติเหล่านี้โดยเฉพาะสามารถปรับปรุงคุณภาพการพิมพ์โดยรวมและลดการบิดเบี้ยวได้" +msgid "Enable this option to allow adjustment of the part cooling fan speed for specifically for overhangs, internal and external bridges. Setting the fan speed specifically for these features can improve overall print quality and reduce warping." +msgstr "เปิดใช้งานตัวเลือกนี้เพื่อให้สามารถปรับความเร็วพัดลมระบายความร้อนของชิ้นส่วนสำหรับส่วนยื่น บริดจ์ภายในและภายนอกโดยเฉพาะ การตั้งค่าความเร็วพัดลมสำหรับคุณสมบัติเหล่านี้โดยเฉพาะสามารถปรับปรุงคุณภาพการพิมพ์โดยรวมและลดการบิดเบี้ยวได้" msgid "Overhangs and external bridges fan speed" msgstr "ส่วนยื่นและความเร็วพัดลมของสะพานภายนอก" msgid "" -"Use this part cooling fan speed when printing bridges or overhang walls with " -"an overhang threshold that exceeds the value set in the 'Overhangs cooling " -"threshold' parameter above. Increasing the cooling specifically for " -"overhangs and bridges can improve the overall print quality of these " -"features.\n" +"Use this part cooling fan speed when printing bridges or overhang walls with an overhang threshold that exceeds the value set in the 'Overhangs cooling threshold' parameter above. Increasing the cooling specifically for overhangs and bridges can improve the overall print quality of these features.\n" "\n" -"Please note, this fan speed is clamped on the lower end by the minimum fan " -"speed threshold set above. It is also adjusted upwards up to the maximum fan " -"speed threshold when the minimum layer time threshold is not met." +"Please note, this fan speed is clamped on the lower end by the minimum fan speed threshold set above. It is also adjusted upwards up to the maximum fan speed threshold when the minimum layer time threshold is not met." msgstr "" -"ใช้ความเร็วพัดลมระบายความร้อนส่วนนี้เมื่อพิมพ์สะพานหรือผนังที่ยื่นออกมาโดยมีเกณฑ์ยื่นเกินค่าที่ตั้งไว้ในพารามิเตอร์ " -"'เกณฑ์การระบายความร้อนที่ยื่นเกิน' ด้านบน " -"การเพิ่มการระบายความร้อนโดยเฉพาะสำหรับส่วนยื่นและสะพานสามารถปรับปรุงคุณภาพการพิมพ์โดยรวมของคุณสมบัติเหล่านี้ได้\n" +"ใช้ความเร็วพัดลมระบายความร้อนส่วนนี้เมื่อพิมพ์สะพานหรือผนังที่ยื่นออกมาโดยมีเกณฑ์ยื่นเกินค่าที่ตั้งไว้ในพารามิเตอร์ 'เกณฑ์การระบายความร้อนที่ยื่นเกิน' ด้านบน การเพิ่มการระบายความร้อนโดยเฉพาะสำหรับส่วนยื่นและสะพานสามารถปรับปรุงคุณภาพการพิมพ์โดยรวมของคุณสมบัติเหล่านี้ได้\n" "\n" -"โปรดทราบว่าความเร็วพัดลมนี้จะถูกยึดที่ปลายล่างตามเกณฑ์ความเร็วพัดลมขั้นต่ำที่ตั้งไว้ด้านบน " -"นอกจากนี้ยังปรับขึ้นไปถึงขีดจำกัดความเร็วพัดลมสูงสุด เมื่อไม่ตรงตามเกณฑ์เวลาเลเยอร์ขั้นต่ำ" +"โปรดทราบว่าความเร็วพัดลมนี้จะถูกยึดที่ปลายล่างตามเกณฑ์ความเร็วพัดลมขั้นต่ำที่ตั้งไว้ด้านบน นอกจากนี้ยังปรับขึ้นไปถึงขีดจำกัดความเร็วพัดลมสูงสุด เมื่อไม่ตรงตามเกณฑ์เวลาเลเยอร์ขั้นต่ำ" msgid "Overhang cooling activation threshold" msgstr "เกณฑ์การเปิดใช้งานการทำความเย็นเกิน" #, no-c-format, no-boost-format -msgid "" -"When the overhang exceeds this specified threshold, force the cooling fan to " -"run at the 'Overhang Fan Speed' set below. This threshold is expressed as a " -"percentage, indicating the portion of each line's width that is unsupported " -"by the layer beneath it. Setting this value to 0% forces the cooling fan to " -"run for all outer walls, regardless of the overhang degree." -msgstr "" -"เมื่อโอเวอร์แฮงค์เกินเกณฑ์ที่ระบุ ให้บังคับพัดลมระบายความร้อนให้ทำงานที่ " -"'ความเร็วพัดลมโอเวอร์แฮง' ที่ตั้งไว้ด้านล่าง เกณฑ์นี้แสดงเป็นเปอร์เซ็นต์ " -"ซึ่งระบุส่วนของความกว้างของแต่ละบรรทัดที่เลเยอร์ด้านล่างไม่รองรับ การตั้งค่านี้เป็น 0% " -"จะบังคับให้พัดลมระบายความร้อนทำงานบนผนังด้านนอกทั้งหมด โดยไม่คำนึงถึงระดับของส่วนยื่น" +msgid "When the overhang exceeds this specified threshold, force the cooling fan to run at the 'Overhang Fan Speed' set below. This threshold is expressed as a percentage, indicating the portion of each line's width that is unsupported by the layer beneath it. Setting this value to 0% forces the cooling fan to run for all outer walls, regardless of the overhang degree." +msgstr "เมื่อโอเวอร์แฮงค์เกินเกณฑ์ที่ระบุ ให้บังคับพัดลมระบายความร้อนให้ทำงานที่ 'ความเร็วพัดลมโอเวอร์แฮง' ที่ตั้งไว้ด้านล่าง เกณฑ์นี้แสดงเป็นเปอร์เซ็นต์ ซึ่งระบุส่วนของความกว้างของแต่ละบรรทัดที่เลเยอร์ด้านล่างไม่รองรับ การตั้งค่านี้เป็น 0% จะบังคับให้พัดลมระบายความร้อนทำงานบนผนังด้านนอกทั้งหมด โดยไม่คำนึงถึงระดับของส่วนยื่น" msgid "External bridge infill direction" msgstr "ทิศทางไส้ในสะพานภายนอก" @@ -12694,14 +11298,11 @@ msgstr "ทิศทางไส้ในสะพานภายนอก" #, no-c-format, no-boost-format msgid "" "External Bridging angle override.\n" -"If left to zero, the bridging angle will be calculated automatically for " -"each specific bridge.\n" +"If left to zero, the bridging angle will be calculated automatically for each specific bridge.\n" "Otherwise the provided angle will be used according to:\n" " - The absolute coordinates\n" -" - The absolute coordinates + Model rotation: If Align infill direction to " -"model is enabled\n" -" - The optimal automatic angle + this value: If 'Relative Bridge Angle' is " -"enabled\n" +" - The absolute coordinates + Model rotation: If Align infill direction to model is enabled\n" +" - The optimal automatic angle + this value: If 'Relative Bridge Angle' is enabled\n" "\n" "Use 180° for zero absolute angle." msgstr "" @@ -12719,14 +11320,11 @@ msgstr "ทิศทางไส้ในสะพานภายใน" msgid "" "Internal Bridging angle override.\n" -"If left to zero, the bridging angle will be calculated automatically for " -"each specific bridge.\n" +"If left to zero, the bridging angle will be calculated automatically for each specific bridge.\n" "Otherwise the provided angle will be used according to:\n" " - The absolute coordinates\n" -" - The absolute coordinates + Model rotation: If Align infill direction to " -"model is enabled\n" -" - The optimal automatic angle + this value: If 'Relative Bridge Angle' is " -"enabled\n" +" - The absolute coordinates + Model rotation: If Align infill direction to model is enabled\n" +" - The optimal automatic angle + this value: If 'Relative Bridge Angle' is enabled\n" "\n" "Use 180° for zero absolute angle." msgstr "" @@ -12742,30 +11340,22 @@ msgstr "" msgid "Relative bridge angle" msgstr "มุมสะพานแบบสัมพันธ์" -msgid "" -"When enabled, the bridge angle values are added to the automatically " -"calculated bridge direction instead of overriding it." -msgstr "" -"เมื่อเปิดใช้งาน ค่ามุมสะพานจะถูกบวกเข้ากับทิศทางสะพานที่คำนวณอัตโนมัติ แทนที่จะแทนที่ค่าเดิม" +msgid "When enabled, the bridge angle values are added to the automatically calculated bridge direction instead of overriding it." +msgstr "เมื่อเปิดใช้งาน ค่ามุมสะพานจะถูกบวกเข้ากับทิศทางสะพานที่คำนวณอัตโนมัติ แทนที่จะแทนที่ค่าเดิม" msgid "External bridge density" msgstr "ความหนาแน่นของสะพานภายนอก" msgid "" "Controls the density (spacing) of external bridge lines.\n" -"Theoretically, 100% means a solid bridge, but due to the tendency of bridge " -"extrusions to sag, 100% may not be sufficient.\n" +"Theoretically, 100% means a solid bridge, but due to the tendency of bridge extrusions to sag, 100% may not be sufficient.\n" "\n" "- Higher than 100% density (Recommended Max 125%):\n" -" - Pros: Produces smoother bridge surfaces, as overlapping lines provide " -"additional support during printing.\n" -" - Cons: Can cause overextrusion, which may reduce lower and upper surface " -"quality and increase the risk of warping.\n" +" - Pros: Produces smoother bridge surfaces, as overlapping lines provide additional support during printing.\n" +" - Cons: Can cause overextrusion, which may reduce lower and upper surface quality and increase the risk of warping.\n" "\n" "- Lower than 100% density (Min 10%):\n" -" - Pros: Can create a string-like first layer. Faster and with better " -"cooling because there is more space for air to circulate around the extruded " -"bridge.\n" +" - Pros: Can create a string-like first layer. Faster and with better cooling because there is more space for air to circulate around the extruded bridge.\n" " - Cons: May lead to sagging and poorer surface finish." msgstr "" "ควบคุมความหนาแน่น (ระยะห่าง) ของเส้นสะพานด้านนอก\n" @@ -12773,12 +11363,10 @@ msgstr "" "\n" "- ความหนาแน่นมากกว่า 100% (แนะนำสูงสุด 125%):\n" " - ข้อดี: ผิวสะพานเรียบขึ้น เพราะเส้นที่ทับซ้อนกันให้การรองรับเพิ่มเติมระหว่างพิมพ์\n" -" - ข้อเสีย: อาจเกิดการดันเส้นเกิน ซึ่งลดคุณภาพผิวด้านล่างและด้านบน " -"และเพิ่มความเสี่ยงการบิดเบี้ยว\n" +" - ข้อเสีย: อาจเกิดการดันเส้นเกิน ซึ่งลดคุณภาพผิวด้านล่างและด้านบน และเพิ่มความเสี่ยงการบิดเบี้ยว\n" "\n" "- ความหนาแน่นต่ำกว่า 100% (ต่ำสุด 10%):\n" -" - ข้อดี: ชั้นแรกอาจเป็นเส้นแบบเส้นใย " -"พิมพ์เร็วขึ้นและระบายความร้อนดีขึ้นเพราะมีพื้นที่ให้อากาศหมุนเวียนรอบเส้นสะพาน\n" +" - ข้อดี: ชั้นแรกอาจเป็นเส้นแบบเส้นใย พิมพ์เร็วขึ้นและระบายความร้อนดีขึ้นเพราะมีพื้นที่ให้อากาศหมุนเวียนรอบเส้นสะพาน\n" " - ข้อเสีย: อาจย้อยตัวและผิวไม่เรียบ" msgid "Internal bridge density" @@ -12786,66 +11374,49 @@ msgstr "ความหนาแน่นสะพานภายใน" msgid "" "Controls the density (spacing) of internal bridge lines.\n" -"Internal bridges act as intermediate support between sparse infill and top " -"solid infill and can strongly affect top surface quality.\n" +"Internal bridges act as intermediate support between sparse infill and top solid infill and can strongly affect top surface quality.\n" "\n" "- Higher than 100% density (Recommended Max 125%):\n" -" - Pros: Improves internal bridge strength and support under top layers, " -"reducing sagging and improving top-surface finish.\n" -" - Cons: Increases material use and print time; excessive density may cause " -"overextrusion and internal stresses.\n" +" - Pros: Improves internal bridge strength and support under top layers, reducing sagging and improving top-surface finish.\n" +" - Cons: Increases material use and print time; excessive density may cause overextrusion and internal stresses.\n" "\n" "- Lower than 100% density (Min 10%):\n" -" - Pros: Can reduce pillowing and improve cooling (more airflow through the " -"bridge), and may speed up printing.\n" -" - Cons: May reduce internal support, increasing the risk of sagging and " -"top surface defects.\n" +" - Pros: Can reduce pillowing and improve cooling (more airflow through the bridge), and may speed up printing.\n" +" - Cons: May reduce internal support, increasing the risk of sagging and top surface defects.\n" "\n" -"This option works particularly well when combined with the second internal " -"bridge over infill option to improve bridging further before solid infill is " -"extruded." +"This option works particularly well when combined with the second internal bridge over infill option to improve bridging further before solid infill is extruded." msgstr "" "ควบคุมความหนาแน่น (ระยะห่าง) ของเส้นสะพานด้านใน\n" -"สะพานด้านในทำหน้าที่เป็นส่วนรองรับชั่วคราวระหว่างไส้ในแบบโปร่งกับไส้ในแบบทึบด้านบน " -"และส่งผลต่อคุณภาพผิวด้านบนอย่างมาก\n" +"สะพานด้านในทำหน้าที่เป็นส่วนรองรับชั่วคราวระหว่างไส้ในแบบโปร่งกับไส้ในแบบทึบด้านบน และส่งผลต่อคุณภาพผิวด้านบนอย่างมาก\n" "\n" "- ความหนาแน่นมากกว่า 100% (แนะนำสูงสุด 125%):\n" -" - ข้อดี: เพิ่มความแข็งแรงของสะพานด้านในและการรองรับใต้ชั้นบน " -"ลดการย้อยตัวและปรับปรุงผิวด้านบน\n" -" - ข้อเสีย: ใช้วัสดุและเวลาพิมพ์มากขึ้น " -"ความหนาแน่นเกินไปอาจทำให้ดันเส้นเกินและเกิดความเครียดภายใน\n" +" - ข้อดี: เพิ่มความแข็งแรงของสะพานด้านในและการรองรับใต้ชั้นบน ลดการย้อยตัวและปรับปรุงผิวด้านบน\n" +" - ข้อเสีย: ใช้วัสดุและเวลาพิมพ์มากขึ้น ความหนาแน่นเกินไปอาจทำให้ดันเส้นเกินและเกิดความเครียดภายใน\n" "\n" "- ความหนาแน่นต่ำกว่า 100% (ต่ำสุด 10%):\n" -" - ข้อดี: ลดการโป่งของผิวด้านบนและระบายความร้อนดีขึ้น (อากาศไหลผ่านสะพานได้มากขึ้น) " -"และอาจพิมพ์เร็วขึ้น\n" +" - ข้อดี: ลดการโป่งของผิวด้านบนและระบายความร้อนดีขึ้น (อากาศไหลผ่านสะพานได้มากขึ้น) และอาจพิมพ์เร็วขึ้น\n" " - ข้อเสีย: ลดการรองรับภายใน เพิ่มความเสี่ยงการย้อยตัวและข้อบกพร่องบนผิวด้านบน\n" "\n" -"ตัวเลือกนี้ทำงานได้ดีเป็นพิเศษเมื่อใช้ร่วมกับตัวเลือกสะพานด้านในชั้นที่สองเหนือไส้ใน " -"เพื่อปรับปรุงการพิมพ์สะพานก่อนอัดไส้ในแบบทึบ" +"ตัวเลือกนี้ทำงานได้ดีเป็นพิเศษเมื่อใช้ร่วมกับตัวเลือกสะพานด้านในชั้นที่สองเหนือไส้ใน เพื่อปรับปรุงการพิมพ์สะพานก่อนอัดไส้ในแบบทึบ" msgid "Bridge flow ratio" msgstr "อัตราส่วนการไหลสะพาน" msgid "" "This value governs the thickness of the external (visible) bridge layer.\n" -"Values above 1.0: Increase the amount of material while maintaining line " -"spacing. This can improve line contact and strength.\n" -"Values below 1.0: Reduce the amount of material while adjusting line spacing " -"to maintain contact. This can improve sagging.\n" +"Values above 1.0: Increase the amount of material while maintaining line spacing. This can improve line contact and strength.\n" +"Values below 1.0: Reduce the amount of material while adjusting line spacing to maintain contact. This can improve sagging.\n" "\n" -"The actual bridge flow used is calculated by multiplying this value with the " -"filament flow ratio, and if set, the object's flow ratio." +"The actual bridge flow used is calculated by multiplying this value with the filament flow ratio, and if set, the object's flow ratio." msgstr "" "ค่านี้กำหนดความหนาของชั้นสะพานด้านนอก (ที่มองเห็นได้)\n" "ค่ามากกว่า 1.0: เพิ่มปริมาณวัสดุโดยคงระยะห่างเส้น ช่วยเพิ่มการสัมผัสและความแข็งแรงของเส้น\n" "ค่าน้อยกว่า 1.0: ลดปริมาณวัสดุโดยปรับระยะห่างเส้นเพื่อคงการสัมผัส ช่วยลดการย้อยตัว\n" "\n" -"อัตราการไหลของสะพานจริงคำนวณจากค่านี้คูณอัตราการไหลของเส้นพลาสติก และหากตั้งค่าไว้ " -"อัตราการไหลของวัตถุ" +"อัตราการไหลของสะพานจริงคำนวณจากค่านี้คูณอัตราการไหลของเส้นพลาสติก และหากตั้งค่าไว้ อัตราการไหลของวัตถุ" msgid "" -"Line width of the Bridge. If expressed as a %, it will be computed over the " -"nozzle diameter.\n" +"Line width of the Bridge. If expressed as a %, it will be computed over the nozzle diameter.\n" "Recommended to use with a higher Bridge density or Bridge flow ratio.\n" "\n" "The maximum value is 100% or the nozzle diameter.\n" @@ -12861,39 +11432,29 @@ msgid "Internal bridge flow ratio" msgstr "อัตราส่วนการไหลสะพานภายใน" msgid "" -"This value governs the thickness of the internal bridge layer. This is the " -"first layer over sparse infill so increasing it may increase strength and " -"upper layer quality.\n" -"Values above 1.0: Increase the amount of material while maintaining line " -"spacing. This can improve line contact and strength.\n" -"Values below 1.0: Reduce the amount of material while adjusting line spacing " -"to maintain contact. This can improve sagging.\n" +"This value governs the thickness of the internal bridge layer. This is the first layer over sparse infill so increasing it may increase strength and upper layer quality.\n" +"Values above 1.0: Increase the amount of material while maintaining line spacing. This can improve line contact and strength.\n" +"Values below 1.0: Reduce the amount of material while adjusting line spacing to maintain contact. This can improve sagging.\n" "\n" -"The actual bridge flow used is calculated by multiplying this value with the " -"filament flow ratio, and if set, the object's flow ratio." +"The actual bridge flow used is calculated by multiplying this value with the filament flow ratio, and if set, the object's flow ratio." msgstr "" -"ค่านี้กำหนดความหนาของชั้นสะพานด้านใน ซึ่งเป็นชั้นแรกเหนือไส้ในแบบโปร่ง " -"การเพิ่มค่าอาจเพิ่มความแข็งแรงและคุณภาพชั้นบน\n" +"ค่านี้กำหนดความหนาของชั้นสะพานด้านใน ซึ่งเป็นชั้นแรกเหนือไส้ในแบบโปร่ง การเพิ่มค่าอาจเพิ่มความแข็งแรงและคุณภาพชั้นบน\n" "ค่ามากกว่า 1.0: เพิ่มปริมาณวัสดุโดยคงระยะห่างเส้น ช่วยเพิ่มการสัมผัสและความแข็งแรงของเส้น\n" "ค่าน้อยกว่า 1.0: ลดปริมาณวัสดุโดยปรับระยะห่างเส้นเพื่อคงการสัมผัส ช่วยลดการย้อยตัว\n" "\n" -"อัตราการไหลของสะพานจริงคำนวณจากค่านี้คูณอัตราการไหลของเส้นพลาสติก และหากตั้งค่าไว้ " -"อัตราการไหลของวัตถุ" +"อัตราการไหลของสะพานจริงคำนวณจากค่านี้คูณอัตราการไหลของเส้นพลาสติก และหากตั้งค่าไว้ อัตราการไหลของวัตถุ" msgid "Top surface flow ratio" msgstr "อัตราส่วนการไหลผิวด้านบน" msgid "" -"This factor affects the amount of material for top solid infill. You can " -"decrease it slightly to have smooth surface finish.\n" +"This factor affects the amount of material for top solid infill. You can decrease it slightly to have smooth surface finish.\n" "\n" -"The actual top surface flow used is calculated by multiplying this value " -"with the filament flow ratio, and if set, the object's flow ratio." +"The actual top surface flow used is calculated by multiplying this value with the filament flow ratio, and if set, the object's flow ratio." msgstr "" "ปัจจัยนี้ส่งผลต่อปริมาณวัสดุสำหรับไส้ในของแข็งด้านบน คุณสามารถลดลงเล็กน้อยเพื่อให้พื้นผิวเรียบ\n" "\n" -"การไหลของพื้นผิวด้านบนจริงที่ใช้คำนวณโดยการคูณค่านี้ด้วยอัตราส่วนการไหลของเส้นพลาสติก " -"และหากตั้งค่าไว้ อัตราการไหลของวัตถุ" +"การไหลของพื้นผิวด้านบนจริงที่ใช้คำนวณโดยการคูณค่านี้ด้วยอัตราส่วนการไหลของเส้นพลาสติก และหากตั้งค่าไว้ อัตราการไหลของวัตถุ" msgid "Bottom surface flow ratio" msgstr "อัตราส่วนการไหลผิวด้านล่าง" @@ -12901,13 +11462,11 @@ msgstr "อัตราส่วนการไหลผิวด้านล่ msgid "" "This factor affects the amount of material for bottom solid infill.\n" "\n" -"The actual bottom solid infill flow used is calculated by multiplying this " -"value with the filament flow ratio, and if set, the object's flow ratio." +"The actual bottom solid infill flow used is calculated by multiplying this value with the filament flow ratio, and if set, the object's flow ratio." msgstr "" "ปัจจัยนี้ส่งผลต่อปริมาณวัสดุสำหรับไส้ในของแข็งด้านล่าง\n" "\n" -"การไหล ไส้ใน ของแข็งด้านล่างจริงที่ใช้คำนวณโดยการคูณค่านี้ด้วยอัตราส่วนการไหลของเส้นพลาสติก " -"และหากตั้งค่าไว้ อัตราการไหลของวัตถุ" +"การไหล ไส้ใน ของแข็งด้านล่างจริงที่ใช้คำนวณโดยการคูณค่านี้ด้วยอัตราส่วนการไหลของเส้นพลาสติก และหากตั้งค่าไว้ อัตราการไหลของวัตถุ" msgid "Set other flow ratios" msgstr "ตั้งค่าอัตราส่วนการไหลอื่นๆ" @@ -12919,16 +11478,13 @@ msgid "First layer flow ratio" msgstr "อัตราการไหลของชั้นแรก" msgid "" -"This factor affects the amount of material on the first layer for the " -"extrusion path roles listed in this section.\n" +"This factor affects the amount of material on the first layer for the extrusion path roles listed in this section.\n" "\n" -"For the first layer, the actual flow ratio for each path role (does not " -"affect brims and skirts) will be multiplied by this value." +"For the first layer, the actual flow ratio for each path role (does not affect brims and skirts) will be multiplied by this value." msgstr "" "ปัจจัยนี้ส่งผลต่อปริมาณวัสดุในชั้นแรกสำหรับบทบาทเส้นทางการอัดขึ้นรูปที่แสดงอยู่ในส่วนนี้\n" "\n" -"สำหรับชั้นแรก อัตราการไหลตามจริงสำหรับแต่ละบทบาทของเส้นทาง " -"(ไม่ส่งผลต่อขอบยึดชิ้นงานและเส้นล้อมชิ้นงาน) จะถูกคูณด้วยค่านี้" +"สำหรับชั้นแรก อัตราการไหลตามจริงสำหรับแต่ละบทบาทของเส้นทาง (ไม่ส่งผลต่อขอบยึดชิ้นงานและเส้นล้อมชิ้นงาน) จะถูกคูณด้วยค่านี้" msgid "Outer wall flow ratio" msgstr "อัตราส่วนการไหลของผนังด้านนอก" @@ -12936,13 +11492,11 @@ msgstr "อัตราส่วนการไหลของผนังด้ msgid "" "This factor affects the amount of material for outer walls.\n" "\n" -"The actual outer wall flow used is calculated by multiplying this value by " -"the filament flow ratio, and if set, the object's flow ratio." +"The actual outer wall flow used is calculated by multiplying this value by the filament flow ratio, and if set, the object's flow ratio." msgstr "" "ปัจจัยนี้ส่งผลต่อปริมาณวัสดุสำหรับผนังด้านนอก\n" "\n" -"การไหลของผนังด้านนอกจริงที่ใช้คำนวณโดยการคูณค่านี้ด้วยอัตราส่วนการไหลของเส้นพลาสติก " -"และหากตั้งค่าไว้ อัตราการไหลของวัตถุ" +"การไหลของผนังด้านนอกจริงที่ใช้คำนวณโดยการคูณค่านี้ด้วยอัตราส่วนการไหลของเส้นพลาสติก และหากตั้งค่าไว้ อัตราการไหลของวัตถุ" msgid "Inner wall flow ratio" msgstr "อัตราส่วนการไหลของผนังด้านใน" @@ -12950,13 +11504,11 @@ msgstr "อัตราส่วนการไหลของผนังด้ msgid "" "This factor affects the amount of material for inner walls.\n" "\n" -"The actual inner wall flow used is calculated by multiplying this value by " -"the filament flow ratio, and if set, the object's flow ratio." +"The actual inner wall flow used is calculated by multiplying this value by the filament flow ratio, and if set, the object's flow ratio." msgstr "" "ปัจจัยนี้ส่งผลต่อปริมาณวัสดุสำหรับผนังด้านใน\n" "\n" -"การไหลของผนังด้านในจริงที่ใช้คำนวณโดยการคูณค่านี้ด้วยอัตราส่วนการไหลของเส้นพลาสติก " -"และหากตั้งค่าไว้ อัตราการไหลของวัตถุ" +"การไหลของผนังด้านในจริงที่ใช้คำนวณโดยการคูณค่านี้ด้วยอัตราส่วนการไหลของเส้นพลาสติก และหากตั้งค่าไว้ อัตราการไหลของวัตถุ" msgid "Overhang flow ratio" msgstr "อัตราส่วนการไหลของส่วนยื่น" @@ -12964,13 +11516,11 @@ msgstr "อัตราส่วนการไหลของส่วนยื msgid "" "This factor affects the amount of material for overhangs.\n" "\n" -"The actual overhang flow used is calculated by multiplying this value by the " -"filament flow ratio, and if set, the object's flow ratio." +"The actual overhang flow used is calculated by multiplying this value by the filament flow ratio, and if set, the object's flow ratio." msgstr "" "ปัจจัยนี้ส่งผลต่อปริมาณวัสดุสำหรับยื่น\n" "\n" -"การไหลยื่นออกมาจริงที่ใช้คำนวณโดยการคูณค่านี้ด้วยอัตราส่วนการไหลของเส้นพลาสติก และหากตั้งค่าไว้ " -"อัตราการไหลของวัตถุ" +"การไหลยื่นออกมาจริงที่ใช้คำนวณโดยการคูณค่านี้ด้วยอัตราส่วนการไหลของเส้นพลาสติก และหากตั้งค่าไว้ อัตราการไหลของวัตถุ" msgid "Sparse infill flow ratio" msgstr "อัตราส่วนการไหลไส้ในแบบโปร่ง" @@ -12978,13 +11528,11 @@ msgstr "อัตราส่วนการไหลไส้ในแบบโ msgid "" "This factor affects the amount of material for sparse infill.\n" "\n" -"The actual sparse infill flow used is calculated by multiplying this value " -"by the filament flow ratio, and if set, the object's flow ratio." +"The actual sparse infill flow used is calculated by multiplying this value by the filament flow ratio, and if set, the object's flow ratio." msgstr "" "ปัจจัยนี้ส่งผลต่อปริมาณวัสดุสำหรับไส้ในแบบโปร่ง\n" "\n" -"การไหล ไส้ใน แบบเบาบางจริงที่ใช้คำนวณโดยการคูณค่านี้ด้วยอัตราส่วนการไหลของเส้นพลาสติก " -"และหากตั้งค่าไว้ อัตราการไหลของวัตถุ" +"การไหล ไส้ใน แบบเบาบางจริงที่ใช้คำนวณโดยการคูณค่านี้ด้วยอัตราส่วนการไหลของเส้นพลาสติก และหากตั้งค่าไว้ อัตราการไหลของวัตถุ" msgid "Internal solid infill flow ratio" msgstr "อัตราส่วนการไหลไส้ในแบบทึบ" @@ -12992,13 +11540,11 @@ msgstr "อัตราส่วนการไหลไส้ในแบบท msgid "" "This factor affects the amount of material for internal solid infill.\n" "\n" -"The actual internal solid infill flow used is calculated by multiplying this " -"value by the filament flow ratio, and if set, the object's flow ratio." +"The actual internal solid infill flow used is calculated by multiplying this value by the filament flow ratio, and if set, the object's flow ratio." msgstr "" "ปัจจัยนี้ส่งผลต่อปริมาณวัสดุสำหรับไส้ในของแข็งภายใน\n" "\n" -"การไหล ไส้ใน ของแข็งภายในจริงที่ใช้คำนวณโดยการคูณค่านี้ด้วยอัตราส่วนการไหลของเส้นพลาสติก " -"และหากตั้งค่าไว้ อัตราการไหลของวัตถุ" +"การไหล ไส้ใน ของแข็งภายในจริงที่ใช้คำนวณโดยการคูณค่านี้ด้วยอัตราส่วนการไหลของเส้นพลาสติก และหากตั้งค่าไว้ อัตราการไหลของวัตถุ" msgid "Gap fill flow ratio" msgstr "อัตราส่วนการไหลเติมช่องว่าง" @@ -13006,13 +11552,11 @@ msgstr "อัตราส่วนการไหลเติมช่องว msgid "" "This factor affects the amount of material for filling the gaps.\n" "\n" -"The actual gap filling flow used is calculated by multiplying this value by " -"the filament flow ratio, and if set, the object's flow ratio." +"The actual gap filling flow used is calculated by multiplying this value by the filament flow ratio, and if set, the object's flow ratio." msgstr "" "ปัจจัยนี้ส่งผลต่อปริมาณวัสดุในการเติมช่องว่าง\n" "\n" -"อัตราการเติมช่องว่างจริงที่ใช้คำนวณโดยการคูณค่านี้ด้วยอัตราส่วนการไหลของเส้นพลาสติก " -"และหากตั้งค่าไว้ อัตราการไหลของวัตถุ" +"อัตราการเติมช่องว่างจริงที่ใช้คำนวณโดยการคูณค่านี้ด้วยอัตราส่วนการไหลของเส้นพลาสติก และหากตั้งค่าไว้ อัตราการไหลของวัตถุ" msgid "Support flow ratio" msgstr "อัตราส่วนการไหลส่วนรองรับ" @@ -13020,13 +11564,11 @@ msgstr "อัตราส่วนการไหลส่วนรองรั msgid "" "This factor affects the amount of material for support.\n" "\n" -"The actual support flow used is calculated by multiplying this value by the " -"filament flow ratio, and if set, the object's flow ratio." +"The actual support flow used is calculated by multiplying this value by the filament flow ratio, and if set, the object's flow ratio." msgstr "" "ปัจจัยนี้ส่งผลต่อปริมาณวัสดุที่รองรับ\n" "\n" -"กระแสรองรับจริงที่ใช้คำนวณโดยการคูณค่านี้ด้วยอัตราส่วนการไหลของเส้นพลาสติก และหากตั้งค่าไว้ " -"อัตราการไหลของวัตถุ" +"กระแสรองรับจริงที่ใช้คำนวณโดยการคูณค่านี้ด้วยอัตราส่วนการไหลของเส้นพลาสติก และหากตั้งค่าไว้ อัตราการไหลของวัตถุ" msgid "Support interface flow ratio" msgstr "อัตราส่วนการไหลผิวสัมผัสส่วนรองรับ" @@ -13034,32 +11576,22 @@ msgstr "อัตราส่วนการไหลผิวสัมผัส msgid "" "This factor affects the amount of material for the support interface.\n" "\n" -"The actual support interface flow used is calculated by multiplying this " -"value by the filament flow ratio, and if set, the object's flow ratio." +"The actual support interface flow used is calculated by multiplying this value by the filament flow ratio, and if set, the object's flow ratio." msgstr "" "ปัจจัยนี้ส่งผลต่อปริมาณวัสดุสำหรับอินเทอร์เฟซส่วนรองรับ\n" "\n" -"โฟลว์อินเทอร์เฟซส่วนรองรับจริงที่ใช้คำนวณโดยการคูณค่านี้ด้วยอัตราส่วนการไหลของเส้นพลาสติก " -"และหากตั้งค่าไว้ อัตราการไหลของวัตถุ" +"โฟลว์อินเทอร์เฟซส่วนรองรับจริงที่ใช้คำนวณโดยการคูณค่านี้ด้วยอัตราส่วนการไหลของเส้นพลาสติก และหากตั้งค่าไว้ อัตราการไหลของวัตถุ" msgid "Precise wall" msgstr "ผนังที่แม่นยำ" -msgid "" -"Improve shell precision by adjusting outer wall spacing. This also improves " -"layer consistency. NOTE: This option will be ignored for outer-inner or " -"inner-outer-inner wall sequences." -msgstr "" -"ปรับปรุงความแม่นยำของเปลือกโดยการปรับระยะห่างผนังด้านนอก " -"นอกจากนี้ยังช่วยปรับปรุงความสม่ำเสมอของชั้นอีกด้วย หมายเหตุ: " -"ตัวเลือกนี้จะถูกละเว้นสำหรับลำดับผนังด้านนอก-ด้านใน หรือด้านใน-ด้านนอก-ด้านใน" +msgid "Improve shell precision by adjusting outer wall spacing. This also improves layer consistency. NOTE: This option will be ignored for outer-inner or inner-outer-inner wall sequences." +msgstr "ปรับปรุงความแม่นยำของเปลือกโดยการปรับระยะห่างผนังด้านนอก นอกจากนี้ยังช่วยปรับปรุงความสม่ำเสมอของชั้นอีกด้วย หมายเหตุ: ตัวเลือกนี้จะถูกละเว้นสำหรับลำดับผนังด้านนอก-ด้านใน หรือด้านใน-ด้านนอก-ด้านใน" msgid "Only one wall on top surfaces" msgstr "มีเพียงผนังเดียวบนพื้นผิวด้านบน" -msgid "" -"Use only one wall on flat top surfaces, to give more space to the top infill " -"pattern." +msgid "Use only one wall on flat top surfaces, to give more space to the top infill pattern." msgstr "ใช้ผนังเพียงด้านเดียวบนพื้นผิวเรียบเพื่อเพิ่มพื้นที่ให้กับรูปแบบไส้ในด้านบน" msgid "One wall threshold" @@ -13067,36 +11599,22 @@ msgstr "เกณฑ์ผนังด้านหนึ่ง" #, no-c-format, no-boost-format msgid "" -"If a top surface has to be printed and it's partially covered by another " -"layer, it won't be considered at a top layer where its width is below this " -"value. This can be useful to not let the 'one perimeter on top' trigger on " -"surface that should be covered only by perimeters. This value can be a mm or " -"a % of the perimeter extrusion width.\n" -"Warning: If enabled, artifacts can be created if you have some thin features " -"on the next layer, like letters. Set this setting to 0 to remove these " -"artifacts." +"If a top surface has to be printed and it's partially covered by another layer, it won't be considered at a top layer where its width is below this value. This can be useful to not let the 'one perimeter on top' trigger on surface that should be covered only by perimeters. This value can be a mm or a % of the perimeter extrusion width.\n" +"Warning: If enabled, artifacts can be created if you have some thin features on the next layer, like letters. Set this setting to 0 to remove these artifacts." msgstr "" -"หากต้องพิมพ์พื้นผิวด้านบนและปิดบางส่วนด้วยชั้นอื่น " -"จะไม่ได้รับการพิจารณาที่ชั้นบนสุดซึ่งมีความกว้างต่ำกว่าค่านี้ วิธีนี้จะเป็นประโยชน์ในการไม่ปล่อยให้ " -"'เส้นรอบวงด้านบนหนึ่งเส้น' ทริกเกอร์บนพื้นผิวที่ควรครอบคลุมเฉพาะเส้นรอบวงเท่านั้น " -"ค่านี้อาจเป็นหน่วย มม. หรือ % ของความกว้างของการอัดขึ้นรูปเส้นรอบวง\n" -"คำเตือน: หากเปิดใช้งาน สามารถสร้างสิ่งประดิษฐ์ได้หากคุณมีคุณสมบัติบางๆ ในเลเยอร์ถัดไป เช่น " -"ตัวอักษร ตั้งค่านี้เป็น 0 เพื่อลบสิ่งประดิษฐ์เหล่านี้" +"หากต้องพิมพ์พื้นผิวด้านบนและปิดบางส่วนด้วยชั้นอื่น จะไม่ได้รับการพิจารณาที่ชั้นบนสุดซึ่งมีความกว้างต่ำกว่าค่านี้ วิธีนี้จะเป็นประโยชน์ในการไม่ปล่อยให้ 'เส้นรอบวงด้านบนหนึ่งเส้น' ทริกเกอร์บนพื้นผิวที่ควรครอบคลุมเฉพาะเส้นรอบวงเท่านั้น ค่านี้อาจเป็นหน่วย มม. หรือ % ของความกว้างของการอัดขึ้นรูปเส้นรอบวง\n" +"คำเตือน: หากเปิดใช้งาน สามารถสร้างสิ่งประดิษฐ์ได้หากคุณมีคุณสมบัติบางๆ ในเลเยอร์ถัดไป เช่น ตัวอักษร ตั้งค่านี้เป็น 0 เพื่อลบสิ่งประดิษฐ์เหล่านี้" msgid "Only one wall on first layer" msgstr "มีเพียงผนังเดียวในชั้นแรก" -msgid "" -"Use only one wall on first layer, to give more space to the bottom infill " -"pattern." +msgid "Use only one wall on first layer, to give more space to the bottom infill pattern." msgstr "ใช้ผนังเพียงชั้นเดียวในชั้นแรก เพื่อให้มีพื้นที่ด้านล่างมากขึ้น" msgid "Extra perimeters on overhangs" msgstr "เส้นรอบวงเพิ่มเติมบนส่วนยื่น" -msgid "" -"Create additional perimeter paths over steep overhangs and areas where " -"bridges cannot be anchored." +msgid "Create additional perimeter paths over steep overhangs and areas where bridges cannot be anchored." msgstr "สร้างเส้นทางเส้นรอบวงเพิ่มเติมเหนือส่วนยื่นชันและพื้นที่ที่ไม่สามารถยึดสะพานได้" msgid "Reverse on even" @@ -13106,15 +11624,11 @@ msgid "Overhang reversal" msgstr "การกลับด้านส่วนยื่นออกมา" msgid "" -"Extrude perimeters that have a part over an overhang in the reverse " -"direction on even layers. This alternating pattern can drastically improve " -"steep overhangs.\n" +"Extrude perimeters that have a part over an overhang in the reverse direction on even layers. This alternating pattern can drastically improve steep overhangs.\n" "\n" -"This setting can also help reduce part warping due to the reduction of " -"stresses in the part walls." +"This setting can also help reduce part warping due to the reduction of stresses in the part walls." msgstr "" -"รีดเส้นรอบวงที่มีส่วนยื่นออกมาในทิศทางย้อนกลับบนชั้นคู่ " -"รูปแบบการสลับนี้สามารถปรับปรุงระยะยื่นที่สูงชันได้อย่างมาก\n" +"รีดเส้นรอบวงที่มีส่วนยื่นออกมาในทิศทางย้อนกลับบนชั้นคู่ รูปแบบการสลับนี้สามารถปรับปรุงระยะยื่นที่สูงชันได้อย่างมาก\n" "\n" "การตั้งค่านี้ยังช่วยลดการบิดงอของชิ้นส่วนเนื่องจากการลดความเค้นในผนังชิ้นส่วนอีกด้วย" @@ -13124,40 +11638,26 @@ msgstr "กลับด้านเฉพาะขอบเขตภายใน msgid "" "Apply the reverse perimeters logic only on internal perimeters.\n" "\n" -"This setting greatly reduces part stresses as they are now distributed in " -"alternating directions. This should reduce part warping while also " -"maintaining external wall quality. This feature can be very useful for warp " -"prone material, like ABS/ASA, and also for elastic filaments, like TPU and " -"Silk PLA. It can also help reduce warping on floating regions over " -"supports.\n" +"This setting greatly reduces part stresses as they are now distributed in alternating directions. This should reduce part warping while also maintaining external wall quality. This feature can be very useful for warp prone material, like ABS/ASA, and also for elastic filaments, like TPU and Silk PLA. It can also help reduce warping on floating regions over supports.\n" "\n" -"For this setting to be the most effective, it is recommended to set the " -"Reverse Threshold to 0 so that all internal walls print in alternating " -"directions on even layers irrespective of their overhang degree." +"For this setting to be the most effective, it is recommended to set the Reverse Threshold to 0 so that all internal walls print in alternating directions on even layers irrespective of their overhang degree." msgstr "" "ใช้ตรรกะเส้นรอบวงย้อนกลับเฉพาะกับเส้นรอบวงภายในเท่านั้น\n" "\n" -"การตั้งค่านี้ช่วยลดความเค้นของชิ้นส่วนได้อย่างมาก เนื่องจากมีการกระจายไปในทิศทางสลับกัน " -"สิ่งนี้จะลดการบิดเบี้ยวของชิ้นส่วนในขณะที่ยังคงรักษาคุณภาพของผนังภายนอกไว้ด้วย " -"คุณลักษณะนี้มีประโยชน์มากสำหรับวัสดุที่บิดงอง่าย เช่น ABS/ASA และสำหรับเส้นพลาสติกยืดหยุ่น เช่น " -"TPU และ Silk PLA " -"นอกจากนี้ยังสามารถช่วยลดการบิดเบี้ยวในบริเวณที่ลอยอยู่เหนือส่วนรองรับได้อีกด้วย\n" +"การตั้งค่านี้ช่วยลดความเค้นของชิ้นส่วนได้อย่างมาก เนื่องจากมีการกระจายไปในทิศทางสลับกัน สิ่งนี้จะลดการบิดเบี้ยวของชิ้นส่วนในขณะที่ยังคงรักษาคุณภาพของผนังภายนอกไว้ด้วย คุณลักษณะนี้มีประโยชน์มากสำหรับวัสดุที่บิดงอง่าย เช่น ABS/ASA และสำหรับเส้นพลาสติกยืดหยุ่น เช่น TPU และ Silk PLA นอกจากนี้ยังสามารถช่วยลดการบิดเบี้ยวในบริเวณที่ลอยอยู่เหนือส่วนรองรับได้อีกด้วย\n" "\n" -"เพื่อให้การตั้งค่านี้มีประสิทธิภาพสูงสุด ขอแนะนำให้ตั้งค่า Reverse Threshold เป็น 0 " -"เพื่อให้ผนังภายในทั้งหมดพิมพ์ในทิศทางสลับกันบนเลเยอร์คู่ โดยไม่คำนึงถึงระดับส่วนยื่น" +"เพื่อให้การตั้งค่านี้มีประสิทธิภาพสูงสุด ขอแนะนำให้ตั้งค่า Reverse Threshold เป็น 0 เพื่อให้ผนังภายในทั้งหมดพิมพ์ในทิศทางสลับกันบนเลเยอร์คู่ โดยไม่คำนึงถึงระดับส่วนยื่น" msgid "Bridge counterbore holes" msgstr "สะพานหลุมเจาะ" msgid "" -"This option creates bridges for counterbore holes, allowing them to be " -"printed without support. Available modes include:\n" +"This option creates bridges for counterbore holes, allowing them to be printed without support. Available modes include:\n" "1. None: No bridge is created\n" "2. Partially Bridged: Only a part of the unsupported area will be bridged\n" "3. Sacrificial Layer: A full sacrificial bridge layer is created" msgstr "" -"ตัวเลือกนี้จะสร้างสะพานเชื่อมสำหรับรูเจาะเคาน์เตอร์ ทำให้สามารถพิมพ์ได้โดยไม่ต้องมีส่วนรองรับ " -"โหมดที่ใช้ได้ ได้แก่ :\n" +"ตัวเลือกนี้จะสร้างสะพานเชื่อมสำหรับรูเจาะเคาน์เตอร์ ทำให้สามารถพิมพ์ได้โดยไม่ต้องมีส่วนรองรับ โหมดที่ใช้ได้ ได้แก่ :\n" "1. ไม่มี: ไม่มีการสร้างสะพาน\n" "2. เชื่อมโยงบางส่วน: เฉพาะส่วนหนึ่งของพื้นที่ที่ไม่ได้รับส่วนรองรับเท่านั้นที่จะถูกเชื่อมโยง\n" "3. Sacrificial Layer: ชั้นสะพานบูชายัญเต็มรูปแบบจะถูกสร้างขึ้น" @@ -13176,83 +11676,53 @@ msgstr "เกณฑ์การกลับรายการส่วนเก #, no-c-format, no-boost-format msgid "" -"Number of mm the overhang need to be for the reversal to be considered " -"useful. Can be a % of the perimeter width.\n" +"Number of mm the overhang need to be for the reversal to be considered useful. Can be a % of the perimeter width.\n" "Value 0 enables reversal on every even layers regardless.\n" -"When Detect overhang wall is not enabled, this option is ignored and " -"reversal happens on every even layers regardless." +"When Detect overhang wall is not enabled, this option is ignored and reversal happens on every even layers regardless." msgstr "" -"ระยะยื่นต้องมีจำนวน มม. เพื่อให้การกลับรายการถือว่ามีประโยชน์ อาจเป็น % " -"ของความกว้างเส้นรอบวงได้\n" +"ระยะยื่นต้องมีจำนวน มม. เพื่อให้การกลับรายการถือว่ามีประโยชน์ อาจเป็น % ของความกว้างเส้นรอบวงได้\n" "ค่า 0 เปิดใช้งานการกลับรายการในทุกเลเยอร์คู่โดยไม่คำนึงถึง\n" -"เมื่อไม่ได้เปิดใช้งาน Detect ส่วนยื่น wall " -"ตัวเลือกนี้จะถูกละเว้นและการกลับรายการจะเกิดขึ้นในทุกเลเยอร์คู่โดยไม่คำนึงถึง" +"เมื่อไม่ได้เปิดใช้งาน Detect ส่วนยื่น wall ตัวเลือกนี้จะถูกละเว้นและการกลับรายการจะเกิดขึ้นในทุกเลเยอร์คู่โดยไม่คำนึงถึง" msgid "Slow down for overhangs" msgstr "ลดความเร็วสำหรับส่วนยื่น" -msgid "" -"Enable this option to slow down when printing overhangs. The speeds for " -"different overhang percentages are set below." -msgstr "" -"เปิดตัวเลือกนี้เพื่อลดความเร็วเมื่อพิมพ์ส่วนยื่น โดยกำหนดความเร็วตามเปอร์เซ็นต์ส่วนยื่นได้ด้านล่าง" +msgid "Enable this option to slow down when printing overhangs. The speeds for different overhang percentages are set below." +msgstr "เปิดตัวเลือกนี้เพื่อลดความเร็วเมื่อพิมพ์ส่วนยื่น โดยกำหนดความเร็วตามเปอร์เซ็นต์ส่วนยื่นได้ด้านล่าง" msgid "Slow down for curled perimeters" msgstr "ชะลอความเร็วลงสำหรับขอบเขตที่โค้งงอ" #, no-c-format, no-boost-format msgid "" -"Enable this option to slow down printing in areas where perimeters may have " -"curled upwards.\n" -"For example, additional slowdown will be applied when printing overhangs on " -"sharp corners like the front of the Benchy hull, reducing curling which " -"compounds over multiple layers.\n" +"Enable this option to slow down printing in areas where perimeters may have curled upwards.\n" +"For example, additional slowdown will be applied when printing overhangs on sharp corners like the front of the Benchy hull, reducing curling which compounds over multiple layers.\n" "\n" -"It is generally recommended to have this option switched on unless your " -"printer cooling is powerful enough or the print speed is slow enough that " -"perimeter curling does not happen. \n" -"If printing with a high external perimeter speed, this parameter may " -"introduce wall artifacts when slowing down, due to the potentially large " -"variance in print speeds causing the extruder to be unable to keep up with " -"the requested flow change.\n" -"Root cause of these artifacts is most likely PA tuning being slightly off, " -"especially when combined with a high PA smooth time.\n" +"It is generally recommended to have this option switched on unless your printer cooling is powerful enough or the print speed is slow enough that perimeter curling does not happen. \n" +"If printing with a high external perimeter speed, this parameter may introduce wall artifacts when slowing down, due to the potentially large variance in print speeds causing the extruder to be unable to keep up with the requested flow change.\n" +"Root cause of these artifacts is most likely PA tuning being slightly off, especially when combined with a high PA smooth time.\n" "\n" "Recommendations when enabling this option:\n" -"1. Reduce Pressure Advance smooth time to 0.015 - 0.02 so the extruder " -"reacts quickly to the speed changes.\n" -"2. Increase the minimum print speeds to limit the magnitude of the slowdown " -"and reduce the variance between fast and slow segments.\n" -"3. If artifacts still appear, enable Extrusion Rate Smoothing (ERS) to " -"further smooth the flow transitions.\n" +"1. Reduce Pressure Advance smooth time to 0.015 - 0.02 so the extruder reacts quickly to the speed changes.\n" +"2. Increase the minimum print speeds to limit the magnitude of the slowdown and reduce the variance between fast and slow segments.\n" +"3. If artifacts still appear, enable Extrusion Rate Smoothing (ERS) to further smooth the flow transitions.\n" "\n" -"Note: When this option is enabled, overhang perimeters are treated like " -"overhangs, meaning the overhang speed is applied even if the overhanging " -"perimeter is part of a bridge.\n" -"For example, when the perimeters are 100% overhanging, with no wall " -"supporting them from underneath, the 100% overhang speed will be applied." +"Note: When this option is enabled, overhang perimeters are treated like overhangs, meaning the overhang speed is applied even if the overhanging perimeter is part of a bridge.\n" +"For example, when the perimeters are 100% overhanging, with no wall supporting them from underneath, the 100% overhang speed will be applied." msgstr "" "เปิดใช้งานตัวเลือกนี้เพื่อชะลอการพิมพ์ในพื้นที่ที่เส้นรอบวงอาจงอขึ้น\n" -"ตัวอย่างเช่น จะชะลอเพิ่มเติมเมื่อพิมพ์ส่วนยื่นที่มุมแหลม เช่นด้านหน้าของตัวเรือ Benchy " -"ช่วยลดการงอที่สะสมข้ามหลายชั้น\n" +"ตัวอย่างเช่น จะชะลอเพิ่มเติมเมื่อพิมพ์ส่วนยื่นที่มุมแหลม เช่นด้านหน้าของตัวเรือ Benchy ช่วยลดการงอที่สะสมข้ามหลายชั้น\n" "\n" -"โดยทั่วไปแนะนำให้เปิดใช้งาน เว้นแต่ระบบระบายความร้อนของเครื่องพิมพ์แรงพอ " -"หรือความเร็วพิมพ์ช้าพอที่เส้นรอบวงจะไม่งอ\n" -"หากพิมพ์ด้วยความเร็วเส้นรอบวงด้านนอกสูง พารามิเตอร์นี้อาจทำให้เกิดข้อบกพร่องบนผนังเมื่อชะลอ " -"เนื่องจากความแตกต่างของความเร็วพิมพ์ที่อาจมาก " -"ทำให้ชุดดันเส้นตอบสนองการเปลี่ยนอัตราการไหลไม่ทัน\n" -"สาเหตุหลักของข้อบกพร่องเหล่านี้มักมาจากการปรับ PA คลาดเคลื่อนเล็กน้อย โดยเฉพาะเมื่อใช้ร่วมกับ " -"PA smooth time ที่สูง\n" +"โดยทั่วไปแนะนำให้เปิดใช้งาน เว้นแต่ระบบระบายความร้อนของเครื่องพิมพ์แรงพอ หรือความเร็วพิมพ์ช้าพอที่เส้นรอบวงจะไม่งอ\n" +"หากพิมพ์ด้วยความเร็วเส้นรอบวงด้านนอกสูง พารามิเตอร์นี้อาจทำให้เกิดข้อบกพร่องบนผนังเมื่อชะลอ เนื่องจากความแตกต่างของความเร็วพิมพ์ที่อาจมาก ทำให้ชุดดันเส้นตอบสนองการเปลี่ยนอัตราการไหลไม่ทัน\n" +"สาเหตุหลักของข้อบกพร่องเหล่านี้มักมาจากการปรับ PA คลาดเคลื่อนเล็กน้อย โดยเฉพาะเมื่อใช้ร่วมกับ PA smooth time ที่สูง\n" "\n" "คำแนะนำเมื่อเปิดใช้งานตัวเลือกนี้:\n" -"1. ลด Pressure Advance smooth time เป็น 0.015 - 0.02 " -"เพื่อให้ชุดดันเส้นตอบสนองการเปลี่ยนความเร็วได้เร็วขึ้น\n" +"1. ลด Pressure Advance smooth time เป็น 0.015 - 0.02 เพื่อให้ชุดดันเส้นตอบสนองการเปลี่ยนความเร็วได้เร็วขึ้น\n" "2. เพิ่มความเร็วพิมพ์ขั้นต่ำเพื่อจำกัดขนาดการชะลอและลดความแตกต่างระหว่างช่วงเร็วและช้า\n" -"3. หากยังมีข้อบกพร่อง ให้เปิดใช้งาน Extrusion Rate Smoothing (ERS) " -"เพื่อปรับการเปลี่ยนอัตราการไหลให้เรียบขึ้น\n" +"3. หากยังมีข้อบกพร่อง ให้เปิดใช้งาน Extrusion Rate Smoothing (ERS) เพื่อปรับการเปลี่ยนอัตราการไหลให้เรียบขึ้น\n" "\n" -"หมายเหตุ: เมื่อเปิดใช้งานตัวเลือกนี้ เส้นรอบวงส่วนยื่นจะถูกจัดเป็นส่วนยื่น " -"หมายความว่าจะใช้ความเร็วส่วนยื่นแม้เส้นรอบวงนั้นเป็นส่วนหนึ่งของสะพาน\n" +"หมายเหตุ: เมื่อเปิดใช้งานตัวเลือกนี้ เส้นรอบวงส่วนยื่นจะถูกจัดเป็นส่วนยื่น หมายความว่าจะใช้ความเร็วส่วนยื่นแม้เส้นรอบวงนั้นเป็นส่วนหนึ่งของสะพาน\n" "ตัวอย่างเช่น เมื่อเส้นรอบวงยื่น 100% โดยไม่มีผนังรองรับจากด้านล่าง จะใช้ความเร็วส่วนยื่น 100%" msgid "mm/s or %" @@ -13261,26 +11731,17 @@ msgstr "มิลลิเมตร/วินาที หรือ %" msgid "" "Speed of the externally visible bridge extrusions.\n" "\n" -"In addition, if Slow down for curled perimeters is disabled or Classic " -"overhang mode is enabled, it will be the print speed of overhang walls that " -"are supported by less than 13%, whether they are part of a bridge or an " -"overhang." +"In addition, if Slow down for curled perimeters is disabled or Classic overhang mode is enabled, it will be the print speed of overhang walls that are supported by less than 13%, whether they are part of a bridge or an overhang." msgstr "" "ความเร็วของการอัดขึ้นรูปสะพานที่มองเห็นได้จากภายนอก\n" "\n" -"นอกจากนี้ หากปิดใช้งานการชะลอความเร็วสำหรับขอบม้วนงอหรือเปิดใช้งานโหมดระยะยื่นแบบคลาสสิก " -"มันจะเป็นความเร็วในการพิมพ์ของผนังส่วนยื่นที่รองรับน้อยกว่า 13% " -"ไม่ว่าจะเป็นส่วนหนึ่งของสะพานหรือส่วนยื่นก็ตาม" +"นอกจากนี้ หากปิดใช้งานการชะลอความเร็วสำหรับขอบม้วนงอหรือเปิดใช้งานโหมดระยะยื่นแบบคลาสสิก มันจะเป็นความเร็วในการพิมพ์ของผนังส่วนยื่นที่รองรับน้อยกว่า 13% ไม่ว่าจะเป็นส่วนหนึ่งของสะพานหรือส่วนยื่นก็ตาม" msgid "Internal" msgstr "ภายใน" -msgid "" -"Speed of internal bridges. If the value is expressed as a percentage, it " -"will be calculated based on the bridge_speed. Default value is 150%." -msgstr "" -"ความเร็วของสะพานภายใน หากค่าแสดงเป็นเปอร์เซ็นต์ ค่าดังกล่าวจะถูกคำนวณตาม bridge_speed " -"ค่าเริ่มต้นคือ 150%" +msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." +msgstr "ความเร็วของสะพานภายใน หากค่าแสดงเป็นเปอร์เซ็นต์ ค่าดังกล่าวจะถูกคำนวณตาม bridge_speed ค่าเริ่มต้นคือ 150%" msgid "Brim width" msgstr "ความกว้าง ขอบยึดชิ้นงาน" @@ -13291,19 +11752,13 @@ msgstr "ระยะห่างจากแบบจำลองถึงเส msgid "Brim type" msgstr "ชนิด ขอบยึดชิ้นงาน" -msgid "" -"This controls the generation of the brim at outer and/or inner side of " -"models. Auto means the brim width is analyzed and calculated automatically." -msgstr "" -"วิธีนี้จะควบคุมการสร้างขอบยึดชิ้นงานที่ด้านนอกและ/หรือด้านในของรุ่น " -"อัตโนมัติหมายถึงความกว้างของขอบยึดชิ้นงานได้รับการวิเคราะห์และคำนวณโดยอัตโนมัติ" +msgid "This controls the generation of the brim at outer and/or inner side of models. Auto means the brim width is analyzed and calculated automatically." +msgstr "วิธีนี้จะควบคุมการสร้างขอบยึดชิ้นงานที่ด้านนอกและ/หรือด้านในของรุ่น อัตโนมัติหมายถึงความกว้างของขอบยึดชิ้นงานได้รับการวิเคราะห์และคำนวณโดยอัตโนมัติ" msgid "Brim-object gap" msgstr "ช่องว่าง ขอบยึดชิ้นงาน กับชิ้นงาน" -msgid "" -"This creates a gap between the innermost brim line and the object and can " -"make the brim easier to remove." +msgid "This creates a gap between the innermost brim line and the object and can make the brim easier to remove." msgstr "ช่องว่างระหว่างเส้นขอบยึดชิ้นงานด้านในสุดกับวัตถุสามารถทำให้ขอบยึดชิ้นงานหลุดออกได้ง่ายขึ้น" msgid "Brim flow ratio" @@ -13312,15 +11767,13 @@ msgstr "อัตราส่วนการไหล ขอบยึดชิ msgid "" "This factor affects the amount of material for brims.\n" "\n" -"The actual brim flow used is calculated by multiplying this value by the " -"filament flow ratio, and if set, the object's flow ratio.\n" +"The actual brim flow used is calculated by multiplying this value by the filament flow ratio, and if set, the object's flow ratio.\n" "\n" "Note: The resulting value will not be affected by the first-layer flow ratio." msgstr "" "ปัจจัยนี้ส่งผลต่อปริมาณวัสดุสำหรับขอบยึดชิ้นงาน\n" "\n" -"การไหลของขอบยึดชิ้นงานที่แท้จริงที่ใช้คำนวณโดยการคูณค่านี้ด้วยอัตราส่วนการไหลของเส้นพลาสติก " -"และหากตั้งค่าไว้ อัตราการไหลของวัตถุ\n" +"การไหลของขอบยึดชิ้นงานที่แท้จริงที่ใช้คำนวณโดยการคูณค่านี้ด้วยอัตราส่วนการไหลของเส้นพลาสติก และหากตั้งค่าไว้ อัตราการไหลของวัตถุ\n" "\n" "หมายเหตุ: ค่าผลลัพธ์จะไม่ได้รับผลกระทบจากอัตราส่วนการไหลของชั้นแรก" @@ -13328,30 +11781,21 @@ msgid "Brim follows compensated outline" msgstr "ขอบยึดชิ้นงาน ปฏิบัติตามโครงร่างที่ได้รับการชดเชย" msgid "" -"When enabled, the brim is aligned with the first-layer perimeter geometry " -"after Elephant Foot Compensation is applied.\n" -"This option is intended for cases where Elephant Foot Compensation " -"significantly alters the first-layer footprint.\n" +"When enabled, the brim is aligned with the first-layer perimeter geometry after Elephant Foot Compensation is applied.\n" +"This option is intended for cases where Elephant Foot Compensation significantly alters the first-layer footprint.\n" "\n" -"If your current setup already works well, enabling it may be unnecessary and " -"can cause the brim to fuse with upper layers." +"If your current setup already works well, enabling it may be unnecessary and can cause the brim to fuse with upper layers." msgstr "" -"เมื่อเปิดใช้งาน " -"ขอบยึดชิ้นงานจะจัดแนวกับรูปทรงเส้นรอบวงชั้นแรกหลังจากใช้การชดเชยอาการฐานบาน\n" +"เมื่อเปิดใช้งาน ขอบยึดชิ้นงานจะจัดแนวกับรูปทรงเส้นรอบวงชั้นแรกหลังจากใช้การชดเชยอาการฐานบาน\n" "ตัวเลือกนี้มีไว้สำหรับกรณีที่การชดเชยอาการฐานบานเปลี่ยนแปลงรอยเท้าชั้นแรกอย่างมีนัยสำคัญ\n" "\n" -"หากการตั้งค่าปัจจุบันของคุณทำงานได้ดีอยู่แล้ว " -"การเปิดใช้งานอาจไม่จำเป็นและอาจทำให้ขอบยึดชิ้นงานหลอมรวมกับชั้นบนได้" +"หากการตั้งค่าปัจจุบันของคุณทำงานได้ดีอยู่แล้ว การเปิดใช้งานอาจไม่จำเป็นและอาจทำให้ขอบยึดชิ้นงานหลอมรวมกับชั้นบนได้" msgid "Combine brims" msgstr "รวมขอบยึดชิ้นงาน" -msgid "" -"Combine multiple brims into one when they are close to each other. This can " -"improve brim adhesion." -msgstr "" -"รวมขอบยึดชิ้นงานหลายอันเป็นอันเดียวเมื่ออยู่ใกล้กัน " -"วิธีนี้สามารถปรับปรุงการยึดเกาะของขอบยึดชิ้นงานได้" +msgid "Combine multiple brims into one when they are close to each other. This can improve brim adhesion." +msgstr "รวมขอบยึดชิ้นงานหลายอันเป็นอันเดียวเมื่ออยู่ใกล้กัน วิธีนี้สามารถปรับปรุงการยึดเกาะของขอบยึดชิ้นงานได้" msgid "Brim ears" msgstr "หู ขอบยึดชิ้นงาน" @@ -13375,12 +11819,10 @@ msgid "Brim ear detection radius" msgstr "รัศมีการตรวจจับขอบหู" msgid "" -"The geometry will be decimated before detecting sharp angles. This parameter " -"indicates the minimum length of the deviation for the decimation.\n" +"The geometry will be decimated before detecting sharp angles. This parameter indicates the minimum length of the deviation for the decimation.\n" "0 to deactivate." msgstr "" -"รูปทรงจะถูกทำลายก่อนที่จะตรวจจับมุมแหลม " -"พารามิเตอร์นี้ระบุความยาวขั้นต่ำของการเบี่ยงเบนสำหรับการทำลาย\n" +"รูปทรงจะถูกทำลายก่อนที่จะตรวจจับมุมแหลม พารามิเตอร์นี้ระบุความยาวขั้นต่ำของการเบี่ยงเบนสำหรับการทำลาย\n" "0 เพื่อปิดการใช้งาน" msgid "Select printers" @@ -13392,28 +11834,16 @@ msgstr "เครื่องที่รองรับขึ้นไป" msgid "Condition" msgstr "เงื่อนไข" -msgid "" -"A Boolean expression using the configuration values of an active printer " -"profile. If this expression evaluates to true, this profile is considered " -"compatible with the active printer profile." -msgstr "" -"นิพจน์บูลีนที่ใช้ค่าการกำหนดค่าของโปรไฟล์เครื่องพิมพ์ที่ใช้งานอยู่ หากนิพจน์นี้ประเมินว่าเป็นจริง " -"โปรไฟล์นี้จะถือว่าเข้ากันได้กับโปรไฟล์เครื่องพิมพ์ที่ใช้งานอยู่" +msgid "A Boolean expression using the configuration values of an active printer profile. If this expression evaluates to true, this profile is considered compatible with the active printer profile." +msgstr "นิพจน์บูลีนที่ใช้ค่าการกำหนดค่าของโปรไฟล์เครื่องพิมพ์ที่ใช้งานอยู่ หากนิพจน์นี้ประเมินว่าเป็นจริง โปรไฟล์นี้จะถือว่าเข้ากันได้กับโปรไฟล์เครื่องพิมพ์ที่ใช้งานอยู่" msgid "Select profiles" msgstr "โปรไฟล์เลือก" -msgid "" -"A Boolean expression using the configuration values of an active print " -"profile. If this expression evaluates to true, this profile is considered " -"compatible with the active print profile." -msgstr "" -"นิพจน์บูลีนที่ใช้ค่าการกำหนดค่าของโปรไฟล์การพิมพ์ที่ใช้งานอยู่ หากนิพจน์นี้ประเมินว่าเป็นจริง " -"โปรไฟล์นี้จะถือว่าเข้ากันได้กับโปรไฟล์การพิมพ์ที่ใช้งานอยู่" +msgid "A Boolean expression using the configuration values of an active print profile. If this expression evaluates to true, this profile is considered compatible with the active print profile." +msgstr "นิพจน์บูลีนที่ใช้ค่าการกำหนดค่าของโปรไฟล์การพิมพ์ที่ใช้งานอยู่ หากนิพจน์นี้ประเมินว่าเป็นจริง โปรไฟล์นี้จะถือว่าเข้ากันได้กับโปรไฟล์การพิมพ์ที่ใช้งานอยู่" -msgid "" -"This determines the print sequence, allowing you to print layer-by-layer or " -"object-by-object." +msgid "This determines the print sequence, allowing you to print layer-by-layer or object-by-object." msgstr "ลำดับการพิมพ์ ทีละชั้น หรือวัตถุต่อวัตถุ" msgid "By layer" @@ -13434,22 +11864,13 @@ msgstr "เป็นรายการวัตถุ" msgid "Slow printing down for better layer cooling" msgstr "ชะลอการพิมพ์ลงเพื่อการระบายความร้อนที่ดีขึ้น" -msgid "" -"Enable this option to slow printing speed down to ensure that the final " -"layer time is not shorter than the layer time threshold in \"Max fan speed " -"threshold\", so that the layer can be cooled for a longer time. This can " -"improve the quality for small details." -msgstr "" -"เปิดใช้งานตัวเลือกนี้เพื่อลดความเร็วในการพิมพ์ลงเพื่อทำให้เวลาเลเยอร์สุดท้ายไม่สั้นกว่าเกณฑ์เวลาของเลเยอร์ใน " -"\"ขีดจำกัดความเร็วพัดลมสูงสุด\" เพื่อให้เลเยอร์นั้นเย็นลงได้นานขึ้น " -"สิ่งนี้สามารถปรับปรุงคุณภาพการระบายความร้อนสำหรับเข็มและรายละเอียดเล็กๆ ได้" +msgid "Enable this option to slow printing speed down to ensure that the final layer time is not shorter than the layer time threshold in \"Max fan speed threshold\", so that the layer can be cooled for a longer time. This can improve the quality for small details." +msgstr "เปิดใช้งานตัวเลือกนี้เพื่อลดความเร็วในการพิมพ์ลงเพื่อทำให้เวลาเลเยอร์สุดท้ายไม่สั้นกว่าเกณฑ์เวลาของเลเยอร์ใน \"ขีดจำกัดความเร็วพัดลมสูงสุด\" เพื่อให้เลเยอร์นั้นเย็นลงได้นานขึ้น สิ่งนี้สามารถปรับปรุงคุณภาพการระบายความร้อนสำหรับเข็มและรายละเอียดเล็กๆ ได้" msgid "Normal printing" msgstr "การพิมพ์ปกติ" -msgid "" -"This is the default acceleration for both normal printing and travel after " -"the first layer." +msgid "This is the default acceleration for both normal printing and travel after the first layer." msgstr "การเร่งความเร็วเริ่มต้นของทั้งการพิมพ์และการเคลื่อนที่ตามปกติ ยกเว้นเลเยอร์เริ่มต้น" msgid "Default filament profile" @@ -13470,24 +11891,17 @@ msgstr "เปิดใช้งานการกรองอากาศ" msgid "Activate for better air filtration. G-code command: M106 P3 S(0-255)" msgstr "เปิดใช้งานเพื่อการกรองอากาศที่ดีขึ้น คำสั่งรหัส G: M106 P3 S(0-255)" -msgid "" -"Enable this to override the fan speed set in custom G-code during print." +msgid "Enable this to override the fan speed set in custom G-code during print." msgstr "เปิดใช้งานสิ่งนี้เพื่อแทนที่ความเร็วพัดลมที่ตั้งไว้ใน G-code แบบกำหนดเองระหว่างการพิมพ์" msgid "On completion" msgstr "เมื่อเสร็จสิ้น" -msgid "" -"Enable this to override the fan speed set in custom G-code after print " -"completion." +msgid "Enable this to override the fan speed set in custom G-code after print completion." msgstr "เปิดใช้งานสิ่งนี้เพื่อแทนที่ความเร็วพัดลมที่ตั้งไว้ใน G-code แบบกำหนดเองหลังจากพิมพ์เสร็จสิ้น" -msgid "" -"Speed of exhaust fan during printing. This speed will override the speed in " -"filament custom G-code." -msgstr "" -"ความเร็วพัดลมดูดอากาศระหว่างการพิมพ์ ความเร็วนี้จะแทนที่ความเร็วใน G-code " -"แบบกำหนดเองของฟิลาเมนต์" +msgid "Speed of exhaust fan during printing. This speed will override the speed in filament custom G-code." +msgstr "ความเร็วพัดลมดูดอากาศระหว่างการพิมพ์ ความเร็วนี้จะแทนที่ความเร็วใน G-code แบบกำหนดเองของฟิลาเมนต์" msgid "Speed of exhaust fan after printing completes." msgstr "ความเร็วของพัดลมดูดอากาศหลังการพิมพ์เสร็จสิ้น" @@ -13495,112 +11909,65 @@ msgstr "ความเร็วของพัดลมดูดอากาศ msgid "No cooling for the first" msgstr "ไม่มีการระบายความร้อนในช่วงแรก" -msgid "" -"Turn off all cooling fans for the first few layers. This can be used to " -"improve build plate adhesion." -msgstr "" -"ปิดพัดลมระบายความร้อนทั้งหมดในช่วงสองสามชั้นแรก สามารถใช้เพื่อปรับปรุงการยึดเกาะของแผ่นรองพื้น" +msgid "Turn off all cooling fans for the first few layers. This can be used to improve build plate adhesion." +msgstr "ปิดพัดลมระบายความร้อนทั้งหมดในช่วงสองสามชั้นแรก สามารถใช้เพื่อปรับปรุงการยึดเกาะของแผ่นรองพื้น" msgid "Don't support bridges" msgstr "ไม่สร้างส่วนรองรับสำหรับสะพาน" -msgid "" -"This disables supporting bridges, which decreases the amount of support " -"required. Bridges can usually be printed directly without support over a " -"reasonable distance." -msgstr "" -"ปิดการสร้างส่วนรองรับสำหรับสะพาน ช่วยลดปริมาณส่วนรองรับที่ต้องใช้ โดยทั่วไปสะพานสั้นๆ " -"พิมพ์ได้โดยตรงโดยไม่ต้องมีส่วนรองรับ" +msgid "This disables supporting bridges, which decreases the amount of support required. Bridges can usually be printed directly without support over a reasonable distance." +msgstr "ปิดการสร้างส่วนรองรับสำหรับสะพาน ช่วยลดปริมาณส่วนรองรับที่ต้องใช้ โดยทั่วไปสะพานสั้นๆ พิมพ์ได้โดยตรงโดยไม่ต้องมีส่วนรองรับ" msgid "Thick external bridges" msgstr "สะพานภายนอกหนา" msgid "" -"If enabled, bridge extrusion uses a line height equal to the nozzle " -"diameter.\n" -"This increases bridge strength and reliability, allowing longer spans, but " -"may worsen appearance.\n" -"If disabled, bridges may look better but are generally reliable only for " -"shorter spans." +"If enabled, bridge extrusion uses a line height equal to the nozzle diameter.\n" +"This increases bridge strength and reliability, allowing longer spans, but may worsen appearance.\n" +"If disabled, bridges may look better but are generally reliable only for shorter spans." msgstr "" "หากเปิดใช้งาน การดันเส้นสะพานจะใช้ความสูงเส้นเท่ากับเส้นผ่านศูนย์กลางหัวฉีด\n" -"ช่วยเพิ่มความแข็งแรงและความน่าเชื่อถือของสะพาน ทำให้ข้ามช่วงระยะไกลขึ้นได้ " -"แต่อาจทำให้รูปลักษณ์แย่ลง\n" +"ช่วยเพิ่มความแข็งแรงและความน่าเชื่อถือของสะพาน ทำให้ข้ามช่วงระยะไกลขึ้นได้ แต่อาจทำให้รูปลักษณ์แย่ลง\n" "หากปิดใช้งาน สะพานอาจดูดีขึ้น แต่โดยทั่วไปเชื่อถือได้เฉพาะช่วงสั้นๆ" msgid "Thick internal bridges" msgstr "สะพานภายในหนา" msgid "" -"If enabled, internal bridge extrusion uses a line height equal to the nozzle " -"diameter.\n" -"This increases internal bridge strength and reliability when printed over " -"sparse infill, but may worsen appearance.\n" -"If disabled, internal bridges may look better but can be less reliable over " -"sparse infill." +"If enabled, internal bridge extrusion uses a line height equal to the nozzle diameter.\n" +"This increases internal bridge strength and reliability when printed over sparse infill, but may worsen appearance.\n" +"If disabled, internal bridges may look better but can be less reliable over sparse infill." msgstr "" "หากเปิดใช้งาน การดันเส้นสะพานด้านในจะใช้ความสูงเส้นเท่ากับเส้นผ่านศูนย์กลางหัวฉีด\n" -"ช่วยเพิ่มความแข็งแรงและความน่าเชื่อถือของสะพานด้านในเมื่อพิมพ์เหนือไส้ในแบบโปร่ง " -"แต่อาจทำให้รูปลักษณ์แย่ลง\n" +"ช่วยเพิ่มความแข็งแรงและความน่าเชื่อถือของสะพานด้านในเมื่อพิมพ์เหนือไส้ในแบบโปร่ง แต่อาจทำให้รูปลักษณ์แย่ลง\n" "หากปิดใช้งาน สะพานด้านในอาจดูดีขึ้น แต่อาจน่าเชื่อถือน้อยลงเมื่อพิมพ์เหนือไส้ในแบบโปร่ง" msgid "Extra bridge layers (beta)" msgstr "เลเยอร์บริดจ์พิเศษ (เบต้า)" msgid "" -"This option enables the generation of an extra bridge layer over internal " -"and/or external bridges.\n" +"This option enables the generation of an extra bridge layer over internal and/or external bridges.\n" "\n" -"Extra bridge layers help improve bridge appearance and reliability, as the " -"solid infill is better supported. This is especially useful in fast " -"printers, where the bridge and solid infill speeds vary greatly. The extra " -"bridge layer results in reduced pillowing on top surfaces, as well as " -"reduced separation of the external bridge layer from its surrounding " -"perimeters.\n" +"Extra bridge layers help improve bridge appearance and reliability, as the solid infill is better supported. This is especially useful in fast printers, where the bridge and solid infill speeds vary greatly. The extra bridge layer results in reduced pillowing on top surfaces, as well as reduced separation of the external bridge layer from its surrounding perimeters.\n" "\n" -"It is generally recommended to set this to at least 'External bridge only', " -"unless specific issues with the sliced model are found.\n" +"It is generally recommended to set this to at least 'External bridge only', unless specific issues with the sliced model are found.\n" "\n" "Options:\n" -"1. Disabled - does not generate second bridge layers. This is the default " -"and is set for compatibility purposes\n" -"2. External bridge only - generates second bridge layers for external-facing " -"bridges only. Please note that small bridges that are shorter or narrower " -"than the set number of perimeters will be skipped as they would not benefit " -"from a second bridge layer. If generated, the second bridge layer will be " -"extruded parallel to the first bridge layer to reinforce the bridge " -"strength\n" -"3. Internal bridge only - generates second bridge layers for internal " -"bridges over sparse infill only. Please note that the internal bridges count " -"towards the top shell layer count of your model. The second internal bridge " -"layer will be extruded as close to perpendicular to the first as possible. " -"If multiple regions in the same island, with varying bridge angles are " -"present, the last region of that island will be selected as the angle " -"reference\n" -"4. Apply to all - generates second bridge layers for both internal and " -"external-facing bridges\n" +"1. Disabled - does not generate second bridge layers. This is the default and is set for compatibility purposes\n" +"2. External bridge only - generates second bridge layers for external-facing bridges only. Please note that small bridges that are shorter or narrower than the set number of perimeters will be skipped as they would not benefit from a second bridge layer. If generated, the second bridge layer will be extruded parallel to the first bridge layer to reinforce the bridge strength\n" +"3. Internal bridge only - generates second bridge layers for internal bridges over sparse infill only. Please note that the internal bridges count towards the top shell layer count of your model. The second internal bridge layer will be extruded as close to perpendicular to the first as possible. If multiple regions in the same island, with varying bridge angles are present, the last region of that island will be selected as the angle reference\n" +"4. Apply to all - generates second bridge layers for both internal and external-facing bridges\n" msgstr "" "ตัวเลือกนี้ช่วยให้สร้างชั้นสะพานพิเศษเหนือสะพานภายในและ/หรือภายนอกได้\n" "\n" -"ชั้นสะพานพิเศษช่วยปรับปรุงรูปลักษณ์และความน่าเชื่อถือของสะพาน " -"เพราะไส้ในแบบทึบได้รับการรองรับดีขึ้น " -"มีประโยชน์อย่างยิ่งกับเครื่องพิมพ์ความเร็วสูงที่ความเร็วสะพานและไส้ในแบบทึบต่างกันมาก " -"ชั้นสะพานพิเศษช่วยลดการโป่งของผิวด้านบน " -"รวมถึงลดการแยกตัวของชั้นสะพานภายนอกจากเส้นรอบวงโดยรอบ\n" +"ชั้นสะพานพิเศษช่วยปรับปรุงรูปลักษณ์และความน่าเชื่อถือของสะพาน เพราะไส้ในแบบทึบได้รับการรองรับดีขึ้น มีประโยชน์อย่างยิ่งกับเครื่องพิมพ์ความเร็วสูงที่ความเร็วสะพานและไส้ในแบบทึบต่างกันมาก ชั้นสะพานพิเศษช่วยลดการโป่งของผิวด้านบน รวมถึงลดการแยกตัวของชั้นสะพานภายนอกจากเส้นรอบวงโดยรอบ\n" "\n" -"โดยทั่วไปขอแนะนำให้ตั้งค่านี้เป็น 'สะพานภายนอกเท่านั้น' เป็นอย่างน้อย " -"เว้นแต่จะพบปัญหาเฉพาะกับโมเดลที่แบ่งส่วน\n" +"โดยทั่วไปขอแนะนำให้ตั้งค่านี้เป็น 'สะพานภายนอกเท่านั้น' เป็นอย่างน้อย เว้นแต่จะพบปัญหาเฉพาะกับโมเดลที่แบ่งส่วน\n" "\n" "ตัวเลือก:\n" "1. ปิดใช้งาน - ไม่สร้างชั้นสะพานที่สอง นี่เป็นค่าเริ่มต้นและตั้งค่าไว้เพื่อความเข้ากันได้\n" -"2. สะพานภายนอกเท่านั้น - สร้างชั้นสะพานที่สองสำหรับสะพานที่หันหน้าไปทางภายนอกเท่านั้น " -"โปรดทราบว่าสะพานขนาดเล็กที่สั้นกว่าหรือแคบกว่าจำนวนเส้นรอบวงที่ตั้งไว้จะถูกข้ามไป " -"เนื่องจากจะไม่ได้รับประโยชน์จากชั้นสะพานที่สอง หากสร้างขึ้น " -"ชั้นสะพานที่สองจะถูกอัดรีดขนานกับชั้นสะพานแรกเพื่อเสริมความแข็งแรงของสะพาน\n" -"3. สะพานภายในเท่านั้น - สร้างชั้นสะพานที่สองสำหรับสะพานภายในเหนือไส้ในแบบโปร่งเท่านั้น " -"โปรดทราบว่าสะพานภายในจะนับรวมกับจำนวนชั้นเปลือกด้านบนของโมเดล " -"ชั้นสะพานภายในที่สองจะถูกอัดรีดให้ใกล้เคียงกับแนวตั้งฉากกับชั้นแรกมากที่สุด " -"หากมีหลายพื้นที่ในเกาะเดียวกันพร้อมมุมสะพานต่างกัน พื้นที่สุดท้ายของเกาะนั้นจะถูกเลือกเป็นมุมอ้างอิง\n" +"2. สะพานภายนอกเท่านั้น - สร้างชั้นสะพานที่สองสำหรับสะพานที่หันหน้าไปทางภายนอกเท่านั้น โปรดทราบว่าสะพานขนาดเล็กที่สั้นกว่าหรือแคบกว่าจำนวนเส้นรอบวงที่ตั้งไว้จะถูกข้ามไป เนื่องจากจะไม่ได้รับประโยชน์จากชั้นสะพานที่สอง หากสร้างขึ้น ชั้นสะพานที่สองจะถูกอัดรีดขนานกับชั้นสะพานแรกเพื่อเสริมความแข็งแรงของสะพาน\n" +"3. สะพานภายในเท่านั้น - สร้างชั้นสะพานที่สองสำหรับสะพานภายในเหนือไส้ในแบบโปร่งเท่านั้น โปรดทราบว่าสะพานภายในจะนับรวมกับจำนวนชั้นเปลือกด้านบนของโมเดล ชั้นสะพานภายในที่สองจะถูกอัดรีดให้ใกล้เคียงกับแนวตั้งฉากกับชั้นแรกมากที่สุด หากมีหลายพื้นที่ในเกาะเดียวกันพร้อมมุมสะพานต่างกัน พื้นที่สุดท้ายของเกาะนั้นจะถูกเลือกเป็นมุมอ้างอิง\n" "4. นำไปใช้กับทั้งหมด - สร้างชั้นสะพานที่สองสำหรับสะพานที่หันหน้าไปทางภายในและภายนอก\n" msgid "External bridge only" @@ -13616,43 +11983,21 @@ msgid "Filter out small internal bridges" msgstr "กรองบริดจ์ภายในขนาดเล็กออก" msgid "" -"This option can help reduce pillowing on top surfaces in heavily slanted or " -"curved models.\n" -"By default, small internal bridges are filtered out and the internal solid " -"infill is printed directly over the sparse infill. This works well in most " -"cases, speeding up printing without too much compromise on top surface " -"quality.\n" -"However, in heavily slanted or curved models, especially where too low a " -"sparse infill density is used, this may result in curling of the unsupported " -"solid infill, causing pillowing.\n" -"Enabling limited filtering or no filtering will print internal bridge layer " -"over slightly unsupported internal solid infill. The options below control " -"the sensitivity of the filtering, i.e. they control where internal bridges " -"are created:\n" -"1. Filter - enables this option. This is the default behavior and works well " -"in most cases\n" -"2. Limited filtering - creates internal bridges on heavily slanted surfaces " -"while avoiding unnecessary bridges. This works well for most difficult " -"models\n" -"3. No filtering - creates internal bridges on every potential internal " -"overhang. This option is useful for heavily slanted top surface models; " -"however, in most cases, it creates too many unnecessary bridges." +"This option can help reduce pillowing on top surfaces in heavily slanted or curved models.\n" +"By default, small internal bridges are filtered out and the internal solid infill is printed directly over the sparse infill. This works well in most cases, speeding up printing without too much compromise on top surface quality.\n" +"However, in heavily slanted or curved models, especially where too low a sparse infill density is used, this may result in curling of the unsupported solid infill, causing pillowing.\n" +"Enabling limited filtering or no filtering will print internal bridge layer over slightly unsupported internal solid infill. The options below control the sensitivity of the filtering, i.e. they control where internal bridges are created:\n" +"1. Filter - enables this option. This is the default behavior and works well in most cases\n" +"2. Limited filtering - creates internal bridges on heavily slanted surfaces while avoiding unnecessary bridges. This works well for most difficult models\n" +"3. No filtering - creates internal bridges on every potential internal overhang. This option is useful for heavily slanted top surface models; however, in most cases, it creates too many unnecessary bridges." msgstr "" "ตัวเลือกนี้สามารถช่วยลดการหมอนอิงบนพื้นผิวด้านบนในรุ่นที่เอียงหรือโค้งมาก\n" -"ตามค่าเริ่มต้น บริดจ์ภายในขนาดเล็กจะถูกกรองออก และ ไส้ใน แบบทึบภายในจะถูกพิมพ์โดยตรงบน " -"ไส้ใน แบบกระจัดกระจาย วิธีนี้ใช้ได้ผลดีในกรณีส่วนใหญ่ " -"โดยจะช่วยเร่งการพิมพ์โดยไม่กระทบต่อคุณภาพพื้นผิวด้านบนมากเกินไป\n" -"อย่างไรก็ตาม ในรุ่นที่เอียงหรือโค้งมาก " -"โดยเฉพาะอย่างยิ่งเมื่อใช้ความหนาแน่นของไส้ในที่เบาบางต่ำเกินไป " -"อาจส่งผลให้ไส้ในที่เป็นของแข็งที่ไม่ได้รับส่วนรองรับโค้งงอ ทำให้เกิดการหมอนหนุน\n" -"การเปิดใช้งานการกรองแบบจำกัดหรือไม่มีการกรองจะพิมพ์เลเยอร์บริดจ์ภายในทับไส้ในโซลิดภายในที่ไม่รองรับเล็กน้อย " -"ตัวเลือกด้านล่างควบคุมความไวของการกรอง กล่าวคือ ควบคุมตำแหน่งที่สร้างบริดจ์ภายใน:\n" +"ตามค่าเริ่มต้น บริดจ์ภายในขนาดเล็กจะถูกกรองออก และ ไส้ใน แบบทึบภายในจะถูกพิมพ์โดยตรงบน ไส้ใน แบบกระจัดกระจาย วิธีนี้ใช้ได้ผลดีในกรณีส่วนใหญ่ โดยจะช่วยเร่งการพิมพ์โดยไม่กระทบต่อคุณภาพพื้นผิวด้านบนมากเกินไป\n" +"อย่างไรก็ตาม ในรุ่นที่เอียงหรือโค้งมาก โดยเฉพาะอย่างยิ่งเมื่อใช้ความหนาแน่นของไส้ในที่เบาบางต่ำเกินไป อาจส่งผลให้ไส้ในที่เป็นของแข็งที่ไม่ได้รับส่วนรองรับโค้งงอ ทำให้เกิดการหมอนหนุน\n" +"การเปิดใช้งานการกรองแบบจำกัดหรือไม่มีการกรองจะพิมพ์เลเยอร์บริดจ์ภายในทับไส้ในโซลิดภายในที่ไม่รองรับเล็กน้อย ตัวเลือกด้านล่างควบคุมความไวของการกรอง กล่าวคือ ควบคุมตำแหน่งที่สร้างบริดจ์ภายใน:\n" "1. ตัวกรอง - เปิดใช้งานตัวเลือกนี้ นี่เป็นพฤติกรรมเริ่มต้นและทำงานได้ดีในกรณีส่วนใหญ่\n" -"2. การกรองแบบจำกัด - สร้างสะพานภายในบนพื้นผิวที่ลาดเอียงอย่างมาก " -"ในขณะที่หลีกเลี่ยงสะพานที่ไม่จำเป็น วิธีนี้ใช้ได้ผลดีกับโมเดลที่ยากที่สุด\n" -"3. ไม่มีการกรอง - สร้างสะพานภายในบนทุกส่วนยื่นภายในที่อาจเกิดขึ้น " -"ตัวเลือกนี้มีประโยชน์สำหรับโมเดลพื้นผิวด้านบนที่เอียงมาก อย่างไรก็ตาม ในกรณีส่วนใหญ่ " -"มันจะสร้างบริดจ์ที่ไม่จำเป็นมากเกินไป" +"2. การกรองแบบจำกัด - สร้างสะพานภายในบนพื้นผิวที่ลาดเอียงอย่างมาก ในขณะที่หลีกเลี่ยงสะพานที่ไม่จำเป็น วิธีนี้ใช้ได้ผลดีกับโมเดลที่ยากที่สุด\n" +"3. ไม่มีการกรอง - สร้างสะพานภายในบนทุกส่วนยื่นภายในที่อาจเกิดขึ้น ตัวเลือกนี้มีประโยชน์สำหรับโมเดลพื้นผิวด้านบนที่เอียงมาก อย่างไรก็ตาม ในกรณีส่วนใหญ่ มันจะสร้างบริดจ์ที่ไม่จำเป็นมากเกินไป" msgid "Limited filtering" msgstr "การกรองที่จำกัด" @@ -13663,14 +12008,8 @@ msgstr "ไม่มีการกรอง" msgid "Max bridge length" msgstr "ความยาวสะพานสูงสุด" -msgid "" -"This is the maximum length of bridges that don't need support. Set it to 0 " -"if you want all bridges to be supported, and set it to a very large value if " -"you don't want any bridges to be supported." -msgstr "" -"ความยาวสูงสุดของสะพานที่ไม่ต้องการการรองรับ ตั้งค่าเป็น 0 " -"หากคุณต้องการให้บริดจ์ทั้งหมดได้รับส่วนรองรับ และตั้งค่าเป็นค่าที่สูงมากหากคุณไม่ต้องการให้บริดจ์ใดๆ " -"ได้รับการรองรับ" +msgid "This is the maximum length of bridges that don't need support. Set it to 0 if you want all bridges to be supported, and set it to a very large value if you don't want any bridges to be supported." +msgstr "ความยาวสูงสุดของสะพานที่ไม่ต้องการการรองรับ ตั้งค่าเป็น 0 หากคุณต้องการให้บริดจ์ทั้งหมดได้รับส่วนรองรับ และตั้งค่าเป็นค่าที่สูงมากหากคุณไม่ต้องการให้บริดจ์ใดๆ ได้รับการรองรับ" msgid "End G-code" msgstr "จบ G-code" @@ -13681,9 +12020,7 @@ msgstr "สิ้นสุด G-code เมื่อพิมพ์เสร็ msgid "Between Object G-code" msgstr "ระหว่างวัตถุ G-code" -msgid "" -"Insert G-code between objects. This parameter will only come into effect " -"when you print your models object by object." +msgid "Insert G-code between objects. This parameter will only come into effect when you print your models object by object." msgstr "แทรกรหัส G ระหว่างวัตถุ พารามิเตอร์นี้จะมีผลเมื่อคุณพิมพ์โมเดลของคุณทีละวัตถุเท่านั้น" msgid "Add end G-code when finishing the printing of this filament." @@ -13693,18 +12030,15 @@ msgid "Ensure vertical shell thickness" msgstr "ตรวจสอบความหนาของเปลือกแนวตั้ง" msgid "" -"Add solid infill near sloping surfaces to guarantee the vertical shell " -"thickness (top+bottom solid layers)\n" -"None: No solid infill will be added anywhere. Caution: Use this option " -"carefully if your model has sloped surfaces\n" +"Add solid infill near sloping surfaces to guarantee the vertical shell thickness (top+bottom solid layers)\n" +"None: No solid infill will be added anywhere. Caution: Use this option carefully if your model has sloped surfaces\n" "Critical Only: Avoid adding solid infill for walls\n" "Moderate: Add solid infill for heavily sloping surfaces only\n" "All: Add solid infill for all suitable sloping surfaces\n" "Default value is All." msgstr "" "เพิ่มไส้ในแบบทึบใกล้พื้นผิวลาดเอียงเพื่อรับประกันความหนาเปลือกในแนวตั้ง (ชั้นทึบบน + ล่าง)\n" -"ไม่มี: จะไม่เพิ่มไส้ในแบบทึบในส่วนใดเลย ข้อควรระวัง: " -"ใช้ตัวเลือกนี้อย่างระมัดระวังหากโมเดลมีพื้นผิวลาดเอียง\n" +"ไม่มี: จะไม่เพิ่มไส้ในแบบทึบในส่วนใดเลย ข้อควรระวัง: ใช้ตัวเลือกนี้อย่างระมัดระวังหากโมเดลมีพื้นผิวลาดเอียง\n" "เฉพาะจุดวิกฤต: หลีกเลี่ยงการเพิ่มไส้ในแบบทึบสำหรับผนัง\n" "ปานกลาง: เพิ่มไส้ในแบบทึบเฉพาะพื้นผิวที่ลาดเอียงมาก\n" "ทั้งหมด: เพิ่มไส้ในแบบทึบสำหรับพื้นผิวลาดเอียงที่เหมาะสมทั้งหมด\n" @@ -13749,51 +12083,31 @@ msgstr "เกลียวแปดเหลี่ยม" msgid "Bottom surface pattern" msgstr "รูปแบบผิวด้านล่าง" -msgid "" -"This is the line pattern of bottom surface infill, not including bridge " -"infill." +msgid "This is the line pattern of bottom surface infill, not including bridge infill." msgstr "รูปแบบเส้นของไส้ในพื้นผิวด้านล่าง ไม่ใช่ไส้ในแบบบริดจ์" msgid "Internal solid infill pattern" msgstr "รูปแบบไส้ในของแข็งภายใน" -msgid "" -"Line pattern of internal solid infill. if the detect narrow internal solid " -"infill be enabled, the concentric pattern will be used for the small area." -msgstr "" -"ลวดลายเส้นทึบภายใน หากเปิดใช้งานการตรวจจับไส้ในของแข็งภายในที่แคบ " -"รูปแบบศูนย์กลางจะถูกใช้สำหรับพื้นที่ขนาดเล็ก" +msgid "Line pattern of internal solid infill. if the detect narrow internal solid infill be enabled, the concentric pattern will be used for the small area." +msgstr "ลวดลายเส้นทึบภายใน หากเปิดใช้งานการตรวจจับไส้ในของแข็งภายในที่แคบ รูปแบบศูนย์กลางจะถูกใช้สำหรับพื้นที่ขนาดเล็ก" -msgid "" -"Line width of outer wall. If expressed as a %, it will be computed over the " -"nozzle diameter." +msgid "Line width of outer wall. If expressed as a %, it will be computed over the nozzle diameter." msgstr "ความกว้างของเส้นของผนังด้านนอก หากแสดงเป็น % จะคำนวณตามเส้นผ่านศูนย์กลางของหัวฉีด" -msgid "" -"This is the printing speed for the outer walls of parts. These are generally " -"printed slower than inner walls for higher quality." -msgstr "" -"ความเร็วของผนังด้านนอกซึ่งอยู่ด้านนอกสุดและมองเห็นได้ " -"เคยช้ากว่าความเร็วผนังด้านในเพื่อให้ได้คุณภาพที่ดีขึ้น" +msgid "This is the printing speed for the outer walls of parts. These are generally printed slower than inner walls for higher quality." +msgstr "ความเร็วของผนังด้านนอกซึ่งอยู่ด้านนอกสุดและมองเห็นได้ เคยช้ากว่าความเร็วผนังด้านในเพื่อให้ได้คุณภาพที่ดีขึ้น" msgid "Small perimeters" msgstr "เส้นรอบวงเล็ก" -msgid "" -"This separate setting will affect the speed of perimeters having radius <= " -"small_perimeter_threshold (usually holes). If expressed as percentage (for " -"example: 80%) it will be calculated on the outer wall speed setting above. " -"Set to zero for auto." -msgstr "" -"การตั้งค่าแยกนี้จะส่งผลต่อความเร็วของเส้นรอบวงที่มีรัศมี <= small_perimeter_threshold " -"(โดยปกติคือรู) หากแสดงเป็นเปอร์เซ็นต์ (เช่น 80%) " -"ระบบจะคำนวณตามการตั้งค่าความเร็วผนังด้านนอกด้านบน ตั้งค่าเป็นศูนย์สำหรับรถยนต์" +msgid "This separate setting will affect the speed of perimeters having radius <= small_perimeter_threshold (usually holes). If expressed as percentage (for example: 80%) it will be calculated on the outer wall speed setting above. Set to zero for auto." +msgstr "การตั้งค่าแยกนี้จะส่งผลต่อความเร็วของเส้นรอบวงที่มีรัศมี <= small_perimeter_threshold (โดยปกติคือรู) หากแสดงเป็นเปอร์เซ็นต์ (เช่น 80%) ระบบจะคำนวณตามการตั้งค่าความเร็วผนังด้านนอกด้านบน ตั้งค่าเป็นศูนย์สำหรับรถยนต์" msgid "Small perimeters threshold" msgstr "เกณฑ์ขอบเขตขนาดเล็ก" -msgid "" -"This sets the threshold for small perimeter length. Default threshold is 0mm." +msgid "This sets the threshold for small perimeter length. Default threshold is 0mm." msgstr "นี่เป็นการกำหนดเกณฑ์สำหรับความยาวเส้นรอบวงเล็กน้อย เกณฑ์เริ่มต้นคือ 0 มม." msgid "Walls printing order" @@ -13802,42 +12116,19 @@ msgstr "สั่งพิมพ์ผนัง" msgid "" "Print sequence of the internal (inner) and external (outer) walls.\n" "\n" -"Use Inner/Outer for best overhangs. This is because the overhanging walls " -"can adhere to a neighbouring perimeter while printing. However, this option " -"results in slightly reduced surface quality as the external perimeter is " -"deformed by being squashed to the internal perimeter.\n" +"Use Inner/Outer for best overhangs. This is because the overhanging walls can adhere to a neighbouring perimeter while printing. However, this option results in slightly reduced surface quality as the external perimeter is deformed by being squashed to the internal perimeter.\n" "\n" -"Use Inner/Outer/Inner for the best external surface finish and dimensional " -"accuracy as the external wall is printed undisturbed from an internal " -"perimeter. However, overhang performance will reduce as there is no internal " -"perimeter to print the external wall against. This option requires a minimum " -"of 3 walls to be effective as it prints the internal walls from the 3rd " -"perimeter onwards first, then the external perimeter and, finally, the first " -"internal perimeter. This option is recommended against the Outer/Inner " -"option in most cases.\n" +"Use Inner/Outer/Inner for the best external surface finish and dimensional accuracy as the external wall is printed undisturbed from an internal perimeter. However, overhang performance will reduce as there is no internal perimeter to print the external wall against. This option requires a minimum of 3 walls to be effective as it prints the internal walls from the 3rd perimeter onwards first, then the external perimeter and, finally, the first internal perimeter. This option is recommended against the Outer/Inner option in most cases.\n" "\n" -"Use Outer/Inner for the same external wall quality and dimensional accuracy " -"benefits of Inner/Outer/Inner option. However, the Z seams will appear less " -"consistent as the first extrusion of a new layer starts on a visible surface." +"Use Outer/Inner for the same external wall quality and dimensional accuracy benefits of Inner/Outer/Inner option. However, the Z seams will appear less consistent as the first extrusion of a new layer starts on a visible surface." msgstr "" "ลำดับการพิมพ์ของผนังภายใน (ด้านใน) และภายนอก (ด้านนอก)\n" "\n" -"ใช้ด้านใน/ด้านนอกเพื่อให้ได้ระยะยื่นที่ดีที่สุด " -"เนื่องจากผนังที่ยื่นออกมาสามารถยึดติดกับปริมณฑลใกล้เคียงขณะพิมพ์ได้ อย่างไรก็ตาม " -"ตัวเลือกนี้ส่งผลให้คุณภาพพื้นผิวลดลงเล็กน้อย " -"เนื่องจากเส้นรอบวงภายนอกเสียรูปเนื่องจากการถูกแบนไปยังเส้นรอบวงภายใน\n" +"ใช้ด้านใน/ด้านนอกเพื่อให้ได้ระยะยื่นที่ดีที่สุด เนื่องจากผนังที่ยื่นออกมาสามารถยึดติดกับปริมณฑลใกล้เคียงขณะพิมพ์ได้ อย่างไรก็ตาม ตัวเลือกนี้ส่งผลให้คุณภาพพื้นผิวลดลงเล็กน้อย เนื่องจากเส้นรอบวงภายนอกเสียรูปเนื่องจากการถูกแบนไปยังเส้นรอบวงภายใน\n" "\n" -"ใช้ด้านใน/ด้านนอก/ด้านในเพื่อให้ได้พื้นผิวภายนอกที่ดีที่สุดและความแม่นยำของขนาด " -"เนื่องจากผนังภายนอกถูกพิมพ์โดยไม่ถูกรบกวนจากขอบเขตภายใน อย่างไรก็ตาม " -"ประสิทธิภาพส่วนยื่นจะลดลงเนื่องจากไม่มีขอบเขตภายในให้พิมพ์ติดกับผนังภายนอก " -"ตัวเลือกนี้ต้องใช้ผนังอย่างน้อย 3 แผ่นจึงจะมีประสิทธิภาพ เนื่องจากจะพิมพ์ผนังภายในจากปริมณฑลที่ 3 " -"เป็นต้นไป จากนั้นจึงพิมพ์ปริมณฑลภายนอก และสุดท้ายคือปริมณฑลภายในแรก " -"แนะนำให้ใช้ตัวเลือกนี้กับตัวเลือกด้านนอก/ด้านในในกรณีส่วนใหญ่\n" +"ใช้ด้านใน/ด้านนอก/ด้านในเพื่อให้ได้พื้นผิวภายนอกที่ดีที่สุดและความแม่นยำของขนาด เนื่องจากผนังภายนอกถูกพิมพ์โดยไม่ถูกรบกวนจากขอบเขตภายใน อย่างไรก็ตาม ประสิทธิภาพส่วนยื่นจะลดลงเนื่องจากไม่มีขอบเขตภายในให้พิมพ์ติดกับผนังภายนอก ตัวเลือกนี้ต้องใช้ผนังอย่างน้อย 3 แผ่นจึงจะมีประสิทธิภาพ เนื่องจากจะพิมพ์ผนังภายในจากปริมณฑลที่ 3 เป็นต้นไป จากนั้นจึงพิมพ์ปริมณฑลภายนอก และสุดท้ายคือปริมณฑลภายในแรก แนะนำให้ใช้ตัวเลือกนี้กับตัวเลือกด้านนอก/ด้านในในกรณีส่วนใหญ่\n" "\n" -"ใช้ด้านนอก/" -"ด้านในเพื่อคุณภาพผนังภายนอกที่เหมือนกันและประโยชน์ด้านความแม่นยำของมิติของตัวเลือกด้านใน/" -"ด้านนอก/ด้านใน อย่างไรก็ตาม รอยตะเข็บ Z " -"จะดูสม่ำเสมอน้อยลงเนื่องจากการอัดขึ้นรูปชั้นใหม่ครั้งแรกเริ่มต้นบนพื้นผิวที่มองเห็นได้" +"ใช้ด้านนอก/ด้านในเพื่อคุณภาพผนังภายนอกที่เหมือนกันและประโยชน์ด้านความแม่นยำของมิติของตัวเลือกด้านใน/ด้านนอก/ด้านใน อย่างไรก็ตาม รอยตะเข็บ Z จะดูสม่ำเสมอน้อยลงเนื่องจากการอัดขึ้นรูปชั้นใหม่ครั้งแรกเริ่มต้นบนพื้นผิวที่มองเห็นได้" msgid "Inner/Outer" msgstr "ภายใน/ภายนอก" @@ -13852,37 +12143,25 @@ msgid "Print infill first" msgstr "พิมพ์ไส้ในก่อน" msgid "" -"Order of wall/infill. When the tickbox is unchecked the walls are printed " -"first, which works best in most cases.\n" +"Order of wall/infill. When the tickbox is unchecked the walls are printed first, which works best in most cases.\n" "\n" -"Printing infill first may help with extreme overhangs as the walls have the " -"neighbouring infill to adhere to. However, the infill will slightly push out " -"the printed walls where it is attached to them, resulting in a worse " -"external surface finish. It can also cause the infill to shine through the " -"external surfaces of the part." +"Printing infill first may help with extreme overhangs as the walls have the neighbouring infill to adhere to. However, the infill will slightly push out the printed walls where it is attached to them, resulting in a worse external surface finish. It can also cause the infill to shine through the external surfaces of the part." msgstr "" -"ลำดับของผนัง/ไส้ใน เมื่อยกเลิกการเลือกช่องทำเครื่องหมาย ผนังจะถูกพิมพ์ก่อน " -"ซึ่งใช้งานได้ดีที่สุดในกรณีส่วนใหญ่\n" +"ลำดับของผนัง/ไส้ใน เมื่อยกเลิกการเลือกช่องทำเครื่องหมาย ผนังจะถูกพิมพ์ก่อน ซึ่งใช้งานได้ดีที่สุดในกรณีส่วนใหญ่\n" "\n" -"การพิมพ์แบบ ไส้ใน ก่อนอาจช่วยให้มีระยะยื่นมากเกินไป เนื่องจากผนังมี ไส้ใน ที่อยู่ใกล้เคียงให้ยึดติด " -"อย่างไรก็ตาม ไส้ในจะดันผนังที่พิมพ์ออกมาเล็กน้อยซึ่งติดอยู่ ส่งผลให้พื้นผิวภายนอกที่ได้คุณภาพแย่ลง " -"นอกจากนี้ยังสามารถทำให้ ไส้ใน ส่องผ่านพื้นผิวภายนอกของชิ้นส่วนได้" +"การพิมพ์แบบ ไส้ใน ก่อนอาจช่วยให้มีระยะยื่นมากเกินไป เนื่องจากผนังมี ไส้ใน ที่อยู่ใกล้เคียงให้ยึดติด อย่างไรก็ตาม ไส้ในจะดันผนังที่พิมพ์ออกมาเล็กน้อยซึ่งติดอยู่ ส่งผลให้พื้นผิวภายนอกที่ได้คุณภาพแย่ลง นอกจากนี้ยังสามารถทำให้ ไส้ใน ส่องผ่านพื้นผิวภายนอกของชิ้นส่วนได้" msgid "Wall loop direction" msgstr "ทิศทางของวงผนัง" msgid "" -"The direction which the contour wall loops are extruded when looking down " -"from the top.\n" -"Holes are printed in the opposite direction to the contour to maintain " -"alignment with layers whose contour polygons are incomplete and change " -"direction, also partially forming the contour of a hole.\n" +"The direction which the contour wall loops are extruded when looking down from the top.\n" +"Holes are printed in the opposite direction to the contour to maintain alignment with layers whose contour polygons are incomplete and change direction, also partially forming the contour of a hole.\n" "\n" "This option will be disabled if spiral vase mode is enabled." msgstr "" "ทิศทางที่ลูปผนังรูปร่างถูกอัดออกมาเมื่อมองลงมาจากด้านบน\n" -"รูจะถูกพิมพ์ในทิศทางตรงกันข้ามกับเส้นขอบเพื่อรักษาแนวเดียวกับชั้นที่มีรูปหลายเหลี่ยมเส้นขอบไม่สมบูรณ์และเปลี่ยนทิศทาง " -"รวมถึงสร้างรูปร่างของรูบางส่วนด้วย\n" +"รูจะถูกพิมพ์ในทิศทางตรงกันข้ามกับเส้นขอบเพื่อรักษาแนวเดียวกับชั้นที่มีรูปหลายเหลี่ยมเส้นขอบไม่สมบูรณ์และเปลี่ยนทิศทาง รวมถึงสร้างรูปร่างของรูบางส่วนด้วย\n" "\n" "ตัวเลือกนี้จะถูกปิดใช้งานหากเปิดใช้งานโหมดแจกันเกลียว" @@ -13895,22 +12174,16 @@ msgstr "ตามเข็มนาฬิกา" msgid "Height to rod" msgstr "ความสูงถึงก้าน" -msgid "" -"Distance from the nozzle tip to the lower rod. Used for collision avoidance " -"in by-object printing." +msgid "Distance from the nozzle tip to the lower rod. Used for collision avoidance in by-object printing." msgstr "ระยะห่างของปลายหัวฉีดถึงแกนล่าง ใช้สำหรับการหลีกเลี่ยงการชนกันในการพิมพ์ตามวัตถุ" msgid "Height to lid" msgstr "ความสูงถึงฝา" -msgid "" -"Distance from the nozzle tip to the lid. Used for collision avoidance in by-" -"object printing." +msgid "Distance from the nozzle tip to the lid. Used for collision avoidance in by-object printing." msgstr "ระยะห่างของปลายหัวฉีดถึงฝา ใช้สำหรับการหลีกเลี่ยงการชนกันในการพิมพ์ตามวัตถุ" -msgid "" -"Clearance radius around extruder: used for collision avoidance in by-object " -"printing." +msgid "Clearance radius around extruder: used for collision avoidance in by-object printing." msgstr "รัศมีระยะปลอดภัยรอบชุดดันเส้น ใช้สำหรับการหลีกเลี่ยงการชนกันในการพิมพ์ตามวัตถุ" msgid "Nozzle height" @@ -13922,61 +12195,25 @@ msgstr "ความสูงของปลายหัวฉีด" msgid "Bed mesh min" msgstr "ตาข่ายฐานพิมพ์ขั้นต่ำ" -msgid "" -"This option sets the min point for the allowed bed mesh area. Due to the " -"probe's XY offset, most printers are unable to probe the entire bed. To " -"ensure the probe point does not go outside the bed area, the minimum and " -"maximum points of the bed mesh should be set appropriately. OrcaSlicer " -"ensures that adaptive_bed_mesh_min/adaptive_bed_mesh_max values do not " -"exceed these min/max points. This information can usually be obtained from " -"your printer manufacturer. The default setting is (-99999, -99999), which " -"means there are no limits, thus allowing probing across the entire bed." -msgstr "" -"ตัวเลือกนี้จะตั้งค่าจุดต่ำสุดสำหรับพื้นที่ตาข่ายเบดที่อนุญาต เนื่องจากออฟเซ็ต XY ของโพรบ " -"เครื่องพิมพ์ส่วนใหญ่จึงไม่สามารถตรวจสอบทั้งเบดได้ " -"เพื่อให้แน่ใจว่าจุดโพรบไม่ออกไปนอกบริเวณฐานพิมพ์ " -"ควรตั้งค่าจุดต่ำสุดและสูงสุดของตาข่ายเบดอย่างเหมาะสม OrcaSlicer ช่วยให้มั่นใจได้ว่าค่าของ " -"adaptive_bed_mesh_min/adaptive_bed_mesh_max จะต้องไม่เกินจุดต่ำสุด/สูงสุดเหล่านี้ " -"โดยปกติข้อมูลนี้สามารถรับได้จากผู้ผลิตเครื่องพิมพ์ของคุณ การตั้งค่าเริ่มต้นคือ (-99999, -99999) " -"ซึ่งหมายความว่าไม่มีขีดจำกัด จึงสามารถตรวจวัดทั่วทั้งฐานพิมพ์ได้" +msgid "This option sets the min point for the allowed bed mesh area. Due to the probe's XY offset, most printers are unable to probe the entire bed. To ensure the probe point does not go outside the bed area, the minimum and maximum points of the bed mesh should be set appropriately. OrcaSlicer ensures that adaptive_bed_mesh_min/adaptive_bed_mesh_max values do not exceed these min/max points. This information can usually be obtained from your printer manufacturer. The default setting is (-99999, -99999), which means there are no limits, thus allowing probing across the entire bed." +msgstr "ตัวเลือกนี้จะตั้งค่าจุดต่ำสุดสำหรับพื้นที่ตาข่ายเบดที่อนุญาต เนื่องจากออฟเซ็ต XY ของโพรบ เครื่องพิมพ์ส่วนใหญ่จึงไม่สามารถตรวจสอบทั้งเบดได้ เพื่อให้แน่ใจว่าจุดโพรบไม่ออกไปนอกบริเวณฐานพิมพ์ ควรตั้งค่าจุดต่ำสุดและสูงสุดของตาข่ายเบดอย่างเหมาะสม OrcaSlicer ช่วยให้มั่นใจได้ว่าค่าของ adaptive_bed_mesh_min/adaptive_bed_mesh_max จะต้องไม่เกินจุดต่ำสุด/สูงสุดเหล่านี้ โดยปกติข้อมูลนี้สามารถรับได้จากผู้ผลิตเครื่องพิมพ์ของคุณ การตั้งค่าเริ่มต้นคือ (-99999, -99999) ซึ่งหมายความว่าไม่มีขีดจำกัด จึงสามารถตรวจวัดทั่วทั้งฐานพิมพ์ได้" msgid "Bed mesh max" msgstr "ตาข่ายฐานพิมพ์สูงสุด" -msgid "" -"This option sets the max point for the allowed bed mesh area. Due to the " -"probe's XY offset, most printers are unable to probe the entire bed. To " -"ensure the probe point does not go outside the bed area, the minimum and " -"maximum points of the bed mesh should be set appropriately. OrcaSlicer " -"ensures that adaptive_bed_mesh_min/adaptive_bed_mesh_max values do not " -"exceed these min/max points. This information can usually be obtained from " -"your printer manufacturer. The default setting is (99999, 99999), which " -"means there are no limits, thus allowing probing across the entire bed." -msgstr "" -"ตัวเลือกนี้จะตั้งค่าจุดสูงสุดสำหรับพื้นที่ตาข่ายฐานพิมพ์ที่อนุญาต เนื่องจากออฟเซ็ต XY ของโพรบ " -"เครื่องพิมพ์ส่วนใหญ่จึงไม่สามารถตรวจสอบทั้งเบดได้ " -"เพื่อให้แน่ใจว่าจุดโพรบไม่ออกไปนอกบริเวณฐานพิมพ์ " -"ควรตั้งค่าจุดต่ำสุดและสูงสุดของตาข่ายเบดอย่างเหมาะสม OrcaSlicer ช่วยให้มั่นใจได้ว่าค่าของ " -"adaptive_bed_mesh_min/adaptive_bed_mesh_max จะต้องไม่เกินจุดต่ำสุด/สูงสุดเหล่านี้ " -"โดยปกติข้อมูลนี้สามารถรับได้จากผู้ผลิตเครื่องพิมพ์ของคุณ การตั้งค่าเริ่มต้นคือ (99999, 99999) " -"ซึ่งหมายความว่าไม่มีขีดจำกัด จึงสามารถตรวจวัดทั่วทั้งฐานพิมพ์ได้" +msgid "This option sets the max point for the allowed bed mesh area. Due to the probe's XY offset, most printers are unable to probe the entire bed. To ensure the probe point does not go outside the bed area, the minimum and maximum points of the bed mesh should be set appropriately. OrcaSlicer ensures that adaptive_bed_mesh_min/adaptive_bed_mesh_max values do not exceed these min/max points. This information can usually be obtained from your printer manufacturer. The default setting is (99999, 99999), which means there are no limits, thus allowing probing across the entire bed." +msgstr "ตัวเลือกนี้จะตั้งค่าจุดสูงสุดสำหรับพื้นที่ตาข่ายฐานพิมพ์ที่อนุญาต เนื่องจากออฟเซ็ต XY ของโพรบ เครื่องพิมพ์ส่วนใหญ่จึงไม่สามารถตรวจสอบทั้งเบดได้ เพื่อให้แน่ใจว่าจุดโพรบไม่ออกไปนอกบริเวณฐานพิมพ์ ควรตั้งค่าจุดต่ำสุดและสูงสุดของตาข่ายเบดอย่างเหมาะสม OrcaSlicer ช่วยให้มั่นใจได้ว่าค่าของ adaptive_bed_mesh_min/adaptive_bed_mesh_max จะต้องไม่เกินจุดต่ำสุด/สูงสุดเหล่านี้ โดยปกติข้อมูลนี้สามารถรับได้จากผู้ผลิตเครื่องพิมพ์ของคุณ การตั้งค่าเริ่มต้นคือ (99999, 99999) ซึ่งหมายความว่าไม่มีขีดจำกัด จึงสามารถตรวจวัดทั่วทั้งฐานพิมพ์ได้" msgid "Probe point distance" msgstr "ระยะทางจุดโพรบ" -msgid "" -"This option sets the preferred distance between probe points (grid size) for " -"the X and Y directions, with the default being 50mm for both X and Y." -msgstr "" -"ตัวเลือกนี้จะตั้งค่าระยะห่างที่ต้องการระหว่างจุดโพรบ (ขนาดตาราง) สำหรับทิศทาง X และ Y " -"โดยค่าเริ่มต้นคือ 50 มม. สำหรับทั้ง X และ Y" +msgid "This option sets the preferred distance between probe points (grid size) for the X and Y directions, with the default being 50mm for both X and Y." +msgstr "ตัวเลือกนี้จะตั้งค่าระยะห่างที่ต้องการระหว่างจุดโพรบ (ขนาดตาราง) สำหรับทิศทาง X และ Y โดยค่าเริ่มต้นคือ 50 มม. สำหรับทั้ง X และ Y" msgid "Mesh margin" msgstr "ขอบตาข่าย" -msgid "" -"This option determines the additional distance by which the adaptive bed " -"mesh area should be expanded in the XY directions." +msgid "This option determines the additional distance by which the adaptive bed mesh area should be expanded in the XY directions." msgstr "ตัวเลือกนี้กำหนดระยะเพิ่มเติมที่ควรขยายพื้นที่ตาข่ายเบดแบบปรับได้ในทิศทาง XY" msgid "Grab length" @@ -13991,156 +12228,81 @@ msgstr "ใช้เป็นภาพช่วยเหลือบน UI เ msgid "Extruder offset" msgstr "การชดเชยชุดดันเส้น" -msgid "" -"The material may have volumetric change after switching between molten and " -"crystalline states. This setting changes all extrusion flow of this filament " -"in G-code proportionally. The recommended value range is between 0.95 and " -"1.05. You may be able to tune this value to get a nice flat surface if there " -"is slight overflow or underflow." -msgstr "" -"วัสดุอาจมีการเปลี่ยนแปลงเชิงปริมาตรหลังจากสลับระหว่างสถานะหลอมเหลวและสถานะผลึก " -"การตั้งค่านี้จะเปลี่ยนการไหลอัดรีดทั้งหมดของเส้นพลาสติกนี้ใน G-code ตามสัดส่วน " -"ช่วงค่าที่แนะนำคือระหว่าง 0.95 ถึง 1.05 " -"คุณอาจสามารถปรับค่านี้เพื่อให้ได้พื้นผิวที่เรียบสวยงามได้หากมีน้ำล้นหรืออันเดอร์โฟลว์เล็กน้อย" +msgid "The material may have volumetric change after switching between molten and crystalline states. This setting changes all extrusion flow of this filament in G-code proportionally. The recommended value range is between 0.95 and 1.05. You may be able to tune this value to get a nice flat surface if there is slight overflow or underflow." +msgstr "วัสดุอาจมีการเปลี่ยนแปลงเชิงปริมาตรหลังจากสลับระหว่างสถานะหลอมเหลวและสถานะผลึก การตั้งค่านี้จะเปลี่ยนการไหลอัดรีดทั้งหมดของเส้นพลาสติกนี้ใน G-code ตามสัดส่วน ช่วงค่าที่แนะนำคือระหว่าง 0.95 ถึง 1.05 คุณอาจสามารถปรับค่านี้เพื่อให้ได้พื้นผิวที่เรียบสวยงามได้หากมีน้ำล้นหรืออันเดอร์โฟลว์เล็กน้อย" msgid "" -"The material may have volumetric change after switching between molten and " -"crystalline states. This setting changes all extrusion flow of this filament " -"in G-code proportionally. The recommended value range is between 0.95 and " -"1.05. You may be able to tune this value to get a nice flat surface if there " -"is slight overflow or underflow.\n" +"The material may have volumetric change after switching between molten and crystalline states. This setting changes all extrusion flow of this filament in G-code proportionally. The recommended value range is between 0.95 and 1.05. You may be able to tune this value to get a nice flat surface if there is slight overflow or underflow.\n" "\n" -"The final object flow ratio is this value multiplied by the filament flow " -"ratio." +"The final object flow ratio is this value multiplied by the filament flow ratio." msgstr "" -"วัสดุอาจมีการเปลี่ยนแปลงเชิงปริมาตรหลังจากสลับระหว่างสถานะหลอมเหลวและสถานะผลึก " -"การตั้งค่านี้จะเปลี่ยนการไหลอัดรีดทั้งหมดของเส้นพลาสติกนี้ใน G-code ตามสัดส่วน " -"ช่วงค่าที่แนะนำคือระหว่าง 0.95 ถึง 1.05 " -"คุณอาจสามารถปรับค่านี้เพื่อให้ได้พื้นผิวที่เรียบสวยงามได้หากมีน้ำล้นหรืออันเดอร์โฟลว์เล็กน้อย\n" +"วัสดุอาจมีการเปลี่ยนแปลงเชิงปริมาตรหลังจากสลับระหว่างสถานะหลอมเหลวและสถานะผลึก การตั้งค่านี้จะเปลี่ยนการไหลอัดรีดทั้งหมดของเส้นพลาสติกนี้ใน G-code ตามสัดส่วน ช่วงค่าที่แนะนำคือระหว่าง 0.95 ถึง 1.05 คุณอาจสามารถปรับค่านี้เพื่อให้ได้พื้นผิวที่เรียบสวยงามได้หากมีน้ำล้นหรืออันเดอร์โฟลว์เล็กน้อย\n" "\n" "อัตราการไหลของวัตถุขั้นสุดท้ายคือค่านี้คูณด้วยอัตราการไหลของเส้นพลาสติก" msgid "Enable pressure advance" msgstr "เปิดใช้งานPressure Advance" -msgid "" -"Enable pressure advance, auto calibration result will be overwritten once " -"enabled." +msgid "Enable pressure advance, auto calibration result will be overwritten once enabled." msgstr "เปิดใช้งานการเลื่อนแรงดัน ผลลัพธ์การสอบเทียบอัตโนมัติจะถูกเขียนทับเมื่อเปิดใช้งาน" msgid "Pressure advance (Klipper) AKA Linear advance factor (Marlin)." -msgstr "" -"แรงดันล่วงหน้า (Pressure Advance) (Klipper) AKA Linear Advance Factor (Marlin)" +msgstr "แรงดันล่วงหน้า (Pressure Advance) (Klipper) AKA Linear Advance Factor (Marlin)" msgid "Enable adaptive pressure advance (beta)" msgstr "เปิดใช้งานการปรับPressure Advance (เบต้า)" #, no-c-format, no-boost-format msgid "" -"With increasing print speeds (and hence increasing volumetric flow through " -"the nozzle) and increasing accelerations, it has been observed that the " -"effective PA value typically decreases. This means that a single PA value is " -"not always 100% optimal for all features and a compromise value is usually " -"used that does not cause too much bulging on features with lower flow speed " -"and accelerations while also not causing gaps on faster features.\n" +"With increasing print speeds (and hence increasing volumetric flow through the nozzle) and increasing accelerations, it has been observed that the effective PA value typically decreases. This means that a single PA value is not always 100% optimal for all features and a compromise value is usually used that does not cause too much bulging on features with lower flow speed and accelerations while also not causing gaps on faster features.\n" "\n" -"This feature aims to address this limitation by modeling the response of " -"your printer's extrusion system depending on the volumetric flow speed and " -"acceleration it is printing at. Internally, it generates a fitted model that " -"can extrapolate the needed pressure advance for any given volumetric flow " -"speed and acceleration, which is then emitted to the printer depending on " -"the current print conditions.\n" +"This feature aims to address this limitation by modeling the response of your printer's extrusion system depending on the volumetric flow speed and acceleration it is printing at. Internally, it generates a fitted model that can extrapolate the needed pressure advance for any given volumetric flow speed and acceleration, which is then emitted to the printer depending on the current print conditions.\n" "\n" -"When enabled, the pressure advance value above is overridden. However, a " -"reasonable default value above is strongly recommended to act as a fallback " -"and for when tool changing.\n" +"When enabled, the pressure advance value above is overridden. However, a reasonable default value above is strongly recommended to act as a fallback and for when tool changing.\n" "\n" msgstr "" -"ด้วยความเร็วในการพิมพ์ที่เพิ่มขึ้น (และด้วยเหตุนี้จึงเพิ่มการไหลเชิงปริมาตรผ่านหัวฉีด) " -"และการเร่งความเร็วที่เพิ่มขึ้น พบว่าค่า PA ที่มีประสิทธิผลโดยทั่วไปจะลดลง ซึ่งหมายความว่าค่า PA " -"เดียวอาจไม่ได้เหมาะสมที่สุด 100% เสมอไปสำหรับคุณสมบัติทั้งหมด " -"และโดยปกติแล้วจะใช้ค่าประนีประนอมที่ไม่ทำให้เกิดการปูดมากเกินไปในคุณสมบัติที่มีความเร็วการไหลและความเร่งต่ำ " -"ขณะเดียวกันก็ไม่ทำให้เกิดช่องว่างในคุณสมบัติที่เร็วกว่า\n" +"ด้วยความเร็วในการพิมพ์ที่เพิ่มขึ้น (และด้วยเหตุนี้จึงเพิ่มการไหลเชิงปริมาตรผ่านหัวฉีด) และการเร่งความเร็วที่เพิ่มขึ้น พบว่าค่า PA ที่มีประสิทธิผลโดยทั่วไปจะลดลง ซึ่งหมายความว่าค่า PA เดียวอาจไม่ได้เหมาะสมที่สุด 100% เสมอไปสำหรับคุณสมบัติทั้งหมด และโดยปกติแล้วจะใช้ค่าประนีประนอมที่ไม่ทำให้เกิดการปูดมากเกินไปในคุณสมบัติที่มีความเร็วการไหลและความเร่งต่ำ ขณะเดียวกันก็ไม่ทำให้เกิดช่องว่างในคุณสมบัติที่เร็วกว่า\n" "\n" -"คุณลักษณะนี้มีจุดมุ่งหมายเพื่อแก้ไขข้อจำกัดนี้โดยการสร้างแบบจำลองการตอบสนองของระบบการอัดขึ้นรูปของเครื่องพิมพ์ของคุณ " -"โดยขึ้นอยู่กับความเร็วการไหลตามปริมาตรและความเร่งที่เครื่องพิมพ์ทำการพิมพ์ ภายใน " -"เครื่องพิมพ์จะสร้างแบบจำลองที่ติดตั้งไว้ซึ่งสามารถคาดเดาPressure " -"Advanceที่จำเป็นสำหรับความเร็วและความเร่งในการไหลตามปริมาตรที่กำหนด " -"ซึ่งจากนั้นจะปล่อยไปยังเครื่องพิมพ์โดยขึ้นอยู่กับสภาพการพิมพ์ในปัจจุบัน\n" +"คุณลักษณะนี้มีจุดมุ่งหมายเพื่อแก้ไขข้อจำกัดนี้โดยการสร้างแบบจำลองการตอบสนองของระบบการอัดขึ้นรูปของเครื่องพิมพ์ของคุณ โดยขึ้นอยู่กับความเร็วการไหลตามปริมาตรและความเร่งที่เครื่องพิมพ์ทำการพิมพ์ ภายใน เครื่องพิมพ์จะสร้างแบบจำลองที่ติดตั้งไว้ซึ่งสามารถคาดเดาPressure Advanceที่จำเป็นสำหรับความเร็วและความเร่งในการไหลตามปริมาตรที่กำหนด ซึ่งจากนั้นจะปล่อยไปยังเครื่องพิมพ์โดยขึ้นอยู่กับสภาพการพิมพ์ในปัจจุบัน\n" "\n" -"เมื่อเปิดใช้งาน ค่าล่วงหน้าของแรงดันด้านบนจะถูกแทนที่ อย่างไรก็ตาม " -"แนะนำให้ใช้ค่าเริ่มต้นที่สมเหตุสมผลด้านบนเพื่อเป็นทางเลือกและเมื่อมีการเปลี่ยนเครื่องมือ\n" +"เมื่อเปิดใช้งาน ค่าล่วงหน้าของแรงดันด้านบนจะถูกแทนที่ อย่างไรก็ตาม แนะนำให้ใช้ค่าเริ่มต้นที่สมเหตุสมผลด้านบนเพื่อเป็นทางเลือกและเมื่อมีการเปลี่ยนเครื่องมือ\n" msgid "Adaptive pressure advance measurements (beta)" msgstr "การวัดล่วงหน้าด้วยแรงดันแบบปรับได้ (เบต้า)" #, no-c-format, no-boost-format msgid "" -"Add sets of pressure advance (PA) values, the volumetric flow speeds and " -"accelerations they were measured at, separated by a comma. One set of values " -"per line. For example\n" +"Add sets of pressure advance (PA) values, the volumetric flow speeds and accelerations they were measured at, separated by a comma. One set of values per line. For example\n" "0.04,3.96,3000\n" "0.033,3.96,10000\n" "0.029,7.91,3000\n" "0.026,7.91,10000\n" "\n" "How to calibrate:\n" -"1. Run the pressure advance test for at least 3 speeds per acceleration " -"value. It is recommended that the test is run for at least the speed of the " -"external perimeters, the speed of the internal perimeters and the fastest " -"feature print speed in your profile (usually its the sparse or solid " -"infill). Then run them for the same speeds for the slowest and fastest print " -"accelerations, and no faster than the recommended maximum acceleration as " -"given by the Klipper input shaper\n" -"2. Take note of the optimal PA value for each volumetric flow speed and " -"acceleration. You can find the flow number by selecting flow from the color " -"scheme drop down and move the horizontal slider over the PA pattern lines. " -"The number should be visible at the bottom of the page. The ideal PA value " -"should be decreasing the higher the volumetric flow is. If it is not, " -"confirm that your extruder is functioning correctly. The slower and with " -"less acceleration you print, the larger the range of acceptable PA values. " -"If no difference is visible, use the PA value from the faster test\n" -"3. Enter the triplets of PA values, Flow and Accelerations in the text box " -"here and save your filament profile." +"1. Run the pressure advance test for at least 3 speeds per acceleration value. It is recommended that the test is run for at least the speed of the external perimeters, the speed of the internal perimeters and the fastest feature print speed in your profile (usually its the sparse or solid infill). Then run them for the same speeds for the slowest and fastest print accelerations, and no faster than the recommended maximum acceleration as given by the Klipper input shaper\n" +"2. Take note of the optimal PA value for each volumetric flow speed and acceleration. You can find the flow number by selecting flow from the color scheme drop down and move the horizontal slider over the PA pattern lines. The number should be visible at the bottom of the page. The ideal PA value should be decreasing the higher the volumetric flow is. If it is not, confirm that your extruder is functioning correctly. The slower and with less acceleration you print, the larger the range of acceptable PA values. If no difference is visible, use the PA value from the faster test\n" +"3. Enter the triplets of PA values, Flow and Accelerations in the text box here and save your filament profile." msgstr "" -"เพิ่มชุดของค่าความดันล่วงหน้า (PA) ความเร็วการไหลเชิงปริมาตร " -"และความเร่งที่วัดได้โดยคั่นด้วยเครื่องหมายจุลภาค หนึ่งชุดของค่าต่อบรรทัด ตัวอย่างเช่น\n" +"เพิ่มชุดของค่าความดันล่วงหน้า (PA) ความเร็วการไหลเชิงปริมาตร และความเร่งที่วัดได้โดยคั่นด้วยเครื่องหมายจุลภาค หนึ่งชุดของค่าต่อบรรทัด ตัวอย่างเช่น\n" "0.04,3.96,3000\n" "0.033,3.96,10000\n" "0.029,7.91,3000\n" "0.026,7.91,10000\n" "\n" "วิธีปรับเทียบ:\n" -"1. รันการทดสอบPressure Advanceอย่างน้อย 3 ความเร็วต่อค่าความเร่ง " -"ขอแนะนำให้ทำการทดสอบความเร็วของเส้นรอบวงภายนอก ความเร็วของเส้นรอบวงภายใน " -"และความเร็วการพิมพ์คุณลักษณะที่เร็วที่สุดในโปรไฟล์ของคุณเป็นอย่างน้อย " -"(โดยปกติจะเป็นการกรอกข้อมูลแบบเบาบางหรือทึบ) " -"จากนั้นรันด้วยความเร็วเท่ากันเพื่อการเร่งความเร็วการพิมพ์ที่ช้าที่สุดและเร็วที่สุด " -"และไม่เร็วกว่าการเร่งความเร็วสูงสุดที่แนะนำตามที่กำหนดโดย Klipper input Shaper\n" -"2. จดบันทึกค่า PA ที่เหมาะสมที่สุดสำหรับความเร็วการไหลและความเร่งตามปริมาตรแต่ละรายการ " -"คุณสามารถค้นหาหมายเลขโฟลว์ได้โดยเลือกโฟลว์จากรายการสีแบบเลื่อนลง " -"และเลื่อนแถบเลื่อนแนวนอนไปเหนือเส้นรูปแบบ PA หมายเลขควรปรากฏที่ด้านล่างของหน้า ค่า PA " -"ในอุดมคติควรลดลงตามอัตราการไหลตามปริมาตรที่สูงขึ้น หากไม่เป็นเช่นนั้น " -"ให้ยืนยันว่าชุดดันเส้นของคุณทำงานอย่างถูกต้อง ยิ่งคุณพิมพ์ช้าลงและเร่งความเร็วน้อยลง ช่วงของค่า PA " -"ที่ยอมรับได้ก็จะยิ่งมากขึ้นเท่านั้น หากไม่เห็นความแตกต่าง ให้ใช้ค่า PA จากการทดสอบที่เร็วกว่า\n" -"3. ป้อนค่า PA, การไหล และความเร่งสามเท่าในกล่องข้อความที่นี่ " -"และบันทึกโปรไฟล์เส้นพลาสติกของคุณ" +"1. รันการทดสอบPressure Advanceอย่างน้อย 3 ความเร็วต่อค่าความเร่ง ขอแนะนำให้ทำการทดสอบความเร็วของเส้นรอบวงภายนอก ความเร็วของเส้นรอบวงภายใน และความเร็วการพิมพ์คุณลักษณะที่เร็วที่สุดในโปรไฟล์ของคุณเป็นอย่างน้อย (โดยปกติจะเป็นการกรอกข้อมูลแบบเบาบางหรือทึบ) จากนั้นรันด้วยความเร็วเท่ากันเพื่อการเร่งความเร็วการพิมพ์ที่ช้าที่สุดและเร็วที่สุด และไม่เร็วกว่าการเร่งความเร็วสูงสุดที่แนะนำตามที่กำหนดโดย Klipper input Shaper\n" +"2. จดบันทึกค่า PA ที่เหมาะสมที่สุดสำหรับความเร็วการไหลและความเร่งตามปริมาตรแต่ละรายการ คุณสามารถค้นหาหมายเลขโฟลว์ได้โดยเลือกโฟลว์จากรายการสีแบบเลื่อนลง และเลื่อนแถบเลื่อนแนวนอนไปเหนือเส้นรูปแบบ PA หมายเลขควรปรากฏที่ด้านล่างของหน้า ค่า PA ในอุดมคติควรลดลงตามอัตราการไหลตามปริมาตรที่สูงขึ้น หากไม่เป็นเช่นนั้น ให้ยืนยันว่าชุดดันเส้นของคุณทำงานอย่างถูกต้อง ยิ่งคุณพิมพ์ช้าลงและเร่งความเร็วน้อยลง ช่วงของค่า PA ที่ยอมรับได้ก็จะยิ่งมากขึ้นเท่านั้น หากไม่เห็นความแตกต่าง ให้ใช้ค่า PA จากการทดสอบที่เร็วกว่า\n" +"3. ป้อนค่า PA, การไหล และความเร่งสามเท่าในกล่องข้อความที่นี่ และบันทึกโปรไฟล์เส้นพลาสติกของคุณ" msgid "Enable adaptive pressure advance for overhangs (beta)" msgstr "เปิดใช้งานการปรับPressure Advanceสำหรับระยะยื่น (เบต้า)" msgid "" -"Enable adaptive PA for overhangs as well as when flow changes within the " -"same feature. This is an experimental option, as if the PA profile is not " -"set accurately, it will cause uniformity issues on the external surfaces " -"before and after overhangs.\n" -"Not compatible with Prusa printers as they pause to process PA changes, " -"which causes delays and defects." +"Enable adaptive PA for overhangs as well as when flow changes within the same feature. This is an experimental option, as if the PA profile is not set accurately, it will cause uniformity issues on the external surfaces before and after overhangs.\n" +"Not compatible with Prusa printers as they pause to process PA changes, which causes delays and defects." msgstr "" -"เปิดใช้งาน PA แบบปรับตัวสำหรับส่วนยื่นและเมื่ออัตราการไหลเปลี่ยนภายในฟีเจอร์เดียวกัน " -"เป็นตัวเลือกทดลอง หากโปรไฟล์ PA ไม่แม่นยำ จะทำให้ผิวด้านนอกไม่สม่ำเสมอก่อนและหลังส่วนยื่น\n" -"ไม่รองรับเครื่องพิมพ์ Prusa เพราะจะหยุดชั่วคราวเพื่อประมวลผลการเปลี่ยน PA " -"ทำให้เกิดความล่าช้าและข้อบกพร่อง" +"เปิดใช้งาน PA แบบปรับตัวสำหรับส่วนยื่นและเมื่ออัตราการไหลเปลี่ยนภายในฟีเจอร์เดียวกัน เป็นตัวเลือกทดลอง หากโปรไฟล์ PA ไม่แม่นยำ จะทำให้ผิวด้านนอกไม่สม่ำเสมอก่อนและหลังส่วนยื่น\n" +"ไม่รองรับเครื่องพิมพ์ Prusa เพราะจะหยุดชั่วคราวเพื่อประมวลผลการเปลี่ยน PA ทำให้เกิดความล่าช้าและข้อบกพร่อง" msgid "Pressure advance for bridges" msgstr "แรงดันล่วงหน้า (Pressure Advance)สำหรับสะพาน" @@ -14148,66 +12310,40 @@ msgstr "แรงดันล่วงหน้า (Pressure Advance)สำห msgid "" "Pressure advance value for bridges. Set to 0 to disable.\n" "\n" -"A lower PA value when printing bridges helps reduce the appearance of slight " -"under extrusion immediately after bridges. This is caused by the pressure " -"drop in the nozzle when printing in the air and a lower PA helps counteract " -"this." +"A lower PA value when printing bridges helps reduce the appearance of slight under extrusion immediately after bridges. This is caused by the pressure drop in the nozzle when printing in the air and a lower PA helps counteract this." msgstr "" "ค่าPressure Advanceสำหรับสะพาน ตั้งค่าเป็น 0 เพื่อปิดใช้งาน\n" "\n" -"ค่า PA ที่ต่ำลงเมื่อพิมพ์บริดจ์จะช่วยลดลักษณะที่ปรากฏเล็กน้อยจากการอัดขึ้นรูปทันทีหลังจากบริดจ์ " -"สาเหตุนี้เกิดจากแรงดันตกในหัวฉีดเมื่อพิมพ์ในอากาศ และค่า PA ที่ต่ำกว่าจะช่วยแก้ปัญหานี้ได้" +"ค่า PA ที่ต่ำลงเมื่อพิมพ์บริดจ์จะช่วยลดลักษณะที่ปรากฏเล็กน้อยจากการอัดขึ้นรูปทันทีหลังจากบริดจ์ สาเหตุนี้เกิดจากแรงดันตกในหัวฉีดเมื่อพิมพ์ในอากาศ และค่า PA ที่ต่ำกว่าจะช่วยแก้ปัญหานี้ได้" -msgid "" -"Default line width if other line widths are set to 0. If expressed as a %, " -"it will be computed over the nozzle diameter." -msgstr "" -"ความกว้างของเส้นเริ่มต้นหากความกว้างของเส้นอื่นตั้งค่าเป็น 0 หากแสดงเป็น % " -"ระบบจะคำนวณตามเส้นผ่านศูนย์กลางของหัวฉีด" +msgid "Default line width if other line widths are set to 0. If expressed as a %, it will be computed over the nozzle diameter." +msgstr "ความกว้างของเส้นเริ่มต้นหากความกว้างของเส้นอื่นตั้งค่าเป็น 0 หากแสดงเป็น % ระบบจะคำนวณตามเส้นผ่านศูนย์กลางของหัวฉีด" msgid "Keep fan always on" msgstr "เปิดพัดลมไว้ตลอดเวลา" -msgid "" -"Enabling this setting means that part cooling fan will never stop entirely " -"and will instead run at least at minimum speed to reduce the frequency of " -"starting and stopping." -msgstr "" -"การเปิดใช้งานการตั้งค่านี้หมายความว่าพัดลมระบายความร้อนของชิ้นส่วนจะไม่หยุดทำงานโดยสิ้นเชิง " -"และจะทำงานที่ความเร็วขั้นต่ำเป็นอย่างน้อยเพื่อลดความถี่ในการสตาร์ทและหยุด" +msgid "Enabling this setting means that part cooling fan will never stop entirely and will instead run at least at minimum speed to reduce the frequency of starting and stopping." +msgstr "การเปิดใช้งานการตั้งค่านี้หมายความว่าพัดลมระบายความร้อนของชิ้นส่วนจะไม่หยุดทำงานโดยสิ้นเชิง และจะทำงานที่ความเร็วขั้นต่ำเป็นอย่างน้อยเพื่อลดความถี่ในการสตาร์ทและหยุด" msgid "Don't slow down outer walls" msgstr "อย่าทำให้ผนังด้านนอกช้าลง" msgid "" -"If enabled, this setting will ensure external perimeters are not slowed down " -"to meet the minimum layer time. This is particularly helpful in the below " -"scenarios:\n" +"If enabled, this setting will ensure external perimeters are not slowed down to meet the minimum layer time. This is particularly helpful in the below scenarios:\n" "1. To avoid changes in shine when printing glossy filaments\n" -"2. To avoid changes in external wall speed which may create slight wall " -"artifacts that appear like Z banding\n" -"3. To avoid printing at speeds which cause VFAs (fine artifacts) on the " -"external walls" +"2. To avoid changes in external wall speed which may create slight wall artifacts that appear like Z banding\n" +"3. To avoid printing at speeds which cause VFAs (fine artifacts) on the external walls" msgstr "" -"หากเปิดใช้งาน " -"การตั้งค่านี้จะช่วยให้แน่ใจว่าขอบเขตภายนอกจะไม่ช้าลงเพื่อให้ตรงตามเวลาขั้นต่ำของเลเยอร์ " -"สิ่งนี้มีประโยชน์อย่างยิ่งในสถานการณ์ด้านล่าง:\n" +"หากเปิดใช้งาน การตั้งค่านี้จะช่วยให้แน่ใจว่าขอบเขตภายนอกจะไม่ช้าลงเพื่อให้ตรงตามเวลาขั้นต่ำของเลเยอร์ สิ่งนี้มีประโยชน์อย่างยิ่งในสถานการณ์ด้านล่าง:\n" "1. เพื่อหลีกเลี่ยงการเปลี่ยนแปลงความมันเงาเมื่อพิมพ์เส้นพลาสติกมัน\n" -"2. " -"เพื่อหลีกเลี่ยงการเปลี่ยนแปลงความเร็วของผนังภายนอกซึ่งอาจสร้างสิ่งรบกวนผนังเล็กน้อยที่ดูเหมือนแถบ " -"Z\n" +"2. เพื่อหลีกเลี่ยงการเปลี่ยนแปลงความเร็วของผนังภายนอกซึ่งอาจสร้างสิ่งรบกวนผนังเล็กน้อยที่ดูเหมือนแถบ Z\n" "3. เพื่อหลีกเลี่ยงการพิมพ์ด้วยความเร็วซึ่งทำให้เกิด VFA (สิ่งประดิษฐ์ที่ดี) บนผนังภายนอก" msgid "Layer time" msgstr "เวลาเลเยอร์" -msgid "" -"The part cooling fan will be enabled for layers where the estimated time is " -"shorter than this value. Fan speed is interpolated between the minimum and " -"maximum fan speeds according to layer printing time." -msgstr "" -"พัดลมระบายความร้อนบางส่วนจะถูกเปิดใช้งานสำหรับเลเยอร์ที่เวลาโดยประมาณสั้นกว่าค่านี้ " -"ความเร็วพัดลมจะถูกประมาณค่าระหว่างความเร็วพัดลมขั้นต่ำและสูงสุดตามเวลาการพิมพ์เลเยอร์" +msgid "The part cooling fan will be enabled for layers where the estimated time is shorter than this value. Fan speed is interpolated between the minimum and maximum fan speeds according to layer printing time." +msgstr "พัดลมระบายความร้อนบางส่วนจะถูกเปิดใช้งานสำหรับเลเยอร์ที่เวลาโดยประมาณสั้นกว่าค่านี้ ความเร็วพัดลมจะถูกประมาณค่าระหว่างความเร็วพัดลมขั้นต่ำและสูงสุดตามเวลาการพิมพ์เลเยอร์" msgid "s" msgstr "วินาที" @@ -14231,11 +12367,8 @@ msgstr "คุณสามารถใส่บันทึกของคุณ msgid "Required nozzle HRC" msgstr "หัวฉีด HRC ที่จำเป็น" -msgid "" -"Minimum HRC of nozzle required to print the filament. A value of 0 means no " -"checking of the nozzle's HRC." -msgstr "" -"HRC ขั้นต่ำของหัวฉีดที่จำเป็นสำหรับการพิมพ์เส้นพลาสติก ศูนย์หมายถึงไม่มีการตรวจสอบ HRC ของหัวฉีด" +msgid "Minimum HRC of nozzle required to print the filament. A value of 0 means no checking of the nozzle's HRC." +msgstr "HRC ขั้นต่ำของหัวฉีดที่จำเป็นสำหรับการพิมพ์เส้นพลาสติก ศูนย์หมายถึงไม่มีการตรวจสอบ HRC ของหัวฉีด" msgid "Filament map to extruder" msgstr "แผนที่เส้นพลาสติกไปยังชุดดันเส้น" @@ -14264,74 +12397,41 @@ msgstr "เครื่องพิมพ์มีฮาร์ดแวร์ต msgid "Flush temperature" msgstr "อุณหภูมิไล่เส้น" -msgid "" -"Temperature when flushing filament. 0 indicates the upper bound of the " -"recommended nozzle temperature range." +msgid "Temperature when flushing filament. 0 indicates the upper bound of the recommended nozzle temperature range." msgstr "อุณหภูมิเมื่อทำการล้างเส้นพลาสติก 0 หมายถึงขอบเขตบนของช่วงอุณหภูมิหัวฉีดที่แนะนำ" msgid "Flush volumetric speed" msgstr "ความเร็วเชิงปริมาตรไล่เส้น" -msgid "" -"Volumetric speed when flushing filament. 0 indicates the max volumetric " -"speed." +msgid "Volumetric speed when flushing filament. 0 indicates the max volumetric speed." msgstr "ความเร็วตามปริมาตรเมื่อทำการล้างเส้นพลาสติก 0 หมายถึงความเร็วปริมาตรสูงสุด" -msgid "" -"This setting is the volume of filament that can be melted and extruded per " -"second. Printing speed is limited by max volumetric speed, in case of too " -"high and unreasonable speed setting. This value cannot be zero." -msgstr "" -"การตั้งค่านี้หมายถึงปริมาณเส้นพลาสติกที่สามารถหลอมและอัดขึ้นรูปต่อวินาทีได้ " -"ความเร็วในการพิมพ์จะถูกจำกัดด้วยความเร็วปริมาตรสูงสุด " -"ในกรณีที่ตั้งค่าความเร็วไว้สูงเกินไปและไม่เหมาะสม ไม่สามารถเป็นศูนย์ได้" +msgid "This setting is the volume of filament that can be melted and extruded per second. Printing speed is limited by max volumetric speed, in case of too high and unreasonable speed setting. This value cannot be zero." +msgstr "การตั้งค่านี้หมายถึงปริมาณเส้นพลาสติกที่สามารถหลอมและอัดขึ้นรูปต่อวินาทีได้ ความเร็วในการพิมพ์จะถูกจำกัดด้วยความเร็วปริมาตรสูงสุด ในกรณีที่ตั้งค่าความเร็วไว้สูงเกินไปและไม่เหมาะสม ไม่สามารถเป็นศูนย์ได้" msgid "Filament load time" msgstr "เวลาในการโหลดเส้นพลาสติก" -msgid "" -"Time to load new filament when switch filament. It's usually applicable for " -"single-extruder multi-material machines. For tool changers or multi-tool " -"machines, it's typically 0. For statistics only." -msgstr "" -"เวลาในการโหลดเส้นพลาสติกใหม่เมื่อเปลี่ยนเส้นพลาสติก " -"โดยปกติแล้วจะใช้ได้กับเครื่องจักรที่ใช้วัสดุหลายชุดดันเส้นเดี่ยว " -"สำหรับเครื่องเปลี่ยนเครื่องมือหรือเครื่องจักรหลายเครื่องมือ โดยทั่วไปจะเป็น 0 สำหรับสถิติเท่านั้น" +msgid "Time to load new filament when switch filament. It's usually applicable for single-extruder multi-material machines. For tool changers or multi-tool machines, it's typically 0. For statistics only." +msgstr "เวลาในการโหลดเส้นพลาสติกใหม่เมื่อเปลี่ยนเส้นพลาสติก โดยปกติแล้วจะใช้ได้กับเครื่องจักรที่ใช้วัสดุหลายชุดดันเส้นเดี่ยว สำหรับเครื่องเปลี่ยนเครื่องมือหรือเครื่องจักรหลายเครื่องมือ โดยทั่วไปจะเป็น 0 สำหรับสถิติเท่านั้น" msgid "Filament unload time" msgstr "เวลาขนถ่ายเส้นพลาสติก" -msgid "" -"Time to unload old filament when switch filament. It's usually applicable " -"for single-extruder multi-material machines. For tool changers or multi-tool " -"machines, it's typically 0. For statistics only." -msgstr "" -"ถึงเวลาขนเส้นพลาสติกเก่าออกเมื่อเปลี่ยนเส้นพลาสติก " -"โดยปกติแล้วจะใช้ได้กับเครื่องจักรที่ใช้วัสดุหลายชุดดันเส้นเดี่ยว " -"สำหรับเครื่องเปลี่ยนเครื่องมือหรือเครื่องจักรหลายเครื่องมือ โดยทั่วไปจะเป็น 0 สำหรับสถิติเท่านั้น" +msgid "Time to unload old filament when switch filament. It's usually applicable for single-extruder multi-material machines. For tool changers or multi-tool machines, it's typically 0. For statistics only." +msgstr "ถึงเวลาขนเส้นพลาสติกเก่าออกเมื่อเปลี่ยนเส้นพลาสติก โดยปกติแล้วจะใช้ได้กับเครื่องจักรที่ใช้วัสดุหลายชุดดันเส้นเดี่ยว สำหรับเครื่องเปลี่ยนเครื่องมือหรือเครื่องจักรหลายเครื่องมือ โดยทั่วไปจะเป็น 0 สำหรับสถิติเท่านั้น" msgid "Tool change time" msgstr "เวลาเปลี่ยนเครื่องมือ" -msgid "" -"Time taken to switch tools. It's usually applicable for tool changers or " -"multi-tool machines. For single-extruder multi-material machines, it's " -"typically 0. For statistics only." -msgstr "" -"เวลาที่ใช้ในการเปลี่ยนเครื่องมือ " -"โดยปกติแล้วจะใช้ได้กับเครื่องเปลี่ยนเครื่องมือหรือเครื่องจักรที่มีเครื่องมือหลายเครื่องมือ " -"สำหรับเครื่องจักรที่ใช้วัสดุหลายชนิดโดยใช้ชุดดันเส้นเดี่ยว โดยทั่วไปจะเป็น 0 สำหรับสถิติเท่านั้น" +msgid "Time taken to switch tools. It's usually applicable for tool changers or multi-tool machines. For single-extruder multi-material machines, it's typically 0. For statistics only." +msgstr "เวลาที่ใช้ในการเปลี่ยนเครื่องมือ โดยปกติแล้วจะใช้ได้กับเครื่องเปลี่ยนเครื่องมือหรือเครื่องจักรที่มีเครื่องมือหลายเครื่องมือ สำหรับเครื่องจักรที่ใช้วัสดุหลายชนิดโดยใช้ชุดดันเส้นเดี่ยว โดยทั่วไปจะเป็น 0 สำหรับสถิติเท่านั้น" msgid "Bed temperature type" msgstr "ประเภทอุณหภูมิฐานพิมพ์" -msgid "" -"This option determines how the bed temperature is set during slicing: based " -"on the temperature of the first filament or the highest temperature of the " -"printed filaments." -msgstr "" -"ตัวเลือกนี้จะกำหนดวิธีการตั้งค่าอุณหภูมิฐานในระหว่างการสไลซ์: " -"ขึ้นอยู่กับอุณหภูมิของเส้นพลาสติกเส้นแรกหรืออุณหภูมิสูงสุดของเส้นพลาสติกที่พิมพ์" +msgid "This option determines how the bed temperature is set during slicing: based on the temperature of the first filament or the highest temperature of the printed filaments." +msgstr "ตัวเลือกนี้จะกำหนดวิธีการตั้งค่าอุณหภูมิฐานในระหว่างการสไลซ์: ขึ้นอยู่กับอุณหภูมิของเส้นพลาสติกเส้นแรกหรืออุณหภูมิสูงสุดของเส้นพลาสติกที่พิมพ์" msgid "By First filament" msgstr "โดย เฟิร์ส ฟิลาเมนท์" @@ -14339,27 +12439,20 @@ msgstr "โดย เฟิร์ส ฟิลาเมนท์" msgid "By Highest Temp" msgstr "โดยอุณหภูมิสูงสุด" -msgid "" -"Filament diameter is used to calculate extrusion variables in G-code, so it " -"is important that this is accurate and precise." -msgstr "" -"เส้นผ่านศูนย์กลางของฟิลาเมนต์ใช้ในการคำนวณการอัดขึ้นรูปใน G-code " -"ดังนั้นจึงมีความสำคัญและควรมีความแม่นยำ" +msgid "Filament diameter is used to calculate extrusion variables in G-code, so it is important that this is accurate and precise." +msgstr "เส้นผ่านศูนย์กลางของฟิลาเมนต์ใช้ในการคำนวณการอัดขึ้นรูปใน G-code ดังนั้นจึงมีความสำคัญและควรมีความแม่นยำ" msgid "Pellet flow coefficient" msgstr "ค่าสัมประสิทธิ์การไหลของเม็ด" msgid "" -"Pellet flow coefficient is empirically derived and allows for volume " -"calculation for pellet printers.\n" +"Pellet flow coefficient is empirically derived and allows for volume calculation for pellet printers.\n" "\n" -"Internally it is converted to filament_diameter. All other volume " -"calculations remain the same.\n" +"Internally it is converted to filament_diameter. All other volume calculations remain the same.\n" "\n" "filament_diameter = sqrt( (4 * pellet_flow_coefficient) / PI )" msgstr "" -"ค่าสัมประสิทธิ์การไหลของเม็ดได้มาจากการทดลอง " -"และช่วยให้สามารถคำนวณปริมาตรสำหรับเครื่องพิมพ์เม็ดพลาสติกได้\n" +"ค่าสัมประสิทธิ์การไหลของเม็ดได้มาจากการทดลอง และช่วยให้สามารถคำนวณปริมาตรสำหรับเครื่องพิมพ์เม็ดพลาสติกได้\n" "\n" "ภายในจะถูกแปลงเป็น fil_diameter การคำนวณปริมาณอื่นๆ ทั้งหมดยังคงเหมือนเดิม\n" "\n" @@ -14369,19 +12462,13 @@ msgid "Adaptive volumetric speed" msgstr "ความเร็วตามปริมาตรแบบปรับได้" msgid "" -"When enabled, the extrusion flow is limited by the smaller of the fitted " -"value (calculated from line width and layer height) and the user-defined " -"maximum flow. When disabled, only the user-defined maximum flow is applied.\n" +"When enabled, the extrusion flow is limited by the smaller of the fitted value (calculated from line width and layer height) and the user-defined maximum flow. When disabled, only the user-defined maximum flow is applied.\n" "\n" -"Note: Experimental and incomplete feature imported from BBS. Functional for " -"some profiles that already have the variable saved." +"Note: Experimental and incomplete feature imported from BBS. Functional for some profiles that already have the variable saved." msgstr "" -"เมื่อเปิดใช้งาน การไหลของการอัดขึ้นรูปจะถูกจำกัดด้วยค่าที่น้อยกว่าของค่าที่ติดตั้ง " -"(คำนวณจากความกว้างของเส้นและความสูงของชั้น) และการไหลสูงสุดที่ผู้ใช้กำหนด เมื่อปิดใช้งาน " -"จะมีการใช้เฉพาะโฟลว์สูงสุดที่ผู้ใช้กำหนดเท่านั้น\n" +"เมื่อเปิดใช้งาน การไหลของการอัดขึ้นรูปจะถูกจำกัดด้วยค่าที่น้อยกว่าของค่าที่ติดตั้ง (คำนวณจากความกว้างของเส้นและความสูงของชั้น) และการไหลสูงสุดที่ผู้ใช้กำหนด เมื่อปิดใช้งาน จะมีการใช้เฉพาะโฟลว์สูงสุดที่ผู้ใช้กำหนดเท่านั้น\n" "\n" -"หมายเหตุ: คุณลักษณะทดลองและไม่สมบูรณ์นำเข้าจาก BBS " -"ใช้งานได้กับบางโปรไฟล์ที่ได้บันทึกตัวแปรไว้แล้ว" +"หมายเหตุ: คุณลักษณะทดลองและไม่สมบูรณ์นำเข้าจาก BBS ใช้งานได้กับบางโปรไฟล์ที่ได้บันทึกตัวแปรไว้แล้ว" msgid "Max volumetric speed multinomial coefficients" msgstr "ค่าสัมประสิทธิ์พหุนามความเร็วปริมาตรสูงสุด" @@ -14391,30 +12478,18 @@ msgstr "การหดตัว (XY)" #, no-c-format, no-boost-format msgid "" -"Enter the shrinkage percentage that the filament will get after cooling (94% " -"if you measure 94mm instead of 100mm). The part will be scaled in XY to " -"compensate. For multi-material prints, ensure filament shrinkage matches " -"across all used filaments\n" -"Be sure to allow enough space between objects, as this compensation is done " -"after the checks." +"Enter the shrinkage percentage that the filament will get after cooling (94% if you measure 94mm instead of 100mm). The part will be scaled in XY to compensate. For multi-material prints, ensure filament shrinkage matches across all used filaments\n" +"Be sure to allow enough space between objects, as this compensation is done after the checks." msgstr "" -"ป้อนเปอร์เซ็นต์การหดตัวที่เส้นพลาสติกจะได้รับหลังจากการทำความเย็น (94% หากคุณวัด 94 มม. " -"แทนที่จะเป็น 100 มม.) ชิ้นส่วนจะถูกปรับขนาดเป็น XY เพื่อชดเชย สำหรับการพิมพ์แบบหลายวัสดุ " -"ตรวจสอบให้แน่ใจว่าการหดตัวของเส้นพลาสติกตรงกันกับเส้นพลาสติกที่ใช้ทั้งหมด\n" -"ตรวจสอบให้แน่ใจว่าได้เว้นระยะห่างระหว่างวัตถุเพียงพอ " -"เนื่องจากการชดเชยนี้เสร็จสิ้นหลังจากการตรวจสอบ" +"ป้อนเปอร์เซ็นต์การหดตัวที่เส้นพลาสติกจะได้รับหลังจากการทำความเย็น (94% หากคุณวัด 94 มม. แทนที่จะเป็น 100 มม.) ชิ้นส่วนจะถูกปรับขนาดเป็น XY เพื่อชดเชย สำหรับการพิมพ์แบบหลายวัสดุ ตรวจสอบให้แน่ใจว่าการหดตัวของเส้นพลาสติกตรงกันกับเส้นพลาสติกที่ใช้ทั้งหมด\n" +"ตรวจสอบให้แน่ใจว่าได้เว้นระยะห่างระหว่างวัตถุเพียงพอ เนื่องจากการชดเชยนี้เสร็จสิ้นหลังจากการตรวจสอบ" msgid "Shrinkage (Z)" msgstr "การหดตัว (Z)" #, no-c-format, no-boost-format -msgid "" -"Enter the shrinkage percentage that the filament will get after cooling (94% " -"if you measure 94mm instead of 100mm). The part will be scaled in Z to " -"compensate." -msgstr "" -"ป้อนเปอร์เซ็นต์การหดตัวที่เส้นพลาสติกจะได้รับหลังจากการทำความเย็น (94% หากคุณวัด 94 มม. " -"แทนที่จะเป็น 100 มม.) ชิ้นส่วนจะถูกปรับขนาดเป็น Z เพื่อชดเชย" +msgid "Enter the shrinkage percentage that the filament will get after cooling (94% if you measure 94mm instead of 100mm). The part will be scaled in Z to compensate." +msgstr "ป้อนเปอร์เซ็นต์การหดตัวที่เส้นพลาสติกจะได้รับหลังจากการทำความเย็น (94% หากคุณวัด 94 มม. แทนที่จะเป็น 100 มม.) ชิ้นส่วนจะถูกปรับขนาดเป็น Z เพื่อชดเชย" msgid "Adhesiveness Category" msgstr "หมวดหมู่การยึดเกาะ" @@ -14437,39 +12512,26 @@ msgstr "ความเร็วที่ใช้ในช่วงเริ่ msgid "Unloading speed" msgstr "ความเร็วในการขนถ่าย" -msgid "" -"Speed used for unloading the filament on the wipe tower (does not affect " -"initial part of unloading just after ramming)." -msgstr "" -"ความเร็วที่ใช้ในการขนถ่ายเส้นพลาสติกบนไวด์ทาวเวอร์ " -"(ไม่ส่งผลต่อส่วนเริ่มแรกของการขนถ่ายหลังจากการชน)" +msgid "Speed used for unloading the filament on the wipe tower (does not affect initial part of unloading just after ramming)." +msgstr "ความเร็วที่ใช้ในการขนถ่ายเส้นพลาสติกบนไวด์ทาวเวอร์ (ไม่ส่งผลต่อส่วนเริ่มแรกของการขนถ่ายหลังจากการชน)" msgid "Unloading speed at the start" msgstr "ขนถ่ายความเร็วที่จุดเริ่มต้น" -msgid "" -"Speed used for unloading the tip of the filament immediately after ramming." +msgid "Speed used for unloading the tip of the filament immediately after ramming." msgstr "ความเร็วที่ใช้ในการขนถ่ายปลายเส้นพลาสติกทันทีหลังจากการชน" msgid "Delay after unloading" msgstr "ความล่าช้าหลังจากการขนถ่าย" -msgid "" -"Time to wait after the filament is unloaded. May help to get reliable tool " -"changes with flexible materials that may need more time to shrink to " -"original dimensions." -msgstr "" -"เวลาที่ต้องรอหลังจากขนเส้นพลาสติกออก " -"อาจช่วยให้มีการเปลี่ยนเครื่องมือที่เชื่อถือได้ด้วยวัสดุที่ยืดหยุ่นซึ่งอาจต้องใช้เวลามากขึ้นในการย่อขนาดให้เป็นขนาดดั้งเดิม" +msgid "Time to wait after the filament is unloaded. May help to get reliable tool changes with flexible materials that may need more time to shrink to original dimensions." +msgstr "เวลาที่ต้องรอหลังจากขนเส้นพลาสติกออก อาจช่วยให้มีการเปลี่ยนเครื่องมือที่เชื่อถือได้ด้วยวัสดุที่ยืดหยุ่นซึ่งอาจต้องใช้เวลามากขึ้นในการย่อขนาดให้เป็นขนาดดั้งเดิม" msgid "Number of cooling moves" msgstr "จำนวนการเคลื่อนย้ายความเย็น" -msgid "" -"Filament is cooled by being moved back and forth in the cooling tubes. " -"Specify desired number of these moves." -msgstr "" -"เส้นพลาสติกถูกทำให้เย็นลงโดยการเคลื่อนไปมาในท่อทำความเย็น ระบุจำนวนการเคลื่อนไหวที่ต้องการ" +msgid "Filament is cooled by being moved back and forth in the cooling tubes. Specify desired number of these moves." +msgstr "เส้นพลาสติกถูกทำให้เย็นลงโดยการเคลื่อนไปมาในท่อทำความเย็น ระบุจำนวนการเคลื่อนไหวที่ต้องการ" msgid "Stamping loading speed" msgstr "ความเร็วในการโหลดการประทับตรา" @@ -14480,14 +12542,8 @@ msgstr "ความเร็วที่ใช้ในการประทั msgid "Stamping distance measured from the center of the cooling tube" msgstr "ระยะการตอกวัดจากศูนย์กลางของท่อทำความเย็น" -msgid "" -"If set to non-zero value, filament is moved toward the nozzle between the " -"individual cooling moves (\"stamping\"). This option configures how long " -"this movement should be before the filament is retracted again." -msgstr "" -"หากตั้งค่าเป็นค่าที่ไม่ใช่ศูนย์ เส้นพลาสติกจะถูกย้ายไปยังหัวฉีดระหว่างการระบายความร้อนแต่ละครั้ง " -"(\"การประทับตรา\") " -"ตัวเลือกนี้จะกำหนดระยะเวลาการเคลื่อนไหวนี้ก่อนที่เส้นพลาสติกจะถูกดึงกลับอีกครั้ง" +msgid "If set to non-zero value, filament is moved toward the nozzle between the individual cooling moves (\"stamping\"). This option configures how long this movement should be before the filament is retracted again." +msgstr "หากตั้งค่าเป็นค่าที่ไม่ใช่ศูนย์ เส้นพลาสติกจะถูกย้ายไปยังหัวฉีดระหว่างการระบายความร้อนแต่ละครั้ง (\"การประทับตรา\") ตัวเลือกนี้จะกำหนดระยะเวลาการเคลื่อนไหวนี้ก่อนที่เส้นพลาสติกจะถูกดึงกลับอีกครั้ง" msgid "Speed of the first cooling move" msgstr "ความเร็วของการทำความเย็นครั้งแรก" @@ -14498,17 +12554,8 @@ msgstr "การเคลื่อนไหวที่เย็นลงจะ msgid "Minimal purge on wipe tower" msgstr "การล้างข้อมูลบน Wipe Tower น้อยที่สุด" -msgid "" -"After a tool change, the exact position of the newly loaded filament inside " -"the nozzle may not be known, and the filament pressure is likely not yet " -"stable. Before purging the print head into an infill or a sacrificial " -"object, Orca Slicer will always prime this amount of material into the wipe " -"tower to produce successive infill or sacrificial object extrusions reliably." -msgstr "" -"หลังจากเปลี่ยนเครื่องมือ อาจไม่ทราบตำแหน่งที่แน่นอนของเส้นพลาสติกที่เพิ่งโหลดใหม่ภายในหัวฉีด " -"และความดันเส้นพลาสติกก็มีแนวโน้มว่ายังไม่เสถียร ก่อนที่จะล้างหัวพิมพ์ลงในวัสดุไส้ในหรือวัตถุบูชายัญ " -"Orca Slicer " -"จะเตรียมวัสดุจำนวนนี้ลงในหอเช็ดเสมอเพื่อสร้างการอัดขึ้นรูปวัตถุแบบไส้ในหรือบูชายัญต่อเนื่องกันอย่างน่าเชื่อถือ" +msgid "After a tool change, the exact position of the newly loaded filament inside the nozzle may not be known, and the filament pressure is likely not yet stable. Before purging the print head into an infill or a sacrificial object, Orca Slicer will always prime this amount of material into the wipe tower to produce successive infill or sacrificial object extrusions reliably." +msgstr "หลังจากเปลี่ยนเครื่องมือ อาจไม่ทราบตำแหน่งที่แน่นอนของเส้นพลาสติกที่เพิ่งโหลดใหม่ภายในหัวฉีด และความดันเส้นพลาสติกก็มีแนวโน้มว่ายังไม่เสถียร ก่อนที่จะล้างหัวพิมพ์ลงในวัสดุไส้ในหรือวัตถุบูชายัญ Orca Slicer จะเตรียมวัสดุจำนวนนี้ลงในหอเช็ดเสมอเพื่อสร้างการอัดขึ้นรูปวัตถุแบบไส้ในหรือบูชายัญต่อเนื่องกันอย่างน่าเชื่อถือ" msgid "Wipe tower cooling" msgstr "เช็ดทาวเวอร์คูลลิ่ง" @@ -14519,27 +12566,19 @@ msgstr "อุณหภูมิลดลงก่อนเข้าหอใย msgid "Interface layer pre-extrusion distance" msgstr "ระยะการอัดรีดชั้นอินเตอร์เฟซ" -msgid "" -"Pre-extrusion distance for prime tower interface layer (where different " -"materials meet)." -msgstr "" -"ระยะก่อนการอัดขึ้นรูปสำหรับชั้นอินเทอร์เฟซของไพรม์ทาวเวอร์ (ที่วัสดุที่แตกต่างกันมาบรรจบกัน)" +msgid "Pre-extrusion distance for prime tower interface layer (where different materials meet)." +msgstr "ระยะก่อนการอัดขึ้นรูปสำหรับชั้นอินเทอร์เฟซของไพรม์ทาวเวอร์ (ที่วัสดุที่แตกต่างกันมาบรรจบกัน)" msgid "Interface layer pre-extrusion length" msgstr "ความยาวชั้นอินเตอร์เฟซก่อนการอัดขึ้นรูป" -msgid "" -"Pre-extrusion length for prime tower interface layer (where different " -"materials meet)." -msgstr "" -"ความยาวก่อนการอัดขึ้นรูปสำหรับชั้นอินเทอร์เฟซของไพรม์ทาวเวอร์ (ที่วัสดุที่แตกต่างกันมาบรรจบกัน)" +msgid "Pre-extrusion length for prime tower interface layer (where different materials meet)." +msgstr "ความยาวก่อนการอัดขึ้นรูปสำหรับชั้นอินเทอร์เฟซของไพรม์ทาวเวอร์ (ที่วัสดุที่แตกต่างกันมาบรรจบกัน)" msgid "Tower ironing area" msgstr "พื้นที่รีดผิวแบบทาวเวอร์" -msgid "" -"Ironing area for prime tower interface layer (where different materials " -"meet)." +msgid "Ironing area for prime tower interface layer (where different materials meet)." msgstr "พื้นที่รีดผิวสำหรับชั้นอินเทอร์เฟซของไพร์มทาวเวอร์ (บริเวณที่วัสดุต่างกันมาบรรจบกัน)" msgid "mm²" @@ -14548,20 +12587,14 @@ msgstr "มม.²" msgid "Interface layer purge length" msgstr "ความยาวการล้างเลเยอร์อินเทอร์เฟซ" -msgid "" -"Purge length for prime tower interface layer (where different materials " -"meet)." +msgid "Purge length for prime tower interface layer (where different materials meet)." msgstr "ความยาวในการไล่ล้างสำหรับชั้นอินเทอร์เฟซของไพรม์ทาวเวอร์ (เมื่อวัสดุต่างกันมาบรรจบกัน)" msgid "Interface layer print temperature" msgstr "อุณหภูมิการพิมพ์เลเยอร์อินเทอร์เฟซ" -msgid "" -"Print temperature for prime tower interface layer (where different materials " -"meet). If set to -1, use max recommended nozzle temperature." -msgstr "" -"อุณหภูมิการพิมพ์สำหรับชั้นอินเทอร์เฟซของไพรม์ทาวเวอร์ (เมื่อวัสดุต่างกันมาบรรจบกัน) หากตั้งค่าเป็น " -"-1 ให้ใช้อุณหภูมิหัวฉีดสูงสุดที่แนะนำ" +msgid "Print temperature for prime tower interface layer (where different materials meet). If set to -1, use max recommended nozzle temperature." +msgstr "อุณหภูมิการพิมพ์สำหรับชั้นอินเทอร์เฟซของไพรม์ทาวเวอร์ (เมื่อวัสดุต่างกันมาบรรจบกัน) หากตั้งค่าเป็น -1 ให้ใช้อุณหภูมิหัวฉีดสูงสุดที่แนะนำ" msgid "Speed of the last cooling move" msgstr "ความเร็วของการทำความเย็นครั้งล่าสุด" @@ -14572,24 +12605,14 @@ msgstr "การเคลื่อนไหวที่เย็นลงจะ msgid "Ramming parameters" msgstr "พารามิเตอร์การกระแทก" -msgid "" -"This string is edited by RammingDialog and contains ramming specific " -"parameters." +msgid "This string is edited by RammingDialog and contains ramming specific parameters." msgstr "สตริงนี้ได้รับการแก้ไขโดย RammingDialog และมีพารามิเตอร์เฉพาะของการชน" msgid "Enable ramming for multi-tool setups" msgstr "เปิดใช้งานการกระแทกสำหรับการตั้งค่าหลายเครื่องมือ" -msgid "" -"Perform ramming when using multi-tool printer (i.e. when the 'Single " -"Extruder Multimaterial' in Printer Settings is unchecked). When checked, a " -"small amount of filament is rapidly extruded on the wipe tower just before " -"the tool change. This option is only used when the wipe tower is enabled." -msgstr "" -"ทำการกระแทกเมื่อใช้เครื่องพิมพ์แบบหลายเครื่องมือ (เช่น เมื่อไม่ได้เลือก 'Single ชุดดันเส้น " -"Multimaterial' ในการตั้งค่าเครื่องพิมพ์) เมื่อตรวจสอบแล้ว " -"เส้นพลาสติกจำนวนเล็กน้อยจะถูกอัดรีดอย่างรวดเร็วบนไวด์ทาวเวอร์ก่อนที่จะเปลี่ยนเครื่องมือ " -"ตัวเลือกนี้ใช้เฉพาะเมื่อเปิดใช้งาน Wipe Tower เท่านั้น" +msgid "Perform ramming when using multi-tool printer (i.e. when the 'Single Extruder Multimaterial' in Printer Settings is unchecked). When checked, a small amount of filament is rapidly extruded on the wipe tower just before the tool change. This option is only used when the wipe tower is enabled." +msgstr "ทำการกระแทกเมื่อใช้เครื่องพิมพ์แบบหลายเครื่องมือ (เช่น เมื่อไม่ได้เลือก 'Single ชุดดันเส้น Multimaterial' ในการตั้งค่าเครื่องพิมพ์) เมื่อตรวจสอบแล้ว เส้นพลาสติกจำนวนเล็กน้อยจะถูกอัดรีดอย่างรวดเร็วบนไวด์ทาวเวอร์ก่อนที่จะเปลี่ยนเครื่องมือ ตัวเลือกนี้ใช้เฉพาะเมื่อเปิดใช้งาน Wipe Tower เท่านั้น" msgid "Multi-tool ramming volume" msgstr "ปริมาณการกระแทกหลายเครื่องมือ" @@ -14618,25 +12641,19 @@ msgstr "ประเภทวัสดุของเส้นพลาสติ msgid "Soluble material" msgstr "วัสดุที่ละลายน้ำได้" -msgid "" -"Soluble material is commonly used to print supports and support interfaces." +msgid "Soluble material is commonly used to print supports and support interfaces." msgstr "วัสดุที่ละลายน้ำได้มักใช้ในการพิมพ์ส่วนรองรับและส่วนต่อประสานรองรับ" msgid "Filament ramming length" msgstr "ความยาวการกระแทกของเส้นพลาสติก" -msgid "" -"When changing the extruder, it is recommended to extrude a certain length of " -"filament from the original extruder. This helps minimize nozzle oozing." -msgstr "" -"เมื่อเปลี่ยนชุดดันเส้น แนะนำให้รีดเส้นพลาสติกที่มีความยาวจำนวนหนึ่งจากชุดดันเส้นเดิม " -"ซึ่งจะช่วยลดการไหลของหัวฉีด" +msgid "When changing the extruder, it is recommended to extrude a certain length of filament from the original extruder. This helps minimize nozzle oozing." +msgstr "เมื่อเปลี่ยนชุดดันเส้น แนะนำให้รีดเส้นพลาสติกที่มีความยาวจำนวนหนึ่งจากชุดดันเส้นเดิม ซึ่งจะช่วยลดการไหลของหัวฉีด" msgid "Support material" msgstr "วัสดุส่วนรองรับ" -msgid "" -"Support material is commonly used to print supports and support interfaces." +msgid "Support material is commonly used to print supports and support interfaces." msgstr "วัสดุรองรับมักใช้ในการพิมพ์ส่วนรองรับและอินเทอร์เฟซรองรับ" msgid "Filament printable" @@ -14648,13 +12665,8 @@ msgstr "เส้นพลาสติกสามารถพิมพ์ได msgid "Softening temperature" msgstr "อุณหภูมิอ่อนลง" -msgid "" -"The material softens at this temperature, so when the bed temperature is " -"equal to or greater than this, it's highly recommended to open the front " -"door and/or remove the upper glass to avoid clogs." -msgstr "" -"วัสดุจะอ่อนตัวลงที่อุณหภูมินี้ ดังนั้นเมื่ออุณหภูมิฐานพิมพ์เท่ากับหรือมากกว่านี้ " -"ขอแนะนำอย่างยิ่งให้เปิดประตูหน้าและ/หรือถอดกระจกด้านบนออกเพื่อหลีกเลี่ยงการอุดตัน" +msgid "The material softens at this temperature, so when the bed temperature is equal to or greater than this, it's highly recommended to open the front door and/or remove the upper glass to avoid clogs." +msgstr "วัสดุจะอ่อนตัวลงที่อุณหภูมินี้ ดังนั้นเมื่ออุณหภูมิฐานพิมพ์เท่ากับหรือมากกว่านี้ ขอแนะนำอย่างยิ่งให้เปิดประตูหน้าและ/หรือถอดกระจกด้านบนออกเพื่อหลีกเลี่ยงการอุดตัน" msgid "Price" msgstr "ราคา" @@ -14677,79 +12689,50 @@ msgstr "(ไม่ระบุ)" msgid "Sparse infill direction" msgstr "ทิศทางไส้ในแบบโปร่ง" -msgid "" -"This is the angle for sparse infill pattern, which controls the start or " -"main direction of lines." +msgid "This is the angle for sparse infill pattern, which controls the start or main direction of lines." msgstr "มุมสำหรับรูปแบบไส้ในแบบกระจาย ซึ่งควบคุมจุดเริ่มต้นหรือทิศทางหลักของเส้น" msgid "Solid infill direction" msgstr "ทิศทางไส้ในแบบทึบ" -msgid "" -"Angle for solid infill pattern, which controls the start or main direction " -"of line." +msgid "Angle for solid infill pattern, which controls the start or main direction of line." msgstr "มุมสำหรับรูปแบบไส้ในแบบทึบ ซึ่งควบคุมจุดเริ่มต้นหรือทิศทางหลักของเส้น" msgid "Sparse infill density" msgstr "ความหนาแน่นไส้ในแบบโปร่ง" #, no-c-format, no-boost-format -msgid "" -"Density of internal sparse infill, 100% turns all sparse infill into solid " -"infill and internal solid infill pattern will be used." -msgstr "" -"ความหนาแน่นของไส้ในแบบโปร่งภายใน 100% จะเปลี่ยนไส้ในแบบโปร่งทั้งหมดให้เป็นไส้ในแบบทึบ " -"และจะใช้รูปแบบไส้ในแบบทึบภายใน" +msgid "Density of internal sparse infill, 100% turns all sparse infill into solid infill and internal solid infill pattern will be used." +msgstr "ความหนาแน่นของไส้ในแบบโปร่งภายใน 100% จะเปลี่ยนไส้ในแบบโปร่งทั้งหมดให้เป็นไส้ในแบบทึบ และจะใช้รูปแบบไส้ในแบบทึบภายใน" msgid "Align infill direction to model" msgstr "จัดทิศทาง ไส้ใน ให้ตรงกับโมเดล" msgid "" -"Aligns infill, bridge, ironing and surface fill directions to follow the " -"model's orientation on the build plate.\n" -"When enabled, directions rotate with the model to maintain optimal strength " -"characteristics." +"Aligns infill, bridge, ironing and surface fill directions to follow the model's orientation on the build plate.\n" +"When enabled, directions rotate with the model to maintain optimal strength characteristics." msgstr "" -"จัดทิศทางไส้ใน สะพาน การรีดเรียบ " -"และการเติมผิวให้สอดคล้องกับการวางแนวของโมเดลบนฐานรองพิมพ์\n" +"จัดทิศทางไส้ใน สะพาน การรีดเรียบ และการเติมผิวให้สอดคล้องกับการวางแนวของโมเดลบนฐานรองพิมพ์\n" "เมื่อเปิดใช้งาน ทิศทางจะหมุนตามโมเดลเพื่อรักษาคุณสมบัติความแข็งแรงที่เหมาะสม" msgid "Insert solid layers" msgstr "แทรกชั้นทึบ" -msgid "" -"Insert solid infill at specific layers. Use N to insert every Nth layer, N#K " -"to insert K consecutive solid layers every N layers (K is optional, e.g. " -"'5#' equals '5#1'), or a comma-separated list (e.g. 1,7,9) to insert at " -"explicit layers. Layers are 1-based." -msgstr "" -"แทรก ไส้ใน แบบทึบที่ชั้นเฉพาะ ใช้ N เพื่อแทรกทุกเลเยอร์ N, N#K เพื่อแทรก K " -"เลเยอร์ทึบต่อเนื่องกันทุกๆ เลเยอร์ N (K เป็นทางเลือก เช่น '5#' เท่ากับ '5#1') " -"หรือรายการที่คั่นด้วยเครื่องหมายจุลภาค (เช่น 1,7,9) เพื่อแทรกในเลเยอร์ที่ชัดเจน เลเยอร์เป็นแบบ " -"1" +msgid "Insert solid infill at specific layers. Use N to insert every Nth layer, N#K to insert K consecutive solid layers every N layers (K is optional, e.g. '5#' equals '5#1'), or a comma-separated list (e.g. 1,7,9) to insert at explicit layers. Layers are 1-based." +msgstr "แทรก ไส้ใน แบบทึบที่ชั้นเฉพาะ ใช้ N เพื่อแทรกทุกเลเยอร์ N, N#K เพื่อแทรก K เลเยอร์ทึบต่อเนื่องกันทุกๆ เลเยอร์ N (K เป็นทางเลือก เช่น '5#' เท่ากับ '5#1') หรือรายการที่คั่นด้วยเครื่องหมายจุลภาค (เช่น 1,7,9) เพื่อแทรกในเลเยอร์ที่ชัดเจน เลเยอร์เป็นแบบ 1" msgid "Fill Multiline" msgstr "ไส้ในหลายบรรทัด" -msgid "" -"Using multiple lines for the infill pattern, if supported by infill pattern." +msgid "Using multiple lines for the infill pattern, if supported by infill pattern." msgstr "การใช้หลายบรรทัดสำหรับรูปแบบ ไส้ใน หากรองรับโดยรูปแบบ ไส้ใน" msgid "Z-buckling bias optimization (experimental)" msgstr "การปรับชดเชยการโก่งตัวตามแกน Z ให้เหมาะสม (ทดลอง)" -#, no-c-format, no-boost-format -msgid "" -"Tightens the gyroid wave along the Z (vertical) axis at low infill density " -"to shorten the effective vertical column length and improve Z-axis " -"compression buckling resistance. Filament use is preserved. No effect at " -"~30% sparse infill density and above. Only applies when Sparse infill " -"pattern is set to Gyroid." -msgstr "" -"ปรับคลื่นไจรอยด์ให้ถี่ขึ้นตามแกน Z (แนวตั้ง) เมื่อความหนาแน่นไส้ในต่ำ " -"เพื่อลดความยาวช่วงเสาแนวตั้งที่มีผลจริง และเพิ่มความต้านทานการโก่งตัวจากแรงอัดตามแกน Z " -"โดยยังใช้เส้นพลาสติกเท่าเดิม ไม่มีผลเมื่อความหนาแน่นไส้ในแบบโปร่งประมาณ ~30% ขึ้นไป " -"ใช้เฉพาะเมื่อรูปแบบไส้ในแบบโปร่งตั้งเป็นไจรอยด์เท่านั้น" +#, fuzzy, c-format, boost-format +msgid "Tightens the gyroid wave along the Z (vertical) axis at low infill density to shorten the effective vertical column length and improve Z-axis compression buckling resistance. Filament use is preserved. No effect at ~30% sparse infill density and above. Only applies when Sparse infill pattern is set to Gyroid." +msgstr "ปรับคลื่นไจรอยด์ให้ถี่ขึ้นตามแกน Z (แนวตั้ง) เมื่อความหนาแน่นไส้ในต่ำ เพื่อลดความยาวช่วงเสาแนวตั้งที่มีผลจริง และเพิ่มความต้านทานการโก่งตัวจากแรงอัดตามแกน Z โดยยังใช้เส้นพลาสติกเท่าเดิม ไม่มีผลเมื่อความหนาแน่นไส้ในแบบโปร่งประมาณ ~30% ขึ้นไป ใช้เฉพาะเมื่อรูปแบบไส้ในแบบโปร่งตั้งเป็นไจรอยด์เท่านั้น" msgid "Sparse infill pattern" msgstr "รูปแบบไส้ในแบบโปร่ง" @@ -14817,24 +12800,19 @@ msgstr "ไจรอยด์" msgid "Lateral lattice angle 1" msgstr "มุมขัดแตะด้านข้าง 1" -msgid "" -"The angle of the first set of Lateral lattice elements in the Z direction. " -"Zero is vertical." +msgid "The angle of the first set of Lateral lattice elements in the Z direction. Zero is vertical." msgstr "มุมขององค์ประกอบขัดแตะด้านข้างชุดแรกในทิศทาง Z ศูนย์คือแนวตั้ง" msgid "Lateral lattice angle 2" msgstr "มุมขัดแตะด้านข้าง 2" -msgid "" -"The angle of the second set of Lateral lattice elements in the Z direction. " -"Zero is vertical." +msgid "The angle of the second set of Lateral lattice elements in the Z direction. Zero is vertical." msgstr "มุมขององค์ประกอบขัดแตะด้านข้างชุดที่สองในทิศทาง Z ศูนย์คือแนวตั้ง" msgid "Infill overhang angle" msgstr "ไส้ในมุมยื่น" -msgid "" -"The angle of the infill angled lines. 60° will result in a pure honeycomb." +msgid "The angle of the infill angled lines. 60° will result in a pure honeycomb." msgstr "มุมของเส้นมุมที่ไส้ในเข้าไป 60° ก็จะได้รังผึ้งที่บริสุทธิ์" msgid "Lightning overhang angle" @@ -14847,8 +12825,7 @@ msgid "Prune angle" msgstr "มุมตัดแต่ง" msgid "" -"Controls how aggressively short or unsupported Lightning branches are " -"pruned.\n" +"Controls how aggressively short or unsupported Lightning branches are pruned.\n" "This angle is converted internally to a per-layer distance." msgstr "" "กำหนดระดับการตัดกิ่ง Lightning ที่สั้นหรือไม่มีส่วนรองรับออก\n" @@ -14864,24 +12841,10 @@ msgid "Sparse infill anchor length" msgstr "ความยาวสมอ ไส้ใน เบาบาง" msgid "" -"Connect an infill line to an internal perimeter with a short segment of an " -"additional perimeter. If expressed as percentage (example: 15%) it is " -"calculated over infill extrusion width. Orca Slicer tries to connect two " -"close infill lines to a short perimeter segment. If no such perimeter " -"segment shorter than infill_anchor_max is found, the infill line is " -"connected to a perimeter segment at just one side and the length of the " -"perimeter segment taken is limited to this parameter, but no longer than " -"anchor_length_max.\n" -"Set this parameter to zero to disable anchoring perimeters connected to a " -"single infill line." +"Connect an infill line to an internal perimeter with a short segment of an additional perimeter. If expressed as percentage (example: 15%) it is calculated over infill extrusion width. Orca Slicer tries to connect two close infill lines to a short perimeter segment. If no such perimeter segment shorter than infill_anchor_max is found, the infill line is connected to a perimeter segment at just one side and the length of the perimeter segment taken is limited to this parameter, but no longer than anchor_length_max.\n" +"Set this parameter to zero to disable anchoring perimeters connected to a single infill line." msgstr "" -"เชื่อมต่อเส้นไส้ในเข้ากับเส้นรอบวงภายในด้วยส่วนสั้นของเส้นรอบวงเพิ่มเติม หากระบุเป็นเปอร์เซ็นต์ " -"(ตัวอย่าง: 15%) ระบบจะคำนวณจากความกว้างการอัดรีดของไส้ใน Orca Slicer " -"จะพยายามเชื่อมต่อเส้นไส้ในสองเส้นที่อยู่ใกล้กันเข้ากับส่วนเส้นรอบวงสั้น ๆ " -"หากไม่พบส่วนเส้นรอบวงที่สั้นกว่า infill_anchor_max " -"เส้นไส้ในจะถูกเชื่อมต่อกับส่วนเส้นรอบวงเพียงด้านเดียว " -"และความยาวของส่วนเส้นรอบวงที่ใช้จะถูกจำกัดด้วยพารามิเตอร์นี้ แต่ต้องไม่เกิน " -"anchor_length_max\n" +"เชื่อมต่อเส้นไส้ในเข้ากับเส้นรอบวงภายในด้วยส่วนสั้นของเส้นรอบวงเพิ่มเติม หากระบุเป็นเปอร์เซ็นต์ (ตัวอย่าง: 15%) ระบบจะคำนวณจากความกว้างการอัดรีดของไส้ใน Orca Slicer จะพยายามเชื่อมต่อเส้นไส้ในสองเส้นที่อยู่ใกล้กันเข้ากับส่วนเส้นรอบวงสั้น ๆ หากไม่พบส่วนเส้นรอบวงที่สั้นกว่า infill_anchor_max เส้นไส้ในจะถูกเชื่อมต่อกับส่วนเส้นรอบวงเพียงด้านเดียว และความยาวของส่วนเส้นรอบวงที่ใช้จะถูกจำกัดด้วยพารามิเตอร์นี้ แต่ต้องไม่เกิน anchor_length_max\n" "ตั้งค่าพารามิเตอร์นี้เป็นศูนย์เพื่อปิดใช้งานเส้นรอบวงยึดที่เชื่อมต่อกับเส้นไส้ในเพียงเส้นเดียว" msgid "0 (no open anchors)" @@ -14894,23 +12857,10 @@ msgid "Maximum length of the infill anchor" msgstr "ความยาวสูงสุดของพุก ไส้ใน" msgid "" -"Connect an infill line to an internal perimeter with a short segment of an " -"additional perimeter. If expressed as percentage (example: 15%) it is " -"calculated over infill extrusion width. Orca Slicer tries to connect two " -"close infill lines to a short perimeter segment. If no such perimeter " -"segment shorter than this parameter is found, the infill line is connected " -"to a perimeter segment at just one side and the length of the perimeter " -"segment taken is limited to infill_anchor, but no longer than this " -"parameter.\n" -"If set to 0, the old algorithm for infill connection will be used, it should " -"create the same result as with 1000 & 0." +"Connect an infill line to an internal perimeter with a short segment of an additional perimeter. If expressed as percentage (example: 15%) it is calculated over infill extrusion width. Orca Slicer tries to connect two close infill lines to a short perimeter segment. If no such perimeter segment shorter than this parameter is found, the infill line is connected to a perimeter segment at just one side and the length of the perimeter segment taken is limited to infill_anchor, but no longer than this parameter.\n" +"If set to 0, the old algorithm for infill connection will be used, it should create the same result as with 1000 & 0." msgstr "" -"เชื่อมต่อเส้นไส้ในเข้ากับเส้นรอบวงภายในด้วยส่วนสั้นของเส้นรอบวงเพิ่มเติม หากระบุเป็นเปอร์เซ็นต์ " -"(ตัวอย่าง: 15%) ระบบจะคำนวณจากความกว้างการอัดรีดของไส้ใน Orca Slicer " -"จะพยายามเชื่อมต่อเส้นไส้ในสองเส้นที่อยู่ใกล้กันเข้ากับส่วนเส้นรอบวงสั้น ๆ " -"หากไม่พบส่วนเส้นรอบวงที่สั้นกว่าพารามิเตอร์นี้ เส้นไส้ในจะถูกเชื่อมต่อกับส่วนเส้นรอบวงเพียงด้านเดียว " -"และความยาวของส่วนเส้นรอบวงที่ใช้จะถูกจำกัดไว้ที่ infill_anchor " -"แต่ต้องไม่ยาวเกินพารามิเตอร์นี้\n" +"เชื่อมต่อเส้นไส้ในเข้ากับเส้นรอบวงภายในด้วยส่วนสั้นของเส้นรอบวงเพิ่มเติม หากระบุเป็นเปอร์เซ็นต์ (ตัวอย่าง: 15%) ระบบจะคำนวณจากความกว้างการอัดรีดของไส้ใน Orca Slicer จะพยายามเชื่อมต่อเส้นไส้ในสองเส้นที่อยู่ใกล้กันเข้ากับส่วนเส้นรอบวงสั้น ๆ หากไม่พบส่วนเส้นรอบวงที่สั้นกว่าพารามิเตอร์นี้ เส้นไส้ในจะถูกเชื่อมต่อกับส่วนเส้นรอบวงเพียงด้านเดียว และความยาวของส่วนเส้นรอบวงที่ใช้จะถูกจำกัดไว้ที่ infill_anchor แต่ต้องไม่ยาวเกินพารามิเตอร์นี้\n" "หากตั้งค่าเป็น 0 จะใช้อัลกอริทึมเดิมสำหรับการเชื่อมต่อไส้ใน ซึ่งควรให้ผลลัพธ์เดียวกับ 1000 & 0" msgid "0 (Simple connect)" @@ -14922,42 +12872,25 @@ msgstr "การเร่งความเร็วของผนังด้ msgid "Acceleration of travel moves." msgstr "การเร่งความเร็วของการเดินทาง" -msgid "" -"This is the acceleration of top surface infill. Using a lower value may " -"improve top surface quality." +msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "ความเร่งของไส้ในพื้นผิวด้านบน การใช้ค่าที่ต่ำกว่าอาจปรับปรุงคุณภาพพื้นผิวด้านบนได้" msgid "Acceleration of outer wall: using a lower value can improve quality." msgstr "ความเร่งของผนังด้านนอก การใช้ค่าที่ต่ำกว่าสามารถปรับปรุงคุณภาพได้" -msgid "" -"Acceleration of bridges. If the value is expressed as a percentage (e.g. " -"50%), it will be calculated based on the outer wall acceleration." -msgstr "" -"การเร่งความเร็วของสะพาน หากค่าแสดงเป็นเปอร์เซ็นต์ (เช่น 50%) " -"ค่าดังกล่าวจะคำนวณตามความเร่งของผนังด้านนอก" +msgid "Acceleration of bridges. If the value is expressed as a percentage (e.g. 50%), it will be calculated based on the outer wall acceleration." +msgstr "การเร่งความเร็วของสะพาน หากค่าแสดงเป็นเปอร์เซ็นต์ (เช่น 50%) ค่าดังกล่าวจะคำนวณตามความเร่งของผนังด้านนอก" msgid "mm/s² or %" msgstr "มม./วินาที² หรือ %" -msgid "" -"Acceleration of sparse infill. If the value is expressed as a percentage (e." -"g. 100%), it will be calculated based on the default acceleration." -msgstr "" -"ความเร่งของไส้ในแบบโปร่ง หากค่าแสดงเป็นเปอร์เซ็นต์ (เช่น 100%) " -"ค่านั้นจะถูกคำนวณตามความเร่งเริ่มต้น" +msgid "Acceleration of sparse infill. If the value is expressed as a percentage (e.g. 100%), it will be calculated based on the default acceleration." +msgstr "ความเร่งของไส้ในแบบโปร่ง หากค่าแสดงเป็นเปอร์เซ็นต์ (เช่น 100%) ค่านั้นจะถูกคำนวณตามความเร่งเริ่มต้น" -msgid "" -"Acceleration of internal solid infill. If the value is expressed as a " -"percentage (e.g. 100%), it will be calculated based on the default " -"acceleration." -msgstr "" -"ความเร่งของไส้ในของแข็งภายใน หากค่าแสดงเป็นเปอร์เซ็นต์ (เช่น 100%) " -"ค่านั้นจะถูกคำนวณตามความเร่งเริ่มต้น" +msgid "Acceleration of internal solid infill. If the value is expressed as a percentage (e.g. 100%), it will be calculated based on the default acceleration." +msgstr "ความเร่งของไส้ในของแข็งภายใน หากค่าแสดงเป็นเปอร์เซ็นต์ (เช่น 100%) ค่านั้นจะถูกคำนวณตามความเร่งเริ่มต้น" -msgid "" -"This is the printing acceleration for the first layer. Using limited " -"acceleration can improve build plate adhesion." +msgid "This is the printing acceleration for the first layer. Using limited acceleration can improve build plate adhesion." msgstr "ความเร่งของชั้นแรก การใช้ค่าที่ต่ำกว่าสามารถปรับปรุงการยึดเกาะของเพลตได้" msgid "First layer travel" @@ -14980,16 +12913,13 @@ msgid "accel_to_decel" msgstr "accel_to_decel" #, c-format, boost-format -msgid "" -"Klipper's max_accel_to_decel will be adjusted to this %% of acceleration." +msgid "Klipper's max_accel_to_decel will be adjusted to this %% of acceleration." msgstr "max_accel_to_decel ของ Klipper จะถูกปรับเป็น %% ของการเร่งความเร็วนี้" msgid "Default jerk." msgstr "กระตุกเริ่มต้น" -msgid "" -"Marlin Firmware Junction Deviation (replaces the traditional XY Jerk " -"setting)." +msgid "Marlin Firmware Junction Deviation (replaces the traditional XY Jerk setting)." msgstr "Marlin Firmware Junction Deviation (แทนที่การตั้งค่า XY Jerk แบบดั้งเดิม)" msgid "Jerk of outer walls." @@ -15017,19 +12947,14 @@ msgstr "" "กระตุกการเดินทางของชั้นแรก\n" "ค่าเปอร์เซ็นต์สัมพันธ์กับ Travel Jerk" -msgid "" -"Line width of the first layer. If expressed as a %, it will be computed over " -"the nozzle diameter." +msgid "Line width of the first layer. If expressed as a %, it will be computed over the nozzle diameter." msgstr "ความกว้างของเส้นของชั้นแรก หากแสดงเป็น % จะคำนวณตามเส้นผ่านศูนย์กลางของหัวฉีด" msgid "First layer height" msgstr "ความสูงของชั้นแรก" -msgid "" -"Height of the first layer. Making the first layer height thicker can improve " -"build plate adhesion." -msgstr "" -"ความสูงของชั้นแรก การเพิ่มความหนาของชั้นแรกสามารถช่วยให้ยึดเกาะกับฐานพิมพ์ได้ดีขึ้น" +msgid "Height of the first layer. Making the first layer height thicker can improve build plate adhesion." +msgstr "ความสูงของชั้นแรก การเพิ่มความหนาของชั้นแรกสามารถช่วยให้ยึดเกาะกับฐานพิมพ์ได้ดีขึ้น" msgid "This is the speed for the first layer except for solid infill sections." msgstr "ความเร็วของชั้นแรก ยกเว้นส่วน ไส้ใน ที่เป็นของแข็ง" @@ -15049,11 +12974,8 @@ msgstr "ความเร็วเดินหัวเปล่าของเ msgid "Number of slow layers" msgstr "จำนวนชั้นที่พิมพ์ช้าลง" -msgid "" -"The first few layers are printed slower than normal. The speed is gradually " -"increased in a linear fashion over the specified number of layers." -msgstr "" -"สองสามชั้นแรกจะพิมพ์ช้ากว่าปกติ ความเร็วจะค่อยๆ เพิ่มขึ้นในลักษณะเชิงเส้นตามจำนวนเลเยอร์ที่ระบุ" +msgid "The first few layers are printed slower than normal. The speed is gradually increased in a linear fashion over the specified number of layers." +msgstr "สองสามชั้นแรกจะพิมพ์ช้ากว่าปกติ ความเร็วจะค่อยๆ เพิ่มขึ้นในลักษณะเชิงเส้นตามจำนวนเลเยอร์ที่ระบุ" msgid "First layer nozzle temperature" msgstr "อุณหภูมิหัวฉีดชั้นแรก" @@ -15064,17 +12986,8 @@ msgstr "อุณหภูมิหัวฉีดสำหรับการพ msgid "Full fan speed at layer" msgstr "ความเร็วพัดลมเต็มชั้น" -msgid "" -"Fan speed will be ramped up linearly from zero at layer " -"\"close_fan_the_first_x_layers\" to maximum at layer \"full_fan_speed_layer" -"\". \"full_fan_speed_layer\" will be ignored if lower than " -"\"close_fan_the_first_x_layers\", in which case the fan will be running at " -"maximum allowed speed at layer \"close_fan_the_first_x_layers\" + 1." -msgstr "" -"ความเร็วพัดลมจะเพิ่มขึ้นเชิงเส้นจากศูนย์ที่เลเยอร์ \"close_fan_the_first_x_layers\" " -"ไปจนถึงสูงสุดที่เลเยอร์ \"full_fan_speed_layer\" \"full_fan_speed_layer\" " -"จะถูกละเว้นหากต่ำกว่า \"close_fan_the_first_x_layers\" ซึ่งในกรณีนี้ " -"พัดลมจะทำงานด้วยความเร็วสูงสุดที่อนุญาตที่เลเยอร์ \"close_fan_the_first_x_layers\" + 1" +msgid "Fan speed will be ramped up linearly from zero at layer \"close_fan_the_first_x_layers\" to maximum at layer \"full_fan_speed_layer\". \"full_fan_speed_layer\" will be ignored if lower than \"close_fan_the_first_x_layers\", in which case the fan will be running at maximum allowed speed at layer \"close_fan_the_first_x_layers\" + 1." +msgstr "ความเร็วพัดลมจะเพิ่มขึ้นเชิงเส้นจากศูนย์ที่เลเยอร์ \"close_fan_the_first_x_layers\" ไปจนถึงสูงสุดที่เลเยอร์ \"full_fan_speed_layer\" \"full_fan_speed_layer\" จะถูกละเว้นหากต่ำกว่า \"close_fan_the_first_x_layers\" ซึ่งในกรณีนี้ พัดลมจะทำงานด้วยความเร็วสูงสุดที่อนุญาตที่เลเยอร์ \"close_fan_the_first_x_layers\" + 1" msgid "layer" msgstr "ชั้น" @@ -15083,16 +12996,11 @@ msgid "Support interface fan speed" msgstr "ความเร็วพัดลมผิวสัมผัสส่วนรองรับ" msgid "" -"This part cooling fan speed is applied when printing support interfaces. " -"Setting this parameter to a higher than regular speed reduces the layer " -"binding strength between supports and the supported part, making them easier " -"to separate.\n" +"This part cooling fan speed is applied when printing support interfaces. Setting this parameter to a higher than regular speed reduces the layer binding strength between supports and the supported part, making them easier to separate.\n" "Set to -1 to disable it.\n" "This setting is overridden by disable_fan_first_layers." msgstr "" -"ความเร็วพัดลมระบายความร้อนในส่วนนี้จะถูกนำไปใช้เมื่อพิมพ์อินเทอร์เฟซที่รองรับ " -"การตั้งค่าพารามิเตอร์นี้ให้สูงกว่าความเร็วปกติจะช่วยลดความแข็งแรงในการผูกเลเยอร์ระหว่างส่วนรองรับและส่วนที่รองรับ " -"ทำให้แยกออกจากกันได้ง่ายขึ้น\n" +"ความเร็วพัดลมระบายความร้อนในส่วนนี้จะถูกนำไปใช้เมื่อพิมพ์อินเทอร์เฟซที่รองรับ การตั้งค่าพารามิเตอร์นี้ให้สูงกว่าความเร็วปกติจะช่วยลดความแข็งแรงในการผูกเลเยอร์ระหว่างส่วนรองรับและส่วนที่รองรับ ทำให้แยกออกจากกันได้ง่ายขึ้น\n" "ตั้งค่าเป็น -1 เพื่อปิดใช้งาน\n" "การตั้งค่านี้ถูกแทนที่โดย Disable_fan_first_layers" @@ -15100,82 +13008,50 @@ msgid "Internal bridges fan speed" msgstr "ความเร็วพัดลมภายในสะพาน" msgid "" -"The part cooling fan speed used for all internal bridges. Set to -1 to use " -"the overhang fan speed settings instead.\n" +"The part cooling fan speed used for all internal bridges. Set to -1 to use the overhang fan speed settings instead.\n" "\n" -"Reducing the internal bridges fan speed, compared to your regular fan speed, " -"can help reduce part warping due to excessive cooling applied over a large " -"surface for a prolonged period of time." +"Reducing the internal bridges fan speed, compared to your regular fan speed, can help reduce part warping due to excessive cooling applied over a large surface for a prolonged period of time." msgstr "" -"ความเร็วพัดลมระบายความร้อนชิ้นส่วนที่ใช้สำหรับบริดจ์ภายในทั้งหมด ตั้งค่าเป็น -1 " -"เพื่อใช้การตั้งค่าความเร็วพัดลมที่ยื่นออกมาแทน\n" +"ความเร็วพัดลมระบายความร้อนชิ้นส่วนที่ใช้สำหรับบริดจ์ภายในทั้งหมด ตั้งค่าเป็น -1 เพื่อใช้การตั้งค่าความเร็วพัดลมที่ยื่นออกมาแทน\n" "\n" -"การลดความเร็วพัดลมบริดจ์ภายในเมื่อเปรียบเทียบกับความเร็วพัดลมปกติ " -"สามารถช่วยลดการบิดงอของชิ้นส่วนเนื่องจากการระบายความร้อนที่มากเกินไปบนพื้นผิวขนาดใหญ่เป็นระยะเวลานาน" +"การลดความเร็วพัดลมบริดจ์ภายในเมื่อเปรียบเทียบกับความเร็วพัดลมปกติ สามารถช่วยลดการบิดงอของชิ้นส่วนเนื่องจากการระบายความร้อนที่มากเกินไปบนพื้นผิวขนาดใหญ่เป็นระยะเวลานาน" msgid "Ironing fan speed" msgstr "ความเร็วพัดลมรีดผิว" msgid "" -"This part cooling fan speed is applied when ironing. Setting this parameter " -"to a lower than regular speed reduces possible nozzle clogging due to the " -"low volumetric flow rate, making the interface smoother.\n" +"This part cooling fan speed is applied when ironing. Setting this parameter to a lower than regular speed reduces possible nozzle clogging due to the low volumetric flow rate, making the interface smoother.\n" "Set to -1 to disable it." msgstr "" -"ความเร็วพัดลมระบายความร้อนส่วนนี้ใช้กับการรีดผิว " -"การตั้งค่าพารามิเตอร์นี้ให้ต่ำกว่าความเร็วปกติจะช่วยลดปัญหาการอุดตันของหัวฉีดที่อาจเกิดขึ้นได้เนื่องจากอัตราการไหลของปริมาตรต่ำ " -"ทำให้อินเทอร์เฟซราบรื่นยิ่งขึ้น\n" +"ความเร็วพัดลมระบายความร้อนส่วนนี้ใช้กับการรีดผิว การตั้งค่าพารามิเตอร์นี้ให้ต่ำกว่าความเร็วปกติจะช่วยลดปัญหาการอุดตันของหัวฉีดที่อาจเกิดขึ้นได้เนื่องจากอัตราการไหลของปริมาตรต่ำ ทำให้อินเทอร์เฟซราบรื่นยิ่งขึ้น\n" "ตั้งค่าเป็น -1 เพื่อปิดใช้งาน" msgid "Ironing flow" msgstr "รีดผิวไหล" -msgid "" -"Filament-specific override for ironing flow. This allows you to customize " -"the ironing flow for each filament type. Too high value results in " -"overextrusion on the surface." -msgstr "" -"การแทนที่เส้นพลาสติกเฉพาะสำหรับกระแสการรีดผิว " -"ซึ่งช่วยให้คุณปรับแต่งกระแสการรีดผิวสำหรับเส้นพลาสติกแต่ละประเภทได้ " -"ค่าที่สูงเกินไปส่งผลให้เกิดการอัดขึ้นรูปมากเกินไปบนพื้นผิว" +msgid "Filament-specific override for ironing flow. This allows you to customize the ironing flow for each filament type. Too high value results in overextrusion on the surface." +msgstr "การแทนที่เส้นพลาสติกเฉพาะสำหรับกระแสการรีดผิว ซึ่งช่วยให้คุณปรับแต่งกระแสการรีดผิวสำหรับเส้นพลาสติกแต่ละประเภทได้ ค่าที่สูงเกินไปส่งผลให้เกิดการอัดขึ้นรูปมากเกินไปบนพื้นผิว" msgid "Ironing line spacing" msgstr "ระยะห่างระหว่างสายรีดผิว" -msgid "" -"Filament-specific override for ironing line spacing. This allows you to " -"customize the spacing between ironing lines for each filament type." -msgstr "" -"การแทนที่เส้นพลาสติกเฉพาะสำหรับระยะห่างระหว่างรีดผิว " -"ซึ่งช่วยให้คุณปรับแต่งระยะห่างระหว่างเส้นรีดผิวสำหรับเส้นพลาสติกแต่ละประเภทได้" +msgid "Filament-specific override for ironing line spacing. This allows you to customize the spacing between ironing lines for each filament type." +msgstr "การแทนที่เส้นพลาสติกเฉพาะสำหรับระยะห่างระหว่างรีดผิว ซึ่งช่วยให้คุณปรับแต่งระยะห่างระหว่างเส้นรีดผิวสำหรับเส้นพลาสติกแต่ละประเภทได้" msgid "Ironing inset" msgstr "อุปกรณ์รีดผิว" -msgid "" -"Filament-specific override for ironing inset. This allows you to customize " -"the distance to keep from the edges when ironing for each filament type." -msgstr "" -"การแทนที่เส้นพลาสติกเฉพาะสำหรับส่วนเสริมการรีดผิว " -"ซึ่งช่วยให้คุณปรับแต่งระยะห่างจากขอบเมื่อรีดผิวสำหรับเส้นพลาสติกแต่ละประเภทได้" +msgid "Filament-specific override for ironing inset. This allows you to customize the distance to keep from the edges when ironing for each filament type." +msgstr "การแทนที่เส้นพลาสติกเฉพาะสำหรับส่วนเสริมการรีดผิว ซึ่งช่วยให้คุณปรับแต่งระยะห่างจากขอบเมื่อรีดผิวสำหรับเส้นพลาสติกแต่ละประเภทได้" msgid "Ironing speed" msgstr "ความเร็วรีดผิว" -msgid "" -"Filament-specific override for ironing speed. This allows you to customize " -"the print speed of ironing lines for each filament type." -msgstr "" -"การแทนที่เส้นพลาสติกเฉพาะเพื่อความเร็วในการรีดผิว " -"ซึ่งช่วยให้คุณปรับแต่งความเร็วในการพิมพ์ของเส้นรีดผิวสำหรับเส้นพลาสติกแต่ละประเภทได้" +msgid "Filament-specific override for ironing speed. This allows you to customize the print speed of ironing lines for each filament type." +msgstr "การแทนที่เส้นพลาสติกเฉพาะเพื่อความเร็วในการรีดผิว ซึ่งช่วยให้คุณปรับแต่งความเร็วในการพิมพ์ของเส้นรีดผิวสำหรับเส้นพลาสติกแต่ละประเภทได้" -msgid "" -"This setting makes the toolhead randomly jitter while printing walls so that " -"the surface has a rough textured look. This setting controls the fuzzy " -"position." -msgstr "" -"การตั้งค่านี้ทำให้หัวพิมพ์สั่นแบบสุ่มขณะพิมพ์ผนัง เพื่อให้พื้นผิวมีลักษณะหยาบ การตั้งค่านี้ควบคุมตำแหน่งของ " -"fuzzy skin" +msgid "This setting makes the toolhead randomly jitter while printing walls so that the surface has a rough textured look. This setting controls the fuzzy position." +msgstr "การตั้งค่านี้ทำให้หัวพิมพ์สั่นแบบสุ่มขณะพิมพ์ผนัง เพื่อให้พื้นผิวมีลักษณะหยาบ การตั้งค่านี้ควบคุมตำแหน่งของ fuzzy skin" msgid "Painted only" msgstr "ทาสีเท่านั้น" @@ -15195,17 +13071,13 @@ msgstr "ผนังทั้งหมด" msgid "Fuzzy skin thickness" msgstr "ความหนาผิวฟัซซี" -msgid "" -"The width of jittering: it’s recommended to keep this lower than the outer " -"wall line width." +msgid "The width of jittering: it’s recommended to keep this lower than the outer wall line width." msgstr "ความกว้างของการสั่นแบบสุ่ม แนะนำให้ตั้งต่ำกว่าความกว้างเส้นผนังด้านนอก" msgid "Fuzzy skin point distance" msgstr "ระยะจุดผิวฟัซซี" -msgid "" -"The average distance between the random points introduced on each line " -"segment." +msgid "The average distance between the random points introduced on each line segment." msgstr "ระยะห่างเฉลี่ยระหว่างจุดสุ่มที่แนะนำในแต่ละส่วนของเส้น" msgid "Apply fuzzy skin to first layer" @@ -15220,41 +13092,18 @@ msgstr "โหมดสร้างผิวฟัซซี" #, c-format, boost-format msgid "" "Fuzzy skin generation mode. Works only with Arachne!\n" -"Displacement: Сlassic mode when the pattern is formed by shifting the nozzle " -"sideways from the original path.\n" -"Extrusion: The mode when the pattern formed by the amount of extruded " -"plastic. This is the fast and straight algorithm without unnecessary nozzle " -"shake that gives a smooth pattern. But it is more useful for forming loose " -"walls in the entire they array.\n" -"Combined: Joint mode [Displacement] + [Extrusion]. The appearance of the " -"walls is similar to [Displacement] Mode, but it leaves no pores between the " -"perimeters.\n" +"Displacement: Сlassic mode when the pattern is formed by shifting the nozzle sideways from the original path.\n" +"Extrusion: The mode when the pattern formed by the amount of extruded plastic. This is the fast and straight algorithm without unnecessary nozzle shake that gives a smooth pattern. But it is more useful for forming loose walls in the entire they array.\n" +"Combined: Joint mode [Displacement] + [Extrusion]. The appearance of the walls is similar to [Displacement] Mode, but it leaves no pores between the perimeters.\n" "\n" -"Attention! The [Extrusion] and [Combined] modes works only the " -"fuzzy_skin_thickness parameter not more than the thickness of printed loop. " -"At the same time, the width of the extrusion for a particular layer should " -"also not be below a certain level. It is usually equal 15-25%% of a layer " -"height. Therefore, the maximum fuzzy skin thickness with a perimeter width " -"of 0.4 mm and a layer height of 0.2 mm will be 0.4-(0.2*0.25)=±0.35mm! If " -"you enter a higher parameter than this, the error Flow::spacing() will " -"displayed, and the model will not be sliced. You can choose this number " -"until this error is repeated." +"Attention! The [Extrusion] and [Combined] modes works only the fuzzy_skin_thickness parameter not more than the thickness of printed loop. At the same time, the width of the extrusion for a particular layer should also not be below a certain level. It is usually equal 15-25%% of a layer height. Therefore, the maximum fuzzy skin thickness with a perimeter width of 0.4 mm and a layer height of 0.2 mm will be 0.4-(0.2*0.25)=±0.35mm! If you enter a higher parameter than this, the error Flow::spacing() will displayed, and the model will not be sliced. You can choose this number until this error is repeated." msgstr "" "โหมดการสร้างผิวฟัซซี ใช้ได้กับ Arachne เท่านั้น!\n" "การกระจัด: โหมดคลาสสิกเมื่อรูปแบบถูกสร้างขึ้นโดยการเลื่อนหัวฉีดไปด้านข้างจากเส้นทางเดิม\n" -"การอัดขึ้นรูป: โหมดเมื่อรูปแบบเกิดขึ้นจากปริมาณของพลาสติกที่อัดขึ้นรูป " -"นี่เป็นอัลกอริธึมที่รวดเร็วและตรงไปตรงมา โดยไม่มีการเขย่าหัวฉีดโดยไม่จำเป็นซึ่งให้รูปแบบที่ราบรื่น " -"แต่จะมีประโยชน์มากกว่าสำหรับการสร้างผนังที่หลวมทั่วทั้งบริเวณ\n" -"รวม: โหมดข้อต่อ [การแทนที่] + [การอัดขึ้นรูป] ลักษณะของผนังจะคล้ายกับโหมด [Displacement] " -"แต่ไม่มีรูพรุนระหว่างเส้นรอบวง\n" +"การอัดขึ้นรูป: โหมดเมื่อรูปแบบเกิดขึ้นจากปริมาณของพลาสติกที่อัดขึ้นรูป นี่เป็นอัลกอริธึมที่รวดเร็วและตรงไปตรงมา โดยไม่มีการเขย่าหัวฉีดโดยไม่จำเป็นซึ่งให้รูปแบบที่ราบรื่น แต่จะมีประโยชน์มากกว่าสำหรับการสร้างผนังที่หลวมทั่วทั้งบริเวณ\n" +"รวม: โหมดข้อต่อ [การแทนที่] + [การอัดขึ้นรูป] ลักษณะของผนังจะคล้ายกับโหมด [Displacement] แต่ไม่มีรูพรุนระหว่างเส้นรอบวง\n" "\n" -"ความสนใจ! โหมด [การอัดขึ้นรูป] และ [รวม] ใช้งานได้เฉพาะพารามิเตอร์ " -"fuzzy_skin_thickness ซึ่งไม่เกินความหนาของลูปที่พิมพ์ ในขณะเดียวกัน " -"ความกว้างของการอัดขึ้นรูปสำหรับชั้นใดชั้นหนึ่งก็ไม่ควรต่ำกว่าระดับที่กำหนดด้วย โดยปกติจะเท่ากับ " -"15-25%% ของความสูงของชั้น ดังนั้น ความหนาของผิวฟัซซีสูงสุดที่มีความกว้างเส้นรอบวง 0.4 มม. " -"และความสูงของชั้น 0.2 มม. จะเป็น 0.4-(0.2*0.25)=±0.35 มม.! " -"หากคุณป้อนพารามิเตอร์ที่สูงกว่านี้ ข้อผิดพลาด Flow::spacing() จะปรากฏขึ้น " -"และโมเดลจะไม่ถูกแบ่งส่วน คุณสามารถเลือกหมายเลขนี้ได้จนกว่าข้อผิดพลาดนี้จะเกิดขึ้นซ้ำ" +"ความสนใจ! โหมด [การอัดขึ้นรูป] และ [รวม] ใช้งานได้เฉพาะพารามิเตอร์ fuzzy_skin_thickness ซึ่งไม่เกินความหนาของลูปที่พิมพ์ ในขณะเดียวกัน ความกว้างของการอัดขึ้นรูปสำหรับชั้นใดชั้นหนึ่งก็ไม่ควรต่ำกว่าระดับที่กำหนดด้วย โดยปกติจะเท่ากับ 15-25%% ของความสูงของชั้น ดังนั้น ความหนาของผิวฟัซซีสูงสุดที่มีความกว้างเส้นรอบวง 0.4 มม. และความสูงของชั้น 0.2 มม. จะเป็น 0.4-(0.2*0.25)=±0.35 มม.! หากคุณป้อนพารามิเตอร์ที่สูงกว่านี้ ข้อผิดพลาด Flow::spacing() จะปรากฏขึ้น และโมเดลจะไม่ถูกแบ่งส่วน คุณสามารถเลือกหมายเลขนี้ได้จนกว่าข้อผิดพลาดนี้จะเกิดขึ้นซ้ำ" msgid "Displacement" msgstr "การกระจัด" @@ -15273,22 +13122,17 @@ msgid "" "Classic: Classic uniform random noise.\n" "Perlin: Perlin noise, which gives a more consistent texture.\n" "Billow: Similar to perlin noise, but clumpier.\n" -"Ridged Multifractal: Ridged noise with sharp, jagged features. Creates " -"marble-like textures.\n" -"Voronoi: Divides the surface into voronoi cells, and displaces each one by a " -"random amount. Creates a patchwork texture.\n" -"Ripple: Uniform ripple pattern that ripples left and right of the original " -"path. Repeating pattern, woven appearance." +"Ridged Multifractal: Ridged noise with sharp, jagged features. Creates marble-like textures.\n" +"Voronoi: Divides the surface into voronoi cells, and displaces each one by a random amount. Creates a patchwork texture.\n" +"Ripple: Uniform ripple pattern that ripples left and right of the original path. Repeating pattern, woven appearance." msgstr "" "ประเภทเสียงรบกวนที่ใช้สำหรับการสร้างผิวที่คลุมเครือ:\n" "คลาสสิก: เสียงสุ่มเครื่องแบบคลาสสิก\n" "Perlin: เสียง Perlin ซึ่งให้เนื้อสัมผัสที่สม่ำเสมอยิ่งขึ้น\n" "Billow: คล้ายกับเสียงเพอร์ลิน แต่เป็นกลุ่มมากกว่า\n" "Ridged Multifractal: สัญญาณรบกวนที่คมชัดพร้อมคุณสมบัติหยัก สร้างพื้นผิวเหมือนหินอ่อน\n" -"โวโรนอย: แบ่งพื้นผิวออกเป็นเซลล์โวโรนอย และแทนที่แต่ละเซลล์ด้วยจำนวนสุ่ม " -"สร้างพื้นผิวแบบเย็บปะติดปะต่อกัน\n" -"ระลอกคลื่น: รูปแบบระลอกคลื่นสม่ำเสมอที่กระเพื่อมไปทางซ้ายและขวาของเส้นทางเดิม ลายซ้ำ " -"ลักษณะการทอ." +"โวโรนอย: แบ่งพื้นผิวออกเป็นเซลล์โวโรนอย และแทนที่แต่ละเซลล์ด้วยจำนวนสุ่ม สร้างพื้นผิวแบบเย็บปะติดปะต่อกัน\n" +"ระลอกคลื่น: รูปแบบระลอกคลื่นสม่ำเสมอที่กระเพื่อมไปทางซ้ายและขวาของเส้นทางเดิม ลายซ้ำ ลักษณะการทอ." msgid "Classic" msgstr "คลาสสิค" @@ -15311,31 +13155,20 @@ msgstr "ระลอกคลื่น" msgid "Fuzzy skin feature size" msgstr "ขนาดลักษณะผิวฟัซซี" -msgid "" -"The base size of the coherent noise features, in mm. Higher values will " -"result in larger features." -msgstr "" -"ขนาดฐานของคุณสมบัติเสียงที่สอดคล้องกัน หน่วยเป็น มม. ค่าที่สูงกว่าจะส่งผลให้มีคุณลักษณะที่ใหญ่ขึ้น" +msgid "The base size of the coherent noise features, in mm. Higher values will result in larger features." +msgstr "ขนาดฐานของคุณสมบัติเสียงที่สอดคล้องกัน หน่วยเป็น มม. ค่าที่สูงกว่าจะส่งผลให้มีคุณลักษณะที่ใหญ่ขึ้น" msgid "Fuzzy Skin Noise Octaves" msgstr "อ็อกเทฟเสียงผิวฟัซซี" -msgid "" -"The number of octaves of coherent noise to use. Higher values increase the " -"detail of the noise, but also increase computation time." -msgstr "" -"จำนวนอ็อกเทฟของสัญญาณรบกวนที่สอดคล้องกันที่จะใช้ ค่าที่สูงกว่าจะเพิ่มรายละเอียดของสัญญาณรบกวน " -"แต่ยังเพิ่มเวลาในการคำนวณด้วย" +msgid "The number of octaves of coherent noise to use. Higher values increase the detail of the noise, but also increase computation time." +msgstr "จำนวนอ็อกเทฟของสัญญาณรบกวนที่สอดคล้องกันที่จะใช้ ค่าที่สูงกว่าจะเพิ่มรายละเอียดของสัญญาณรบกวน แต่ยังเพิ่มเวลาในการคำนวณด้วย" msgid "Fuzzy skin noise persistence" msgstr "ความคงอยู่ของเสียงผิวเลือน" -msgid "" -"The decay rate for higher octaves of the coherent noise. Lower values will " -"result in smoother noise." -msgstr "" -"อัตราการสลายตัวของอ็อกเทฟที่สูงขึ้นของสัญญาณรบกวนที่สอดคล้องกัน " -"ค่าที่ต่ำกว่าจะส่งผลให้มีสัญญาณรบกวนที่นุ่มนวลขึ้น" +msgid "The decay rate for higher octaves of the coherent noise. Lower values will result in smoother noise." +msgstr "อัตราการสลายตัวของอ็อกเทฟที่สูงขึ้นของสัญญาณรบกวนที่สอดคล้องกัน ค่าที่ต่ำกว่าจะส่งผลให้มีสัญญาณรบกวนที่นุ่มนวลขึ้น" msgid "Number of ripples per layer" msgstr "จำนวนระลอกคลื่นต่อชั้น" @@ -15347,45 +13180,33 @@ msgid "Ripple offset" msgstr "ระลอกชดเชย" msgid "" -"Shifts the ripple phase forward along the print path by the specified " -"percentage of a wavelength each layer period.\n" +"Shifts the ripple phase forward along the print path by the specified percentage of a wavelength each layer period.\n" "- 0% keeps every layer identical.\n" -"- 50% shifts the pattern by half a wavelength, effectively inverting the " -"phase.\n" -"- 100% shifts the pattern by a full wavelength, returning to the original " -"phase.\n" +"- 50% shifts the pattern by half a wavelength, effectively inverting the phase.\n" +"- 100% shifts the pattern by a full wavelength, returning to the original phase.\n" "\n" -"The shift is applied once every number of layers set by Layers between " -"ripple offset, so layers within the same group are printed identically." +"The shift is applied once every number of layers set by Layers between ripple offset, so layers within the same group are printed identically." msgstr "" "เลื่อนระยะระลอกคลื่นไปข้างหน้าตามเส้นทางการพิมพ์ตามเปอร์เซ็นต์ที่ระบุของความยาวคลื่นในแต่ละช่วงของเลเยอร์\n" "- 0% ทำให้ทุกชั้นเหมือนกัน\n" "- 50% เลื่อนรูปแบบลงครึ่งหนึ่งของความยาวคลื่น จึงสามารถกลับเฟสได้อย่างมีประสิทธิภาพ\n" "- 100% เลื่อนรูปแบบตามความยาวคลื่นเต็ม และกลับสู่เฟสเดิม\n" "\n" -"การเปลี่ยนแปลงจะใช้ทุกๆ จำนวนเลเยอร์ที่กำหนดโดยเลเยอร์ระหว่างออฟเซ็ตระลอกคลื่น " -"ดังนั้นเลเยอร์ภายในกลุ่มเดียวกันจึงถูกพิมพ์เหมือนกัน" +"การเปลี่ยนแปลงจะใช้ทุกๆ จำนวนเลเยอร์ที่กำหนดโดยเลเยอร์ระหว่างออฟเซ็ตระลอกคลื่น ดังนั้นเลเยอร์ภายในกลุ่มเดียวกันจึงถูกพิมพ์เหมือนกัน" msgid "Layers between ripple offset" msgstr "เลเยอร์ระหว่างการชดเชยระลอกคลื่น" msgid "" -"Specifies how many consecutive layers share the same ripple phase before the " -"offset is applied.\n" +"Specifies how many consecutive layers share the same ripple phase before the offset is applied.\n" "For example:\n" -"- 1 = Layer 1 is printed with the base ripple pattern, then layer 2 is " -"shifted by the configured offset, then layer 3 returns to the base pattern, " -"and so on.\n" -"- 3 = Layers 1 to 3 are printed with the base ripple pattern, then layers 4 " -"to 6 are shifted by the configured offset, then layers 7 to 9 return to the " -"base pattern, etc." +"- 1 = Layer 1 is printed with the base ripple pattern, then layer 2 is shifted by the configured offset, then layer 3 returns to the base pattern, and so on.\n" +"- 3 = Layers 1 to 3 are printed with the base ripple pattern, then layers 4 to 6 are shifted by the configured offset, then layers 7 to 9 return to the base pattern, etc." msgstr "" "ระบุจำนวนเลเยอร์ที่ต่อเนื่องกันที่ใช้เฟสระลอกคลื่นเดียวกันก่อนที่จะใช้ออฟเซ็ต\n" "ตัวอย่างเช่น:\n" -"- 1 = เลเยอร์ 1 ถูกพิมพ์ด้วยรูปแบบระลอกคลื่นฐาน จากนั้นเลเยอร์ 2 " -"จะถูกเลื่อนโดยออฟเซ็ตที่กำหนดค่าไว้ จากนั้นเลเยอร์ 3 จะกลับสู่รูปแบบฐาน และอื่นๆ\n" -"- 3 = เลเยอร์ 1 ถึง 3 จะถูกพิมพ์ด้วยรูปแบบระลอกฐาน จากนั้นเลเยอร์ 4 ถึง 6 " -"จะถูกเลื่อนโดยออฟเซ็ตที่กำหนดค่าไว้ จากนั้นเลเยอร์ 7 ถึง 9 จะกลับสู่รูปแบบพื้นฐาน เป็นต้น" +"- 1 = เลเยอร์ 1 ถูกพิมพ์ด้วยรูปแบบระลอกคลื่นฐาน จากนั้นเลเยอร์ 2 จะถูกเลื่อนโดยออฟเซ็ตที่กำหนดค่าไว้ จากนั้นเลเยอร์ 3 จะกลับสู่รูปแบบฐาน และอื่นๆ\n" +"- 3 = เลเยอร์ 1 ถึง 3 จะถูกพิมพ์ด้วยรูปแบบระลอกฐาน จากนั้นเลเยอร์ 4 ถึง 6 จะถูกเลื่อนโดยออฟเซ็ตที่กำหนดค่าไว้ จากนั้นเลเยอร์ 7 ถึง 9 จะกลับสู่รูปแบบพื้นฐาน เป็นต้น" msgid "Filter out tiny gaps" msgstr "กรองช่องว่างเล็กๆ ออกไป" @@ -15393,83 +13214,47 @@ msgstr "กรองช่องว่างเล็กๆ ออกไป" msgid "Layers and Perimeters" msgstr "ชั้นและปริมณฑล" -msgid "" -"Don't print gap fill with a length is smaller than the threshold specified " -"(in mm). This setting applies to top, bottom and solid infill and, if using " -"the classic perimeter generator, to wall gap fill." -msgstr "" -"อย่าพิมพ์การเติมช่องว่างโดยมีความยาวน้อยกว่าเกณฑ์ที่ระบุ (เป็นมม.) " -"การตั้งค่านี้ใช้กับการเติมช่องว่างด้านบน ด้านล่าง และทึบ " -"และหากใช้เครื่องกำเนิดเส้นรอบวงแบบคลาสสิก ก็สามารถทำการเติมช่องว่างที่ผนังได้" +msgid "Don't print gap fill with a length is smaller than the threshold specified (in mm). This setting applies to top, bottom and solid infill and, if using the classic perimeter generator, to wall gap fill." +msgstr "อย่าพิมพ์การเติมช่องว่างโดยมีความยาวน้อยกว่าเกณฑ์ที่ระบุ (เป็นมม.) การตั้งค่านี้ใช้กับการเติมช่องว่างด้านบน ด้านล่าง และทึบ และหากใช้เครื่องกำเนิดเส้นรอบวงแบบคลาสสิก ก็สามารถทำการเติมช่องว่างที่ผนังได้" -msgid "" -"This is the speed for gap infill. Gaps usually have irregular line width and " -"should be printed more slowly." -msgstr "" -"ความเร็วของการเติมช่องว่าง Gap มักจะมีความกว้างของเส้นไม่สม่ำเสมอ และควรพิมพ์ให้ช้าลง" +msgid "This is the speed for gap infill. Gaps usually have irregular line width and should be printed more slowly." +msgstr "ความเร็วของการเติมช่องว่าง Gap มักจะมีความกว้างของเส้นไม่สม่ำเสมอ และควรพิมพ์ให้ช้าลง" msgid "Precise Z height" msgstr "ความสูง Z ที่แม่นยำ" -msgid "" -"Enable this to get precise Z height of object after slicing. It will get the " -"precise object height by fine-tuning the layer heights of the last few " -"layers. Note that this is an experimental parameter." -msgstr "" -"เปิดใช้งานสิ่งนี้เพื่อรับความสูง Z ของวัตถุที่แม่นยำหลังจากการแบ่งส่วน " -"มันจะได้ความสูงของวัตถุที่แม่นยำโดยการปรับความสูงของเลเยอร์ของสองสามเลเยอร์สุดท้ายอย่างละเอียด " -"โปรดทราบว่านี่เป็นพารามิเตอร์ทดลอง" +msgid "Enable this to get precise Z height of object after slicing. It will get the precise object height by fine-tuning the layer heights of the last few layers. Note that this is an experimental parameter." +msgstr "เปิดใช้งานสิ่งนี้เพื่อรับความสูง Z ของวัตถุที่แม่นยำหลังจากการแบ่งส่วน มันจะได้ความสูงของวัตถุที่แม่นยำโดยการปรับความสูงของเลเยอร์ของสองสามเลเยอร์สุดท้ายอย่างละเอียด โปรดทราบว่านี่เป็นพารามิเตอร์ทดลอง" msgid "Arc fitting" msgstr "ข้อต่อส่วนโค้ง" msgid "" -"Enable this to get a G-code file which has G2 and G3 moves. The fitting " -"tolerance is same as the resolution.\n" +"Enable this to get a G-code file which has G2 and G3 moves. The fitting tolerance is same as the resolution.\n" "\n" -"Note: For Klipper machines, this option is recommended to be disabled. " -"Klipper does not benefit from arc commands as these are split again into " -"line segments by the firmware. This results in a reduction in surface " -"quality as line segments are converted to arcs by the slicer and then back " -"to line segments by the firmware." +"Note: For Klipper machines, this option is recommended to be disabled. Klipper does not benefit from arc commands as these are split again into line segments by the firmware. This results in a reduction in surface quality as line segments are converted to arcs by the slicer and then back to line segments by the firmware." msgstr "" -"เปิดใช้งานสิ่งนี้เพื่อรับไฟล์ G-code ซึ่งมีการเคลื่อนไหว G2 และ G3 " -"ความคลาดเคลื่อนในการติดตั้งจะเหมือนกับความละเอียด\n" +"เปิดใช้งานสิ่งนี้เพื่อรับไฟล์ G-code ซึ่งมีการเคลื่อนไหว G2 และ G3 ความคลาดเคลื่อนในการติดตั้งจะเหมือนกับความละเอียด\n" "\n" -"หมายเหตุ: สำหรับเครื่อง Klipper แนะนำให้ปิดการใช้งานตัวเลือกนี้ Klipper " -"ไม่ได้รับประโยชน์จากคำสั่ง arc " -"เนื่องจากคำสั่งเหล่านี้จะถูกแบ่งออกเป็นส่วนของบรรทัดอีกครั้งโดยเฟิร์มแวร์ " -"ซึ่งส่งผลให้คุณภาพพื้นผิวลดลง เนื่องจากส่วนของเส้นถูกแปลงเป็นส่วนโค้งโดยตัวแบ่งส่วนข้อมูล " -"จากนั้นเฟิร์มแวร์จะแปลงกลับไปยังส่วนของเส้นตรง" +"หมายเหตุ: สำหรับเครื่อง Klipper แนะนำให้ปิดการใช้งานตัวเลือกนี้ Klipper ไม่ได้รับประโยชน์จากคำสั่ง arc เนื่องจากคำสั่งเหล่านี้จะถูกแบ่งออกเป็นส่วนของบรรทัดอีกครั้งโดยเฟิร์มแวร์ ซึ่งส่งผลให้คุณภาพพื้นผิวลดลง เนื่องจากส่วนของเส้นถูกแปลงเป็นส่วนโค้งโดยตัวแบ่งส่วนข้อมูล จากนั้นเฟิร์มแวร์จะแปลงกลับไปยังส่วนของเส้นตรง" msgid "Add line number" msgstr "เพิ่มหมายเลขบรรทัด" -msgid "" -"Enable this to add line number(Nx) at the beginning of each G-code line." +msgid "Enable this to add line number(Nx) at the beginning of each G-code line." msgstr "เปิดใช้งานสิ่งนี้เพื่อเพิ่มหมายเลขบรรทัด (Nx) ที่จุดเริ่มต้นของบรรทัด G-code แต่ละบรรทัด" msgid "Scan first layer" msgstr "สแกนชั้นแรก" -msgid "" -"Enable this to allow the camera on the printer to check the quality of the " -"first layer." +msgid "Enable this to allow the camera on the printer to check the quality of the first layer." msgstr "เปิดใช้งานสิ่งนี้เพื่อให้กล้องบนเครื่องพิมพ์สามารถตรวจสอบคุณภาพของเลเยอร์แรกได้" msgid "Power Loss Recovery" msgstr "การกู้คืนการสูญเสียพลังงาน" -msgid "" -"Choose how to control power loss recovery. When set to Printer " -"configuration, the slicer will not emit power loss recovery G-code and will " -"leave the printer's configuration unchanged. Applicable to Bambu Lab or " -"Marlin 2 firmware based printers." -msgstr "" -"เลือกวิธีควบคุมการกู้คืนพลังงานที่สูญเสียไป เมื่อตั้งค่าเป็นการกำหนดค่าเครื่องพิมพ์ " -"ตัวแบ่งส่วนข้อมูลจะไม่ปล่อย G-code การกู้คืนการสูญเสียพลังงาน " -"และจะปล่อยให้การกำหนดค่าของเครื่องพิมพ์ไม่เปลี่ยนแปลง ใช้ได้กับเครื่องพิมพ์ที่ใช้เฟิร์มแวร์ Bambu " -"Lab หรือ Marlin 2" +msgid "Choose how to control power loss recovery. When set to Printer configuration, the slicer will not emit power loss recovery G-code and will leave the printer's configuration unchanged. Applicable to Bambu Lab or Marlin 2 firmware based printers." +msgstr "เลือกวิธีควบคุมการกู้คืนพลังงานที่สูญเสียไป เมื่อตั้งค่าเป็นการกำหนดค่าเครื่องพิมพ์ ตัวแบ่งส่วนข้อมูลจะไม่ปล่อย G-code การกู้คืนการสูญเสียพลังงาน และจะปล่อยให้การกำหนดค่าของเครื่องพิมพ์ไม่เปลี่ยนแปลง ใช้ได้กับเครื่องพิมพ์ที่ใช้เฟิร์มแวร์ Bambu Lab หรือ Marlin 2" msgid "Printer configuration" msgstr "การกำหนดค่าเครื่องพิมพ์" @@ -15477,12 +13262,8 @@ msgstr "การกำหนดค่าเครื่องพิมพ์" msgid "Nozzle type" msgstr "ชนิดหัวฉีด" -msgid "" -"The metallic material of the nozzle: This determines the abrasive resistance " -"of the nozzle and what kind of filament can be printed." -msgstr "" -"วัสดุโลหะของหัวฉีด สิ่งนี้จะกำหนดความต้านทานต่อการขัดถูของหัวฉีด " -"และชนิดของฟิลาเมนท์ที่สามารถพิมพ์ได้" +msgid "The metallic material of the nozzle: This determines the abrasive resistance of the nozzle and what kind of filament can be printed." +msgstr "วัสดุโลหะของหัวฉีด สิ่งนี้จะกำหนดความต้านทานต่อการขัดถูของหัวฉีด และชนิดของฟิลาเมนท์ที่สามารถพิมพ์ได้" msgid "Hardened steel" msgstr "เหล็กชุบแข็ง" @@ -15496,9 +13277,7 @@ msgstr "ทังสเตนคาร์ไบด์" msgid "Nozzle HRC" msgstr "หัวฉีด HRC" -msgid "" -"The nozzle's hardness. Zero means no checking for nozzle hardness during " -"slicing." +msgid "The nozzle's hardness. Zero means no checking for nozzle hardness during slicing." msgstr "ความแข็งของหัวฉีด ค่าศูนย์หมายถึงไม่ต้องตรวจสอบความแข็งของหัวฉีดระหว่างการตัด" msgid "HRC" @@ -15528,26 +13307,16 @@ msgstr "ตำแหน่งวัตถุที่ดีที่สุด" msgid "Best auto arranging position in range [0,1] w.r.t. bed shape." msgstr "ตำแหน่งการจัดเรียงอัตโนมัติที่ดีที่สุดในช่วง [0,1] w.r.t. รูปร่างฐานพิมพ์" -msgid "" -"Enable this option if machine has auxiliary part cooling fan. G-code " -"command: M106 P2 S(0-255)." -msgstr "" -"เปิดใช้งานตัวเลือกนี้หากเครื่องมีพัดลมระบายความร้อนชิ้นส่วนเสริม คำสั่งรหัส G: M106 P2 S(0-255)" +msgid "Enable this option if machine has auxiliary part cooling fan. G-code command: M106 P2 S(0-255)." +msgstr "เปิดใช้งานตัวเลือกนี้หากเครื่องมีพัดลมระบายความร้อนชิ้นส่วนเสริม คำสั่งรหัส G: M106 P2 S(0-255)" msgid "" -"Start the fan this number of seconds earlier than its target start time (you " -"can use fractional seconds). It assumes infinite acceleration for this time " -"estimation, and will only take into account G1 and G0 moves (arc fitting is " -"unsupported).\n" -"It won't move fan commands from custom G-code (they act as a sort of " -"'barrier').\n" -"It won't move fan commands into the start G-code if the 'only custom start G-" -"code' is activated.\n" +"Start the fan this number of seconds earlier than its target start time (you can use fractional seconds). It assumes infinite acceleration for this time estimation, and will only take into account G1 and G0 moves (arc fitting is unsupported).\n" +"It won't move fan commands from custom G-code (they act as a sort of 'barrier').\n" +"It won't move fan commands into the start G-code if the 'only custom start G-code' is activated.\n" "Use 0 to deactivate." msgstr "" -"เริ่มพัดลมตามจำนวนวินาทีนี้เร็วกว่าเวลาเริ่มต้นเป้าหมาย (คุณสามารถใช้เศษส่วนวินาทีได้) " -"โดยจะใช้ความเร่งอนันต์สำหรับการประมาณเวลานี้ และจะพิจารณาเฉพาะการเคลื่อนที่ของ G1 และ G0 " -"เท่านั้น (ไม่รองรับการปรับส่วนโค้ง)\n" +"เริ่มพัดลมตามจำนวนวินาทีนี้เร็วกว่าเวลาเริ่มต้นเป้าหมาย (คุณสามารถใช้เศษส่วนวินาทีได้) โดยจะใช้ความเร่งอนันต์สำหรับการประมาณเวลานี้ และจะพิจารณาเฉพาะการเคลื่อนที่ของ G1 และ G0 เท่านั้น (ไม่รองรับการปรับส่วนโค้ง)\n" "มันจะไม่ย้ายคำสั่งพัดลมจาก G-code ที่กำหนดเอง (คำสั่งเหล่านั้นทำหน้าที่เป็น 'อุปสรรค')\n" "มันจะไม่ย้ายคำสั่งพัดลมไปที่รหัส G เริ่มต้นหากเปิดใช้งาน 'รหัส G-รหัสเริ่มต้นแบบกำหนดเองเท่านั้น'\n" "ใช้ 0 เพื่อปิดใช้งาน" @@ -15562,50 +13331,24 @@ msgid "Fan kick-start time" msgstr "เวลาเริ่มเตะพัดลม" msgid "" -"Emit a max fan speed command for this amount of seconds before reducing to " -"target speed to kick-start the cooling fan.\n" -"This is useful for fans where a low PWM/power may be insufficient to get the " -"fan started spinning from a stop, or to get the fan up to speed faster.\n" +"Emit a max fan speed command for this amount of seconds before reducing to target speed to kick-start the cooling fan.\n" +"This is useful for fans where a low PWM/power may be insufficient to get the fan started spinning from a stop, or to get the fan up to speed faster.\n" "Set to 0 to deactivate." msgstr "" "ปล่อยคำสั่งความเร็วพัดลมสูงสุดเป็นเวลาไม่กี่วินาทีก่อนที่จะลดความเร็วลงตามเป้าหมายเพื่อสตาร์ทพัดลมระบายความร้อน\n" -"สิ่งนี้มีประโยชน์สำหรับพัดลมที่ความเร็ว/PWM ต่ำอาจไม่เพียงพอที่จะทำให้พัดลมเริ่มหมุนจากจุดหยุด " -"หรือเพื่อให้พัดลมทำงานเร็วขึ้น\n" +"สิ่งนี้มีประโยชน์สำหรับพัดลมที่ความเร็ว/PWM ต่ำอาจไม่เพียงพอที่จะทำให้พัดลมเริ่มหมุนจากจุดหยุด หรือเพื่อให้พัดลมทำงานเร็วขึ้น\n" "ตั้งค่าเป็น 0 เพื่อปิดใช้งาน" msgid "Minimum non-zero part cooling fan speed" msgstr "ความเร็วพัดลมระบายความร้อนชิ้นส่วนขั้นต่ำที่ไม่เป็นศูนย์" msgid "" -"Some part-cooling fans cannot start spinning when commanded below a certain " -"PWM duty cycle. When set above 0, any non-zero part-cooling fan command will " -"be raised to at least this percentage so the fan reliably starts. A fan " -"command of 0 (fan off) is always honoured exactly. This clamp is applied " -"after every other fan calculation (first-layer ramp, layer-time " -"interpolation, overhang/bridge/support-interface/ironing overrides), so " -"scaling still operates within the range [this value, 100%].\n" -"If your firmware already disables the fan below a threshold (for example " -"Klipper's [fan] off_below: 0.10 shuts the fan off whenever the commanded " -"duty cycle is below 10%), this option and the firmware threshold should " -"ideally be set to the same value. Matching them (e.g. off_below: 0.10 in " -"Klipper and 10% here) guarantees the slicer never emits a non-zero value " -"that the firmware would silently drop, and the fan never receives a value " -"below the one you know it can actually spool at.\n" +"Some part-cooling fans cannot start spinning when commanded below a certain PWM duty cycle. When set above 0, any non-zero part-cooling fan command will be raised to at least this percentage so the fan reliably starts. A fan command of 0 (fan off) is always honoured exactly. This clamp is applied after every other fan calculation (first-layer ramp, layer-time interpolation, overhang/bridge/support-interface/ironing overrides), so scaling still operates within the range [this value, 100%].\n" +"If your firmware already disables the fan below a threshold (for example Klipper's [fan] off_below: 0.10 shuts the fan off whenever the commanded duty cycle is below 10%), this option and the firmware threshold should ideally be set to the same value. Matching them (e.g. off_below: 0.10 in Klipper and 10% here) guarantees the slicer never emits a non-zero value that the firmware would silently drop, and the fan never receives a value below the one you know it can actually spool at.\n" "Set to 0 to deactivate." msgstr "" -"พัดลมระบายความร้อนบางส่วนไม่สามารถเริ่มหมุนได้เมื่อได้รับคำสั่งให้ต่ำกว่ารอบการทำงานของ PWM " -"ที่กำหนด เมื่อตั้งค่าสูงกว่า 0 " -"คำสั่งพัดลมระบายความร้อนที่ไม่เป็นศูนย์จะเพิ่มขึ้นเป็นเปอร์เซ็นต์เป็นอย่างน้อย " -"เพื่อให้พัดลมเริ่มทำงานได้อย่างน่าเชื่อถือ คำสั่งพัดลมเป็น 0 (ปิดพัดลม) จะได้รับเกียรติเสมอ " -"แคลมป์นี้จะถูกนำไปใช้หลังจากการคำนวณพัดลมอื่นๆ ทุกครั้ง (การลาดชั้นแรก การแก้ไขเลเยอร์ไทม์ " -"ส่วนยื่น/บริดจ์/ส่วนรองรับ/การแทนที่การรีดผิว) ดังนั้นการปรับขนาดยังคงทำงานภายในช่วง [ค่านี้ " -"100%]\n" -"หากเฟิร์มแวร์ของคุณปิดใช้งานพัดลมที่ต่ำกว่าเกณฑ์อยู่แล้ว (เช่น [พัดลม] off_below ของ Klipper: " -"0.10 จะปิดพัดลมเมื่อใดก็ตามที่รอบการทำงานที่ได้รับคำสั่งต่ำกว่า 10%) " -"ตัวเลือกนี้และเกณฑ์ของเฟิร์มแวร์ควรได้รับการตั้งค่าตามอุดมคติให้เป็นค่าเดียวกัน การจับคู่พวกมัน (เช่น " -"off_below: 0.10 ใน Klipper และ 10% ที่นี่) " -"รับประกันว่าตัวแบ่งส่วนข้อมูลจะไม่ปล่อยค่าที่ไม่ใช่ศูนย์ซึ่งเฟิร์มแวร์จะดรอปอย่างเงียบๆ " -"และพัดลมจะไม่ได้รับค่าที่ต่ำกว่าค่าที่คุณรู้ว่าสามารถสปูลได้จริง\n" +"พัดลมระบายความร้อนบางส่วนไม่สามารถเริ่มหมุนได้เมื่อได้รับคำสั่งให้ต่ำกว่ารอบการทำงานของ PWM ที่กำหนด เมื่อตั้งค่าสูงกว่า 0 คำสั่งพัดลมระบายความร้อนที่ไม่เป็นศูนย์จะเพิ่มขึ้นเป็นเปอร์เซ็นต์เป็นอย่างน้อย เพื่อให้พัดลมเริ่มทำงานได้อย่างน่าเชื่อถือ คำสั่งพัดลมเป็น 0 (ปิดพัดลม) จะได้รับเกียรติเสมอ แคลมป์นี้จะถูกนำไปใช้หลังจากการคำนวณพัดลมอื่นๆ ทุกครั้ง (การลาดชั้นแรก การแก้ไขเลเยอร์ไทม์ ส่วนยื่น/บริดจ์/ส่วนรองรับ/การแทนที่การรีดผิว) ดังนั้นการปรับขนาดยังคงทำงานภายในช่วง [ค่านี้ 100%]\n" +"หากเฟิร์มแวร์ของคุณปิดใช้งานพัดลมที่ต่ำกว่าเกณฑ์อยู่แล้ว (เช่น [พัดลม] off_below ของ Klipper: 0.10 จะปิดพัดลมเมื่อใดก็ตามที่รอบการทำงานที่ได้รับคำสั่งต่ำกว่า 10%) ตัวเลือกนี้และเกณฑ์ของเฟิร์มแวร์ควรได้รับการตั้งค่าตามอุดมคติให้เป็นค่าเดียวกัน การจับคู่พวกมัน (เช่น off_below: 0.10 ใน Klipper และ 10% ที่นี่) รับประกันว่าตัวแบ่งส่วนข้อมูลจะไม่ปล่อยค่าที่ไม่ใช่ศูนย์ซึ่งเฟิร์มแวร์จะดรอปอย่างเงียบๆ และพัดลมจะไม่ได้รับค่าที่ต่ำกว่าค่าที่คุณรู้ว่าสามารถสปูลได้จริง\n" "ตั้งค่าเป็น 0 เพื่อปิดใช้งาน" msgid "%" @@ -15664,15 +13407,8 @@ msgstr "เปิดใช้งานตัวเลือกนี้หาก msgid "Label objects" msgstr "ติดป้ายกำกับวัตถุ" -msgid "" -"Enable this to add comments into the G-code labeling print moves with what " -"object they belong to, which is useful for the Octoprint CancelObject plug-" -"in. This setting is NOT compatible with Single Extruder Multi Material setup " -"and Wipe into Object / Wipe into Infill." -msgstr "" -"เปิดใช้งานสิ่งนี้เพื่อเพิ่มความคิดเห็นลงในการพิมพ์การติดฉลาก G-code ด้วยวัตถุที่อยู่ในนั้น " -"ซึ่งมีประโยชน์สำหรับปลั๊กอิน Octoprint CancelObject การตั้งค่านี้เข้ากันไม่ได้กับการตั้งค่า " -"Single ชุดดันเส้น Multi Material และ Wipe into Object / Wipe into ไส้ใน" +msgid "Enable this to add comments into the G-code labeling print moves with what object they belong to, which is useful for the Octoprint CancelObject plug-in. This setting is NOT compatible with Single Extruder Multi Material setup and Wipe into Object / Wipe into Infill." +msgstr "เปิดใช้งานสิ่งนี้เพื่อเพิ่มความคิดเห็นลงในการพิมพ์การติดฉลาก G-code ด้วยวัตถุที่อยู่ในนั้น ซึ่งมีประโยชน์สำหรับปลั๊กอิน Octoprint CancelObject การตั้งค่านี้เข้ากันไม่ได้กับการตั้งค่า Single ชุดดันเส้น Multi Material และ Wipe into Object / Wipe into ไส้ใน" msgid "Exclude objects" msgstr "ไม่รวมวัตถุ" @@ -15683,95 +13419,44 @@ msgstr "เปิดใช้งานตัวเลือกนี้เพื msgid "Verbose G-code" msgstr "G-code แบบละเอียด" -msgid "" -"Enable this to get a commented G-code file, with each line explained by a " -"descriptive text. If you print from SD card, the additional weight of the " -"file could make your firmware slow down." -msgstr "" -"เปิดใช้งานสิ่งนี้เพื่อรับไฟล์ G-code ที่มีความคิดเห็น โดยแต่ละบรรทัดจะอธิบายด้วยข้อความอธิบาย " -"หากคุณพิมพ์จากการ์ด SD น้ำหนักที่เพิ่มขึ้นของไฟล์อาจทำให้เฟิร์มแวร์ของคุณช้าลง" +msgid "Enable this to get a commented G-code file, with each line explained by a descriptive text. If you print from SD card, the additional weight of the file could make your firmware slow down." +msgstr "เปิดใช้งานสิ่งนี้เพื่อรับไฟล์ G-code ที่มีความคิดเห็น โดยแต่ละบรรทัดจะอธิบายด้วยข้อความอธิบาย หากคุณพิมพ์จากการ์ด SD น้ำหนักที่เพิ่มขึ้นของไฟล์อาจทำให้เฟิร์มแวร์ของคุณช้าลง" msgid "Infill combination" msgstr "ไส้ในชุดค่าผสม" -msgid "" -"Automatically combine sparse infill of several layers to print together in " -"order to reduce time. Walls are still printed with original layer height." -msgstr "" -"รวมไส้ในแบบโปร่งของหลายชั้นโดยอัตโนมัติเพื่อพิมพ์เข้าด้วยกันเพื่อลดเวลา " -"ผนังยังคงพิมพ์ด้วยความสูงชั้นเดิม" +msgid "Automatically combine sparse infill of several layers to print together in order to reduce time. Walls are still printed with original layer height." +msgstr "รวมไส้ในแบบโปร่งของหลายชั้นโดยอัตโนมัติเพื่อพิมพ์เข้าด้วยกันเพื่อลดเวลา ผนังยังคงพิมพ์ด้วยความสูงชั้นเดิม" msgid "Infill shift step" msgstr "ขั้นตอนไส้ในกะ" -msgid "" -"This parameter adds a slight displacement to each layer of infill to create " -"a cross texture." +msgid "This parameter adds a slight displacement to each layer of infill to create a cross texture." msgstr "พารามิเตอร์นี้จะเพิ่มการกระจัดเล็กน้อยในแต่ละชั้นของ ไส้ใน เพื่อสร้างพื้นผิวแบบกากบาท" msgid "Sparse infill rotation template" msgstr "เทมเพลตการหมุน ไส้ใน แบบกระจัดกระจาย" -msgid "" -"Rotate the sparse infill direction per layer using a template of angles. " -"Enter comma-separated degrees (e.g., '0,30,60,90'). Angles are applied in " -"order by layer and repeat when the list ends. Advanced syntax is supported: " -"'+5' rotates +5° every layer; '+5#5' rotates +5° every 5 layers. See the " -"Wiki for details. When a template is set, the standard infill direction " -"setting is ignored. Note: some infill patterns (e.g., Gyroid) control " -"rotation themselves; use with care." -msgstr "" -"หมุนทิศทางไส้ในแบบโปร่งต่อเลเยอร์โดยใช้เทมเพลตมุม ป้อนองศาที่คั่นด้วยเครื่องหมายจุลภาค (เช่น " -"'0,30,60,90') มุมจะถูกนำไปใช้ตามลำดับชั้นและทำซ้ำเมื่อรายการสิ้นสุด รองรับไวยากรณ์ขั้นสูง: " -"'+5' หมุน +5° ทุกเลเยอร์; '+5#5' หมุน +5° ทุกๆ 5 เลเยอร์ ดูวิกิสำหรับรายละเอียด " -"เมื่อตั้งค่าเทมเพลต การตั้งค่าทิศทางไส้ในมาตรฐานจะถูกละเว้น หมายเหตุ: รูปแบบไส้ในบางรูปแบบ " -"(เช่น ไจรอยด์) จะควบคุมการหมุนด้วยตัวเอง ใช้ด้วยความระมัดระวัง" +msgid "Rotate the sparse infill direction per layer using a template of angles. Enter comma-separated degrees (e.g., '0,30,60,90'). Angles are applied in order by layer and repeat when the list ends. Advanced syntax is supported: '+5' rotates +5° every layer; '+5#5' rotates +5° every 5 layers. See the Wiki for details. When a template is set, the standard infill direction setting is ignored. Note: some infill patterns (e.g., Gyroid) control rotation themselves; use with care." +msgstr "หมุนทิศทางไส้ในแบบโปร่งต่อเลเยอร์โดยใช้เทมเพลตมุม ป้อนองศาที่คั่นด้วยเครื่องหมายจุลภาค (เช่น '0,30,60,90') มุมจะถูกนำไปใช้ตามลำดับชั้นและทำซ้ำเมื่อรายการสิ้นสุด รองรับไวยากรณ์ขั้นสูง: '+5' หมุน +5° ทุกเลเยอร์; '+5#5' หมุน +5° ทุกๆ 5 เลเยอร์ ดูวิกิสำหรับรายละเอียด เมื่อตั้งค่าเทมเพลต การตั้งค่าทิศทางไส้ในมาตรฐานจะถูกละเว้น หมายเหตุ: รูปแบบไส้ในบางรูปแบบ (เช่น ไจรอยด์) จะควบคุมการหมุนด้วยตัวเอง ใช้ด้วยความระมัดระวัง" msgid "Solid infill rotation template" msgstr "เทมเพลตการหมุน ไส้ใน ที่เป็นของแข็ง" -msgid "" -"This parameter adds a rotation of solid infill direction to each layer " -"according to the specified template. The template is a comma-separated list " -"of angles in degrees, e.g. '0,90'. The first angle is applied to the first " -"layer, the second angle to the second layer, and so on. If there are more " -"layers than angles, the angles will be repeated. Note that not all solid " -"infill patterns support rotation." -msgstr "" -"พารามิเตอร์นี้จะเพิ่มการหมุนของทิศทางไส้ในแบบทึบลงในแต่ละชั้นตามเทมเพลตที่ระบุ " -"เทมเพลตคือรายการมุมในหน่วยองศาที่คั่นด้วยเครื่องหมายจุลภาค เช่น '0,90'. " -"มุมแรกใช้กับเลเยอร์แรก มุมที่สองกับเลเยอร์ที่สอง และอื่นๆ หากมีชั้นมากกว่ามุม มุมจะเกิดซ้ำ " -"โปรดทราบว่ารูปแบบไส้ในแบบทึบบางรูปแบบไม่รองรับการหมุน" +msgid "This parameter adds a rotation of solid infill direction to each layer according to the specified template. The template is a comma-separated list of angles in degrees, e.g. '0,90'. The first angle is applied to the first layer, the second angle to the second layer, and so on. If there are more layers than angles, the angles will be repeated. Note that not all solid infill patterns support rotation." +msgstr "พารามิเตอร์นี้จะเพิ่มการหมุนของทิศทางไส้ในแบบทึบลงในแต่ละชั้นตามเทมเพลตที่ระบุ เทมเพลตคือรายการมุมในหน่วยองศาที่คั่นด้วยเครื่องหมายจุลภาค เช่น '0,90'. มุมแรกใช้กับเลเยอร์แรก มุมที่สองกับเลเยอร์ที่สอง และอื่นๆ หากมีชั้นมากกว่ามุม มุมจะเกิดซ้ำ โปรดทราบว่ารูปแบบไส้ในแบบทึบบางรูปแบบไม่รองรับการหมุน" msgid "Skeleton infill density" msgstr "ความหนาแน่นของโครงกระดูก" -msgid "" -"The remaining part of the model contour after removing a certain depth from " -"the surface is called the skeleton. This parameter is used to adjust the " -"density of this section. When two regions have the same sparse infill " -"settings but different skeleton densities, their skeleton areas will develop " -"overlapping sections. Default is as same as infill density." -msgstr "" -"ส่วนที่เหลือของรูปร่างของแบบจำลองหลังจากลบความลึกบางส่วนออกจากพื้นผิวแล้วเรียกว่าโครงกระดูก " -"พารามิเตอร์นี้ใช้เพื่อปรับความหนาแน่นของส่วนนี้ เมื่อสองภูมิภาคมีการตั้งค่าไส้ในแบบโปร่งเหมือนกัน " -"แต่มีความหนาแน่นของโครงกระดูกต่างกัน พื้นที่โครงกระดูกของพวกมันจะพัฒนาส่วนที่ทับซ้อนกัน " -"ค่าเริ่มต้นจะเหมือนกับความหนาแน่นของ ไส้ใน" +msgid "The remaining part of the model contour after removing a certain depth from the surface is called the skeleton. This parameter is used to adjust the density of this section. When two regions have the same sparse infill settings but different skeleton densities, their skeleton areas will develop overlapping sections. Default is as same as infill density." +msgstr "ส่วนที่เหลือของรูปร่างของแบบจำลองหลังจากลบความลึกบางส่วนออกจากพื้นผิวแล้วเรียกว่าโครงกระดูก พารามิเตอร์นี้ใช้เพื่อปรับความหนาแน่นของส่วนนี้ เมื่อสองภูมิภาคมีการตั้งค่าไส้ในแบบโปร่งเหมือนกัน แต่มีความหนาแน่นของโครงกระดูกต่างกัน พื้นที่โครงกระดูกของพวกมันจะพัฒนาส่วนที่ทับซ้อนกัน ค่าเริ่มต้นจะเหมือนกับความหนาแน่นของ ไส้ใน" msgid "Skin infill density" msgstr "ไส้ในความหนาแน่นของผิวหนัง" -msgid "" -"The portion of the model's outer surface within a certain depth range is " -"called the skin. This parameter is used to adjust the density of this " -"section. When two regions have the same sparse infill settings but different " -"skin densities, this area will not be split into two separate regions. " -"Default is as same as infill density." -msgstr "" -"ส่วนของพื้นผิวด้านนอกของแบบจำลองภายในช่วงความลึกที่กำหนดเรียกว่าผิวหนัง " -"พารามิเตอร์นี้ใช้เพื่อปรับความหนาแน่นของส่วนนี้ เมื่อสองภูมิภาคมีการตั้งค่าไส้ในแบบโปร่งเหมือนกัน " -"แต่มีความหนาแน่นของผิวหนังต่างกัน พื้นที่นี้จะไม่ถูกแบ่งออกเป็นสองภูมิภาคที่แยกจากกัน " -"ค่าเริ่มต้นจะเหมือนกับความหนาแน่นของ ไส้ใน" +msgid "The portion of the model's outer surface within a certain depth range is called the skin. This parameter is used to adjust the density of this section. When two regions have the same sparse infill settings but different skin densities, this area will not be split into two separate regions. Default is as same as infill density." +msgstr "ส่วนของพื้นผิวด้านนอกของแบบจำลองภายในช่วงความลึกที่กำหนดเรียกว่าผิวหนัง พารามิเตอร์นี้ใช้เพื่อปรับความหนาแน่นของส่วนนี้ เมื่อสองภูมิภาคมีการตั้งค่าไส้ในแบบโปร่งเหมือนกัน แต่มีความหนาแน่นของผิวหนังต่างกัน พื้นที่นี้จะไม่ถูกแบ่งออกเป็นสองภูมิภาคที่แยกจากกัน ค่าเริ่มต้นจะเหมือนกับความหนาแน่นของ ไส้ใน" msgid "Skin infill depth" msgstr "ไส้ในความลึกให้กับผิว" @@ -15800,13 +13485,8 @@ msgstr "ปรับความกว้างของเส้นของเ msgid "Symmetric infill Y axis" msgstr "แกน Y ไส้ในแบบสมมาตร" -msgid "" -"If the model has two parts that are symmetric about the Y axis, and you want " -"these parts to have symmetric textures, please click this option on one of " -"the parts." -msgstr "" -"หากแบบจำลองมีสองส่วนที่สมมาตรเกี่ยวกับแกน Y และคุณต้องการให้ส่วนเหล่านี้มีพื้นผิวที่สมมาตร " -"โปรดคลิกตัวเลือกนี้บนส่วนใดส่วนหนึ่ง" +msgid "If the model has two parts that are symmetric about the Y axis, and you want these parts to have symmetric textures, please click this option on one of the parts." +msgstr "หากแบบจำลองมีสองส่วนที่สมมาตรเกี่ยวกับแกน Y และคุณต้องการให้ส่วนเหล่านี้มีพื้นผิวที่สมมาตร โปรดคลิกตัวเลือกนี้บนส่วนใดส่วนหนึ่ง" msgid "Infill combination - Max layer height" msgstr "ชุดค่าผสม ไส้ใน - ความสูงของชั้นสูงสุด" @@ -15814,25 +13494,19 @@ msgstr "ชุดค่าผสม ไส้ใน - ความสูงข msgid "" "Maximum layer height for the combined sparse infill.\n" "\n" -"Set it to 0 or 100% to use the nozzle diameter (for maximum reduction in " -"print time) or a value of ~80% to maximize sparse infill strength.\n" +"Set it to 0 or 100% to use the nozzle diameter (for maximum reduction in print time) or a value of ~80% to maximize sparse infill strength.\n" "\n" -"The number of layers over which infill is combined is derived by dividing " -"this value with the layer height and rounded down to the nearest decimal.\n" +"The number of layers over which infill is combined is derived by dividing this value with the layer height and rounded down to the nearest decimal.\n" "\n" -"Use either absolute mm values (eg. 0.32mm for a 0.4mm nozzle) or % values " -"(eg 80%). This value must not be larger than the nozzle diameter." +"Use either absolute mm values (eg. 0.32mm for a 0.4mm nozzle) or % values (eg 80%). This value must not be larger than the nozzle diameter." msgstr "" "ความสูงสูงสุดของชั้นสำหรับไส้ในแบบกระจายแบบรวม\n" "\n" -"ตั้งค่าเป็น 0 หรือ 100% เพื่อใช้เส้นผ่านศูนย์กลางหัวฉีด (เพื่อลดเวลาการพิมพ์สูงสุด) หรือค่า ~80% " -"เพื่อเพิ่มความแข็งแรงของวัสดุไส้ในแบบโปร่งให้สูงสุด\n" +"ตั้งค่าเป็น 0 หรือ 100% เพื่อใช้เส้นผ่านศูนย์กลางหัวฉีด (เพื่อลดเวลาการพิมพ์สูงสุด) หรือค่า ~80% เพื่อเพิ่มความแข็งแรงของวัสดุไส้ในแบบโปร่งให้สูงสุด\n" "\n" -"จำนวนเลเยอร์ที่รวม ไส้ใน " -"ได้มาโดยการหารค่านี้ด้วยความสูงของเลเยอร์และปัดเศษลงให้เป็นทศนิยมที่ใกล้ที่สุด\n" +"จำนวนเลเยอร์ที่รวม ไส้ใน ได้มาโดยการหารค่านี้ด้วยความสูงของเลเยอร์และปัดเศษลงให้เป็นทศนิยมที่ใกล้ที่สุด\n" "\n" -"ใช้ค่าสัมบูรณ์ มม. (เช่น 0.32 มม. สำหรับหัวฉีด 0.4 มม.) หรือค่า % (เช่น 80%) " -"ค่านี้ต้องไม่ใหญ่กว่าเส้นผ่านศูนย์กลางของหัวฉีด" +"ใช้ค่าสัมบูรณ์ มม. (เช่น 0.32 มม. สำหรับหัวฉีด 0.4 มม.) หรือค่า % (เช่น 80%) ค่านี้ต้องไม่ใหญ่กว่าเส้นผ่านศูนย์กลางของหัวฉีด" msgid "Enable clumping detection" msgstr "เปิดใช้งานการตรวจจับการจับกันเป็นก้อน" @@ -15856,42 +13530,22 @@ msgstr "" "เส้นพลาสติกสำหรับพิมพ์ไส้ในแบบโปร่งภายใน\n" "\"ค่าเริ่มต้น\" ใช้เส้นพลาสติกของวัตถุ/ชิ้นส่วนที่ใช้งานอยู่" -msgid "" -"Line width of internal sparse infill. If expressed as a %, it will be " -"computed over the nozzle diameter." -msgstr "" -"ความกว้างของเส้นของไส้ในแบบกระจัดกระจายภายใน หากแสดงเป็น % " -"จะคำนวณตามเส้นผ่านศูนย์กลางของหัวฉีด" +msgid "Line width of internal sparse infill. If expressed as a %, it will be computed over the nozzle diameter." +msgstr "ความกว้างของเส้นของไส้ในแบบกระจัดกระจายภายใน หากแสดงเป็น % จะคำนวณตามเส้นผ่านศูนย์กลางของหัวฉีด" msgid "Infill/wall overlap" msgstr "ไส้ในซ้อนทับผนัง" #, no-c-format, no-boost-format -msgid "" -"This allows the infill area to be enlarged slightly to overlap with walls " -"for better bonding. The percentage value is relative to line width of sparse " -"infill. Set this value to ~10-15% to minimize potential over extrusion and " -"accumulation of material resulting in rough top surfaces." -msgstr "" -"พื้นที่ไส้ในจะขยายใหญ่ขึ้นเล็กน้อยเพื่อให้ทับซ้อนกับผนังเพื่อการยึดเกาะที่ดีขึ้น " -"ค่าเปอร์เซ็นต์สัมพันธ์กับความกว้างของเส้นของไส้ในแบบกระจัดกระจาย ตั้งค่านี้เป็น ~10-15% " -"เพื่อลดโอกาสเกิดการอัดขึ้นรูปและการสะสมของวัสดุ ส่งผลให้พื้นผิวด้านบนขรุขระ" +msgid "This allows the infill area to be enlarged slightly to overlap with walls for better bonding. The percentage value is relative to line width of sparse infill. Set this value to ~10-15% to minimize potential over extrusion and accumulation of material resulting in rough top surfaces." +msgstr "พื้นที่ไส้ในจะขยายใหญ่ขึ้นเล็กน้อยเพื่อให้ทับซ้อนกับผนังเพื่อการยึดเกาะที่ดีขึ้น ค่าเปอร์เซ็นต์สัมพันธ์กับความกว้างของเส้นของไส้ในแบบกระจัดกระจาย ตั้งค่านี้เป็น ~10-15% เพื่อลดโอกาสเกิดการอัดขึ้นรูปและการสะสมของวัสดุ ส่งผลให้พื้นผิวด้านบนขรุขระ" msgid "Top/Bottom solid infill/wall overlap" msgstr "ด้านบน/ด้านล่างของช่องทึบ/ผนังทับซ้อนกัน" #, no-c-format, no-boost-format -msgid "" -"Top solid infill area is enlarged slightly to overlap with wall for better " -"bonding and to minimize the appearance of pinholes where the top infill " -"meets the walls. A value of 25-30% is a good starting point, minimizing the " -"appearance of pinholes. The percentage value is relative to line width of " -"sparse infill." -msgstr "" -"พื้นที่ไส้ในของแข็งด้านบนจะถูกขยายให้ใหญ่ขึ้นเล็กน้อยเพื่อให้ทับซ้อนกับผนังเพื่อการยึดเกาะที่ดีขึ้น " -"และเพื่อลดการเกิดรูเข็มที่ซึ่งอินฟิลด้านบนบรรจบกับผนัง ค่า 25-30% ถือเป็นจุดเริ่มต้นที่ดี " -"โดยช่วยลดการเกิดรูเข็มให้เหลือน้อยที่สุด " -"ค่าเปอร์เซ็นต์สัมพันธ์กับความกว้างของเส้นของไส้ในแบบกระจัดกระจาย" +msgid "Top solid infill area is enlarged slightly to overlap with wall for better bonding and to minimize the appearance of pinholes where the top infill meets the walls. A value of 25-30% is a good starting point, minimizing the appearance of pinholes. The percentage value is relative to line width of sparse infill." +msgstr "พื้นที่ไส้ในของแข็งด้านบนจะถูกขยายให้ใหญ่ขึ้นเล็กน้อยเพื่อให้ทับซ้อนกับผนังเพื่อการยึดเกาะที่ดีขึ้น และเพื่อลดการเกิดรูเข็มที่ซึ่งอินฟิลด้านบนบรรจบกับผนัง ค่า 25-30% ถือเป็นจุดเริ่มต้นที่ดี โดยช่วยลดการเกิดรูเข็มให้เหลือน้อยที่สุด ค่าเปอร์เซ็นต์สัมพันธ์กับความกว้างของเส้นของไส้ในแบบกระจัดกระจาย" msgid "This is the speed for internal sparse infill." msgstr "ความเร็วของ ไส้ใน เบาบางภายใน" @@ -15905,45 +13559,26 @@ msgstr "ชื่อของโปรไฟล์ผู้ปกครอง" msgid "Interface shells" msgstr "เชลล์อินเตอร์เฟส" -msgid "" -"Force the generation of solid shells between adjacent materials/volumes. " -"Useful for multi-extruder prints with translucent materials or manual " -"soluble support material." -msgstr "" -"บังคับการสร้างเปลือกแข็งระหว่างวัสดุ/ปริมาตรที่อยู่ติดกัน " -"มีประโยชน์สำหรับการพิมพ์แบบหลายชุดดันเส้นด้วยวัสดุโปร่งแสงหรือวัสดุรองรับที่ละลายน้ำได้ด้วยตนเอง" +msgid "Force the generation of solid shells between adjacent materials/volumes. Useful for multi-extruder prints with translucent materials or manual soluble support material." +msgstr "บังคับการสร้างเปลือกแข็งระหว่างวัสดุ/ปริมาตรที่อยู่ติดกัน มีประโยชน์สำหรับการพิมพ์แบบหลายชุดดันเส้นด้วยวัสดุโปร่งแสงหรือวัสดุรองรับที่ละลายน้ำได้ด้วยตนเอง" msgid "Maximum width of a segmented region" msgstr "ความกว้างสูงสุดของขอบเขตที่แบ่งส่วน" -msgid "" -"Maximum width of a segmented region. A value of 0 disables this feature." +msgid "Maximum width of a segmented region. A value of 0 disables this feature." msgstr "ความกว้างสูงสุดของขอบเขตที่แบ่งส่วน ศูนย์ปิดใช้งานคุณลักษณะนี้" msgid "Interlocking depth of a segmented region" msgstr "ความลึกที่ประสานกันของภูมิภาคที่แบ่งส่วน" -msgid "" -"Interlocking depth of a segmented region. It will be ignored if " -"\"mmu_segmented_region_max_width\" is zero or if " -"\"mmu_segmented_region_interlocking_depth\" is bigger than " -"\"mmu_segmented_region_max_width\". Zero disables this feature." -msgstr "" -"ความลึกที่ประสานกันของภูมิภาคที่แบ่งส่วน ระบบจะไม่สนใจหาก " -"\"mmu_segmented_region_max_width\" เป็นศูนย์ หรือหาก " -"\"mmu_segmented_region_interlocking_detail\" ใหญ่กว่า " -"\"mmu_segmented_region_max_width\" ศูนย์ปิดใช้งานคุณลักษณะนี้" +msgid "Interlocking depth of a segmented region. It will be ignored if \"mmu_segmented_region_max_width\" is zero or if \"mmu_segmented_region_interlocking_depth\" is bigger than \"mmu_segmented_region_max_width\". Zero disables this feature." +msgstr "ความลึกที่ประสานกันของภูมิภาคที่แบ่งส่วน ระบบจะไม่สนใจหาก \"mmu_segmented_region_max_width\" เป็นศูนย์ หรือหาก \"mmu_segmented_region_interlocking_detail\" ใหญ่กว่า \"mmu_segmented_region_max_width\" ศูนย์ปิดใช้งานคุณลักษณะนี้" msgid "Use beam interlocking" msgstr "ใช้การประสานลำแสง" -msgid "" -"Generate interlocking beam structure at the locations where different " -"filaments touch. This improves the adhesion between filaments, especially " -"models printed in different materials." -msgstr "" -"สร้างโครงสร้างลำแสงที่เชื่อมต่อกันในตำแหน่งที่เส้นพลาสติกต่างๆ สัมผัสกัน " -"ซึ่งจะช่วยปรับปรุงการยึดเกาะระหว่างเส้นพลาสติก โดยเฉพาะรุ่นที่พิมพ์ด้วยวัสดุที่แตกต่างกัน" +msgid "Generate interlocking beam structure at the locations where different filaments touch. This improves the adhesion between filaments, especially models printed in different materials." +msgstr "สร้างโครงสร้างลำแสงที่เชื่อมต่อกันในตำแหน่งที่เส้นพลาสติกต่างๆ สัมผัสกัน ซึ่งจะช่วยปรับปรุงการยึดเกาะระหว่างเส้นพลาสติก โดยเฉพาะรุ่นที่พิมพ์ด้วยวัสดุที่แตกต่างกัน" msgid "Interlocking beam width" msgstr "ความกว้างของลำแสงที่เชื่อมต่อกัน" @@ -15960,40 +13595,26 @@ msgstr "การวางแนวของคานลูกโซ่" msgid "Interlocking beam layers" msgstr "ชั้นลำแสงที่เชื่อมต่อกัน" -msgid "" -"The height of the beams of the interlocking structure, measured in number of " -"layers. Less layers is stronger, but more prone to defects." -msgstr "" -"ความสูงของคานของโครงสร้างที่เชื่อมต่อกัน วัดเป็นจำนวนชั้น ชั้นที่น้อยลงจะแข็งแกร่งขึ้น " -"แต่มีแนวโน้มที่จะเกิดข้อบกพร่องมากขึ้น" +msgid "The height of the beams of the interlocking structure, measured in number of layers. Less layers is stronger, but more prone to defects." +msgstr "ความสูงของคานของโครงสร้างที่เชื่อมต่อกัน วัดเป็นจำนวนชั้น ชั้นที่น้อยลงจะแข็งแกร่งขึ้น แต่มีแนวโน้มที่จะเกิดข้อบกพร่องมากขึ้น" msgid "Interlocking depth" msgstr "ความลึกที่ประสานกัน" -msgid "" -"The distance from the boundary between filaments to generate interlocking " -"structure, measured in cells. Too few cells will result in poor adhesion." -msgstr "" -"ระยะห่างจากขอบเขตระหว่างเส้นพลาสติกเพื่อสร้างโครงสร้างที่เชื่อมต่อกัน วัดในเซลล์ " -"จำนวนเซลล์ที่มากเกินไปจะทำให้การยึดเกาะไม่ดี" +msgid "The distance from the boundary between filaments to generate interlocking structure, measured in cells. Too few cells will result in poor adhesion." +msgstr "ระยะห่างจากขอบเขตระหว่างเส้นพลาสติกเพื่อสร้างโครงสร้างที่เชื่อมต่อกัน วัดในเซลล์ จำนวนเซลล์ที่มากเกินไปจะทำให้การยึดเกาะไม่ดี" msgid "Interlocking boundary avoidance" msgstr "การหลีกเลี่ยงขอบเขตที่ประสานกัน" -msgid "" -"The distance from the outside of a model where interlocking structures will " -"not be generated, measured in cells." +msgid "The distance from the outside of a model where interlocking structures will not be generated, measured in cells." msgstr "ระยะห่างจากด้านนอกของแบบจำลองซึ่งจะไม่สร้างโครงสร้างที่เชื่อมต่อกัน โดยวัดเป็นเซลล์" msgid "Ironing type" msgstr "ชนิดการรีดผิว" -msgid "" -"Ironing uses a small flow to print at the same height of a surface to make " -"flat surfaces smoother. This setting controls which layers are being ironed." -msgstr "" -"การรีดผิวคือการใช้การไหลเล็กน้อยเพื่อพิมพ์บนพื้นผิวที่มีความสูงเท่ากันอีกครั้งเพื่อให้พื้นผิวเรียบเรียบยิ่งขึ้น " -"การตั้งค่านี้ควบคุมว่าชั้นใดที่จะรีด" +msgid "Ironing uses a small flow to print at the same height of a surface to make flat surfaces smoother. This setting controls which layers are being ironed." +msgstr "การรีดผิวคือการใช้การไหลเล็กน้อยเพื่อพิมพ์บนพื้นผิวที่มีความสูงเท่ากันอีกครั้งเพื่อให้พื้นผิวเรียบเรียบยิ่งขึ้น การตั้งค่านี้ควบคุมว่าชั้นใดที่จะรีด" msgid "No ironing" msgstr "ไม่รีดผิว" @@ -16013,20 +13634,13 @@ msgstr "รูปแบบการรีดผิว" msgid "The pattern that will be used when ironing." msgstr "รูปแบบที่จะใช้ในการรีด" -msgid "" -"This is the amount of material to be extruded during ironing. It is relative " -"to the flow of normal layer height. Too high a value will result in " -"overextrusion on the surface." -msgstr "" -"ปริมาณวัสดุที่จะพ่นออกมาระหว่างรีดผิว สัมพันธ์กับการไหลของความสูงของชั้นปกติ " -"ค่าที่สูงเกินไปส่งผลให้เกิดการอัดขึ้นรูปมากเกินไปบนพื้นผิว" +msgid "This is the amount of material to be extruded during ironing. It is relative to the flow of normal layer height. Too high a value will result in overextrusion on the surface." +msgstr "ปริมาณวัสดุที่จะพ่นออกมาระหว่างรีดผิว สัมพันธ์กับการไหลของความสูงของชั้นปกติ ค่าที่สูงเกินไปส่งผลให้เกิดการอัดขึ้นรูปมากเกินไปบนพื้นผิว" msgid "This is the distance between the lines used for ironing." msgstr "ระยะห่างระหว่างเส้นรีดผิว" -msgid "" -"The distance to keep from the edges. A value of 0 sets this to half of the " -"nozzle diameter." +msgid "The distance to keep from the edges. A value of 0 sets this to half of the nozzle diameter." msgstr "ระยะห่างที่ต้องเก็บจากขอบ ค่า 0 ตั้งค่านี้เป็นครึ่งหนึ่งของเส้นผ่านศูนย์กลางหัวฉีด" msgid "This is the print speed for ironing lines." @@ -16093,9 +13707,7 @@ msgstr "G-code การตรวจจับการจับตัวเป msgid "Silent Mode" msgstr "โหมดเงียบ" -msgid "" -"Whether the machine supports silent mode in which machine uses lower " -"acceleration to print more quietly" +msgid "Whether the machine supports silent mode in which machine uses lower acceleration to print more quietly" msgstr "เครื่องรองรับโหมดเงียบซึ่งเครื่องใช้ความเร่งในการพิมพ์ต่ำกว่าหรือไม่" msgid "Emit limits to G-code" @@ -16111,12 +13723,8 @@ msgstr "" "หากเปิดใช้งาน ขีดจำกัดของเครื่องจะถูกส่งไปยังไฟล์ G-code\n" "ตัวเลือกนี้จะถูกละเว้นหากตั้งค่ารสชาติ G-code เป็น Klipper" -msgid "" -"This G-code will be used as a code for the pause print. Users can insert " -"pause G-code in the G-code viewer." -msgstr "" -"G-code นี้จะใช้เป็นรหัสสำหรับการหยุดการพิมพ์ชั่วคราว ผู้ใช้สามารถแทรก G-code " -"หยุดชั่วคราวในโปรแกรมดู G-code" +msgid "This G-code will be used as a code for the pause print. Users can insert pause G-code in the G-code viewer." +msgstr "G-code นี้จะใช้เป็นรหัสสำหรับการหยุดการพิมพ์ชั่วคราว ผู้ใช้สามารถแทรก G-code หยุดชั่วคราวในโปรแกรมดู G-code" msgid "This G-code will be used as a custom code." msgstr "รหัส G นี้จะใช้เป็นรหัสที่กำหนดเอง" @@ -16130,15 +13738,8 @@ msgstr "เปิดใช้งานการชดเชยการไหล msgid "Flow Compensation Model" msgstr "แบบจำลองการชดเชยการไหล" -msgid "" -"Flow Compensation Model, used to adjust the flow for small infill areas. The " -"model is expressed as a comma separated pair of values for extrusion length " -"and flow correction factor. Each pair is on a separate line, followed by a " -"semicolon, in the following format: \"1.234, 5.678;\"" -msgstr "" -"Flow Compensation Model ใช้เพื่อปรับการไหลสำหรับพื้นที่ไส้ในขนาดเล็ก " -"แบบจำลองนี้จะแสดงเป็นคู่ของค่าที่คั่นด้วยเครื่องหมายจุลภาคสำหรับความยาวการอัดขึ้นรูปและปัจจัยการแก้ไขการไหล " -"แต่ละคู่อยู่บนบรรทัดแยกกัน ตามด้วยอัฒภาค ในรูปแบบต่อไปนี้: \"1.234, 5.678;\"" +msgid "Flow Compensation Model, used to adjust the flow for small infill areas. The model is expressed as a comma separated pair of values for extrusion length and flow correction factor. Each pair is on a separate line, followed by a semicolon, in the following format: \"1.234, 5.678;\"" +msgstr "Flow Compensation Model ใช้เพื่อปรับการไหลสำหรับพื้นที่ไส้ในขนาดเล็ก แบบจำลองนี้จะแสดงเป็นคู่ของค่าที่คั่นด้วยเครื่องหมายจุลภาคสำหรับความยาวการอัดขึ้นรูปและปัจจัยการแก้ไขการไหล แต่ละคู่อยู่บนบรรทัดแยกกัน ตามด้วยอัฒภาค ในรูปแบบต่อไปนี้: \"1.234, 5.678;\"" msgid "Maximum speed X" msgstr "ความเร็วสูงสุด X" @@ -16216,8 +13817,7 @@ msgid "Maximum Junction Deviation" msgstr "ส่วนเบี่ยงเบนทางแยกสูงสุด" msgid "" -"Maximum junction deviation (M205 J, only apply if JD > 0 for Marlin " -"Firmware\n" +"Maximum junction deviation (M205 J, only apply if JD > 0 for Marlin Firmware\n" "If your Marlin 2 printer uses Classic Jerk set this value to 0.)" msgstr "" "ค่าเบี่ยงเบนจุดเชื่อมต่อสูงสุด (M205 J ใช้เฉพาะเมื่อ JD > 0 สำหรับเฟิร์มแวร์ Marlin\n" @@ -16257,12 +13857,10 @@ msgid "Resonance avoidance" msgstr "การหลีกเลี่ยงการสั่นพ้อง" msgid "" -"By reducing the speed of the outer wall to avoid the resonance zone of the " -"printer, ringing on the surface of the model are avoided.\n" +"By reducing the speed of the outer wall to avoid the resonance zone of the printer, ringing on the surface of the model are avoided.\n" "Please turn this option off when testing ringing." msgstr "" -"โดยการลดความเร็วของผนังด้านนอกเพื่อหลีกเลี่ยงโซนสั่นพ้องของเครื่องพิมพ์ " -"หลีกเลี่ยงรอยคลื่นบนพื้นผิวของโมเดล\n" +"โดยการลดความเร็วของผนังด้านนอกเพื่อหลีกเลี่ยงโซนสั่นพ้องของเครื่องพิมพ์ หลีกเลี่ยงรอยคลื่นบนพื้นผิวของโมเดล\n" "โปรดปิดตัวเลือกนี้เมื่อทดสอบการสั่นพ้อง" msgid "Min" @@ -16378,72 +13976,39 @@ msgstr "" "ศูนย์จะใช้อัตราส่วนการทำให้หมาด ๆ ของเฟิร์มแวร์\n" "หากต้องการปิดใช้งานการสร้างอินพุต ให้ใช้ประเภทปิดใช้งาน" -msgid "" -"The part cooling fan speed may be increased when auto cooling is enabled. " -"This is the maximum speed for the part cooling fan." -msgstr "" -"ความเร็วพัดลมระบายความร้อนบางส่วนอาจเพิ่มขึ้นเมื่อเปิดใช้งานการระบายความร้อนอัตโนมัติ " -"นี่คือความเร็วสูงสุดสำหรับพัดลมระบายความร้อนของชิ้นส่วน" +msgid "The part cooling fan speed may be increased when auto cooling is enabled. This is the maximum speed for the part cooling fan." +msgstr "ความเร็วพัดลมระบายความร้อนบางส่วนอาจเพิ่มขึ้นเมื่อเปิดใช้งานการระบายความร้อนอัตโนมัติ นี่คือความเร็วสูงสุดสำหรับพัดลมระบายความร้อนของชิ้นส่วน" -msgid "" -"The highest printable layer height for the extruder. Used to limit the " -"maximum layer height when enable adaptive layer height." -msgstr "" -"ความสูงของชั้นสูงสุดที่สามารถพิมพ์ได้สำหรับชุดดันเส้น " -"ใช้เพื่อจำกัดความสูงของเลเยอร์สูงสุดเมื่อเปิดใช้งานความสูงของเลเยอร์แบบปรับได้" +msgid "The highest printable layer height for the extruder. Used to limit the maximum layer height when enable adaptive layer height." +msgstr "ความสูงของชั้นสูงสุดที่สามารถพิมพ์ได้สำหรับชุดดันเส้น ใช้เพื่อจำกัดความสูงของเลเยอร์สูงสุดเมื่อเปิดใช้งานความสูงของเลเยอร์แบบปรับได้" msgid "Extrusion rate smoothing" msgstr "อัตราการอัดรีดให้เรียบ" msgid "" -"This parameter smooths out sudden extrusion rate changes that happen when " -"the printer transitions from printing a high flow (high speed/larger width) " -"extrusion to a lower flow (lower speed/smaller width) extrusion and vice " -"versa.\n" +"This parameter smooths out sudden extrusion rate changes that happen when the printer transitions from printing a high flow (high speed/larger width) extrusion to a lower flow (lower speed/smaller width) extrusion and vice versa.\n" "\n" -"It defines the maximum rate by which the extruded volumetric flow in mm³/s " -"can change over time. Higher values mean higher extrusion rate changes are " -"allowed, resulting in faster speed transitions.\n" +"It defines the maximum rate by which the extruded volumetric flow in mm³/s can change over time. Higher values mean higher extrusion rate changes are allowed, resulting in faster speed transitions.\n" "\n" "A value of 0 disables the feature.\n" "\n" -"For a high speed, high flow direct drive printer (like the Bambu lab or " -"Voron) this value is usually not needed. However it can provide some " -"marginal benefit in certain cases where feature speeds vary greatly. For " -"example, when there are aggressive slowdowns due to overhangs. In these " -"cases a high value of around 300-350 mm³/s² is recommended as this allows " -"for just enough smoothing to assist pressure advance achieve a smoother flow " -"transition.\n" +"For a high speed, high flow direct drive printer (like the Bambu lab or Voron) this value is usually not needed. However it can provide some marginal benefit in certain cases where feature speeds vary greatly. For example, when there are aggressive slowdowns due to overhangs. In these cases a high value of around 300-350 mm³/s² is recommended as this allows for just enough smoothing to assist pressure advance achieve a smoother flow transition.\n" "\n" -"For slower printers without pressure advance, the value should be set much " -"lower. A value of 10-15 mm³/s² is a good starting point for direct drive " -"extruders and 5-10 mm³/s² for Bowden style.\n" +"For slower printers without pressure advance, the value should be set much lower. A value of 10-15 mm³/s² is a good starting point for direct drive extruders and 5-10 mm³/s² for Bowden style.\n" "\n" "This feature is known as Pressure Equalizer in Prusa slicer.\n" "\n" "Note: this parameter disables arc fitting." msgstr "" -"พารามิเตอร์นี้จะทำให้การเปลี่ยนแปลงอัตราการอัดขึ้นรูปกะทันหันที่เกิดขึ้นเมื่อเครื่องพิมพ์เปลี่ยนจากการพิมพ์ที่มีอัตราการไหลสูง " -"(ความเร็วสูง/ความกว้างมากขึ้น) ไปเป็นการไหลที่ต่ำกว่า (ความเร็วต่ำ/ความกว้างน้อยลง) " -"และในทางกลับกัน\n" +"พารามิเตอร์นี้จะทำให้การเปลี่ยนแปลงอัตราการอัดขึ้นรูปกะทันหันที่เกิดขึ้นเมื่อเครื่องพิมพ์เปลี่ยนจากการพิมพ์ที่มีอัตราการไหลสูง (ความเร็วสูง/ความกว้างมากขึ้น) ไปเป็นการไหลที่ต่ำกว่า (ความเร็วต่ำ/ความกว้างน้อยลง) และในทางกลับกัน\n" "\n" -"โดยจะกำหนดอัตราสูงสุดที่การไหลเชิงปริมาตรที่อัดออกมาในหน่วย mm³/s " -"สามารถเปลี่ยนแปลงได้ตลอดเวลา " -"ค่าที่สูงกว่าหมายถึงอนุญาตให้มีการเปลี่ยนแปลงอัตราการอัดขึ้นรูปที่สูงขึ้น " -"ส่งผลให้การเปลี่ยนความเร็วเร็วขึ้น\n" +"โดยจะกำหนดอัตราสูงสุดที่การไหลเชิงปริมาตรที่อัดออกมาในหน่วย mm³/s สามารถเปลี่ยนแปลงได้ตลอดเวลา ค่าที่สูงกว่าหมายถึงอนุญาตให้มีการเปลี่ยนแปลงอัตราการอัดขึ้นรูปที่สูงขึ้น ส่งผลให้การเปลี่ยนความเร็วเร็วขึ้น\n" "\n" "ค่า 0 จะปิดใช้งานคุณลักษณะนี้\n" "\n" -"สำหรับเครื่องพิมพ์แบบขับเคลื่อนโดยตรงความเร็วสูงที่มีอัตราการไหลสูง (เช่น ห้องปฏิบัติการ Bambu " -"หรือ Voron) โดยปกติไม่จำเป็นต้องใช้ค่านี้ อย่างไรก็ตาม " -"มันสามารถให้ประโยชน์เล็กน้อยในบางกรณีที่ความเร็วของคุณสมบัติแตกต่างกันมาก ตัวอย่างเช่น " -"เมื่อมีการชะลอตัวอย่างรุนแรงเนื่องจากการโอเวอร์แฮงค์ ในกรณีเหล่านี้ แนะนำให้ใช้ค่าที่สูงประมาณ " -"300-350 mm³/s² เนื่องจากจะทำให้มีการปรับให้เรียบเพียงพอเพื่อช่วยให้Pressure " -"Advanceบรรลุการเปลี่ยนแปลงการไหลที่ราบรื่นยิ่งขึ้น\n" +"สำหรับเครื่องพิมพ์แบบขับเคลื่อนโดยตรงความเร็วสูงที่มีอัตราการไหลสูง (เช่น ห้องปฏิบัติการ Bambu หรือ Voron) โดยปกติไม่จำเป็นต้องใช้ค่านี้ อย่างไรก็ตาม มันสามารถให้ประโยชน์เล็กน้อยในบางกรณีที่ความเร็วของคุณสมบัติแตกต่างกันมาก ตัวอย่างเช่น เมื่อมีการชะลอตัวอย่างรุนแรงเนื่องจากการโอเวอร์แฮงค์ ในกรณีเหล่านี้ แนะนำให้ใช้ค่าที่สูงประมาณ 300-350 mm³/s² เนื่องจากจะทำให้มีการปรับให้เรียบเพียงพอเพื่อช่วยให้Pressure Advanceบรรลุการเปลี่ยนแปลงการไหลที่ราบรื่นยิ่งขึ้น\n" "\n" -"สำหรับเครื่องพิมพ์ที่ช้ากว่าซึ่งไม่มีการดันล่วงหน้า ควรตั้งค่าให้ต่ำลงมาก ค่า 10-15 มม./วินาที² " -"เป็นจุดเริ่มต้นที่ดีสำหรับชุดดันเส้นแบบขับเคลื่อนโดยตรง และ 5-10 มม./วินาที² สำหรับรูปแบบ " -"Bowden\n" +"สำหรับเครื่องพิมพ์ที่ช้ากว่าซึ่งไม่มีการดันล่วงหน้า ควรตั้งค่าให้ต่ำลงมาก ค่า 10-15 มม./วินาที² เป็นจุดเริ่มต้นที่ดีสำหรับชุดดันเส้นแบบขับเคลื่อนโดยตรง และ 5-10 มม./วินาที² สำหรับรูปแบบ Bowden\n" "\n" "คุณลักษณะนี้เรียกว่า Pressure Equalizer ในตัวแบ่งส่วนข้อมูล Prusa\n" "\n" @@ -16456,50 +14021,33 @@ msgid "Smoothing segment length" msgstr "ความยาวส่วนที่เรียบ" msgid "" -"A lower value results in smoother extrusion rate transitions. However, this " -"results in a significantly larger G-code file and more instructions for the " -"printer to process.\n" +"A lower value results in smoother extrusion rate transitions. However, this results in a significantly larger G-code file and more instructions for the printer to process.\n" "\n" -"Default value of 3 works well for most cases. If your printer is stuttering, " -"increase this value to reduce the number of adjustments made.\n" +"Default value of 3 works well for most cases. If your printer is stuttering, increase this value to reduce the number of adjustments made.\n" "\n" "Allowed values: 0.5-5" msgstr "" -"ค่าที่ต่ำกว่าส่งผลให้อัตราการอัดรีดเปลี่ยนผ่านได้เรียบขึ้น อย่างไรก็ตาม ไฟล์ G-code " -"จะมีขนาดใหญ่ขึ้นอย่างเห็นได้ชัดและมีคำสั่งให้เครื่องพิมพ์ประมวลผลมากขึ้น\n" +"ค่าที่ต่ำกว่าส่งผลให้อัตราการอัดรีดเปลี่ยนผ่านได้เรียบขึ้น อย่างไรก็ตาม ไฟล์ G-code จะมีขนาดใหญ่ขึ้นอย่างเห็นได้ชัดและมีคำสั่งให้เครื่องพิมพ์ประมวลผลมากขึ้น\n" "\n" -"ค่าเริ่มต้นของ 3 ทำงานได้ดีในกรณีส่วนใหญ่ หากเครื่องพิมพ์ของคุณกระตุก " -"ให้เพิ่มค่านี้เพื่อลดจำนวนการปรับเปลี่ยน\n" +"ค่าเริ่มต้นของ 3 ทำงานได้ดีในกรณีส่วนใหญ่ หากเครื่องพิมพ์ของคุณกระตุก ให้เพิ่มค่านี้เพื่อลดจำนวนการปรับเปลี่ยน\n" "\n" "ค่าที่อนุญาต: 0.5-5" msgid "Apply only on external features" msgstr "ใช้กับคุณสมบัติภายนอกเท่านั้น" -msgid "" -"Applies extrusion rate smoothing only on external perimeters and overhangs. " -"This can help reduce artefacts due to sharp speed transitions on externally " -"visible overhangs without impacting the print speed of features that will " -"not be visible to the user." -msgstr "" -"ใช้การปรับให้เรียบด้วยอัตราการอัดขึ้นรูปเฉพาะกับขอบด้านนอกและส่วนยื่นเท่านั้น " -"วิธีนี้สามารถช่วยลดสิ่งแปลกปลอมเนื่องจากการเปลี่ยนความเร็วที่คมชัดบนส่วนยื่นซึ่งมองเห็นได้จากภายนอก " -"โดยไม่ส่งผลกระทบต่อความเร็วในการพิมพ์ของคุณลักษณะที่ผู้ใช้จะมองไม่เห็น" +msgid "Applies extrusion rate smoothing only on external perimeters and overhangs. This can help reduce artefacts due to sharp speed transitions on externally visible overhangs without impacting the print speed of features that will not be visible to the user." +msgstr "ใช้การปรับให้เรียบด้วยอัตราการอัดขึ้นรูปเฉพาะกับขอบด้านนอกและส่วนยื่นเท่านั้น วิธีนี้สามารถช่วยลดสิ่งแปลกปลอมเนื่องจากการเปลี่ยนความเร็วที่คมชัดบนส่วนยื่นซึ่งมองเห็นได้จากภายนอก โดยไม่ส่งผลกระทบต่อความเร็วในการพิมพ์ของคุณลักษณะที่ผู้ใช้จะมองไม่เห็น" msgid "Minimum speed for part cooling fan." msgstr "ความเร็วขั้นต่ำสำหรับพัดลมระบายความร้อนชิ้นส่วน" msgid "" -"Speed of auxiliary part cooling fan. Auxiliary fan will run at this speed " -"during printing except the first several layers which is defined by no " -"cooling layers.\n" -"Please enable auxiliary_fan in printer settings to use this feature. G-code " -"command: M106 P2 S(0-255)" +"Speed of auxiliary part cooling fan. Auxiliary fan will run at this speed during printing except the first several layers which is defined by no cooling layers.\n" +"Please enable auxiliary_fan in printer settings to use this feature. G-code command: M106 P2 S(0-255)" msgstr "" -"ความเร็วของพัดลมระบายความร้อนชิ้นส่วนเสริม พัดลมเสริมจะทำงานที่ความเร็วนี้ในระหว่างการพิมพ์ " -"ยกเว้นหลายชั้นแรกซึ่งถูกกำหนดโดยไม่มีชั้นระบายความร้อน\n" -"โปรดเปิดใช้งาน auxiliary_fan ในการตั้งค่าเครื่องพิมพ์เพื่อใช้คุณสมบัตินี้ คำสั่งรหัส G: M106 P2 " -"S(0-255)" +"ความเร็วของพัดลมระบายความร้อนชิ้นส่วนเสริม พัดลมเสริมจะทำงานที่ความเร็วนี้ในระหว่างการพิมพ์ ยกเว้นหลายชั้นแรกซึ่งถูกกำหนดโดยไม่มีชั้นระบายความร้อน\n" +"โปรดเปิดใช้งาน auxiliary_fan ในการตั้งค่าเครื่องพิมพ์เพื่อใช้คุณสมบัตินี้ คำสั่งรหัส G: M106 P2 S(0-255)" msgid "For the first" msgstr "สำหรับครั้งแรก" @@ -16508,38 +14056,23 @@ msgid "Set special auxiliary cooling fan for the first certain layers." msgstr "ตั้งพัดลมระบายความร้อนเสริมพิเศษสำหรับบางชั้นแรก" msgid "" -"Auxiliary fan speed will be ramped up linearly from layer \"For the first\" " -"to maximum at layer \"Full fan speed at layer\".\n" -"\"Full fan speed at layer\" will be ignored if lower than \"For the first\", " -"in which case the fan will run at maximum allowed speed at layer \"For the " -"first\" + 1." +"Auxiliary fan speed will be ramped up linearly from layer \"For the first\" to maximum at layer \"Full fan speed at layer\".\n" +"\"Full fan speed at layer\" will be ignored if lower than \"For the first\", in which case the fan will run at maximum allowed speed at layer \"For the first\" + 1." msgstr "" -"ความเร็วพัดลมเสริมจะเพิ่มขึ้นเชิงเส้นจากเลเยอร์ \"สำหรับอันแรก\" ไปจนถึงสูงสุดที่เลเยอร์ " -"\"ความเร็วพัดลมเต็มในเลเยอร์\"\n" -"\"ความเร็วพัดลมเต็มที่เลเยอร์\" จะถูกละเว้นหากต่ำกว่า \"สำหรับครั้งแรก\" ซึ่งในกรณีนี้ " -"พัดลมจะทำงานที่ความเร็วสูงสุดที่อนุญาตที่เลเยอร์ \"สำหรับครั้งแรก\" + 1" +"ความเร็วพัดลมเสริมจะเพิ่มขึ้นเชิงเส้นจากเลเยอร์ \"สำหรับอันแรก\" ไปจนถึงสูงสุดที่เลเยอร์ \"ความเร็วพัดลมเต็มในเลเยอร์\"\n" +"\"ความเร็วพัดลมเต็มที่เลเยอร์\" จะถูกละเว้นหากต่ำกว่า \"สำหรับครั้งแรก\" ซึ่งในกรณีนี้ พัดลมจะทำงานที่ความเร็วสูงสุดที่อนุญาตที่เลเยอร์ \"สำหรับครั้งแรก\" + 1" -msgid "" -"Special auxiliary cooling fan speed, effective only for the first x layers." +msgid "Special auxiliary cooling fan speed, effective only for the first x layers." msgstr "ความเร็วพัดลมระบายความร้อนเสริมพิเศษ มีผลเฉพาะ x เลเยอร์แรกเท่านั้น" -msgid "" -"The lowest printable layer height for the extruder. Used to limit the " -"minimum layer height when enable adaptive layer height." -msgstr "" -"ความสูงของชั้นต่ำสุดที่สามารถพิมพ์ได้สำหรับชุดดันเส้น " -"ใช้เพื่อจำกัดความสูงของเลเยอร์ขั้นต่ำเมื่อเปิดใช้งานความสูงของเลเยอร์แบบปรับได้" +msgid "The lowest printable layer height for the extruder. Used to limit the minimum layer height when enable adaptive layer height." +msgstr "ความสูงของชั้นต่ำสุดที่สามารถพิมพ์ได้สำหรับชุดดันเส้น ใช้เพื่อจำกัดความสูงของเลเยอร์ขั้นต่ำเมื่อเปิดใช้งานความสูงของเลเยอร์แบบปรับได้" msgid "Min print speed" msgstr "ความเร็วในการพิมพ์ขั้นต่ำ" -msgid "" -"The minimum print speed to which the printer slows down to maintain the " -"minimum layer time defined above when the slowdown for better layer cooling " -"is enabled." -msgstr "" -"ความเร็วการพิมพ์ขั้นต่ำที่เครื่องพิมพ์ช้าลง เพื่อรักษาเวลาเลเยอร์ขั้นต่ำที่กำหนดไว้ข้างต้น " -"เมื่อเปิดใช้งานการชะลอตัวเพื่อการระบายความร้อนของเลเยอร์ที่ดีขึ้น" +msgid "The minimum print speed to which the printer slows down to maintain the minimum layer time defined above when the slowdown for better layer cooling is enabled." +msgstr "ความเร็วการพิมพ์ขั้นต่ำที่เครื่องพิมพ์ช้าลง เพื่อรักษาเวลาเลเยอร์ขั้นต่ำที่กำหนดไว้ข้างต้น เมื่อเปิดใช้งานการชะลอตัวเพื่อการระบายความร้อนของเลเยอร์ที่ดีขึ้น" msgid "The diameter of nozzle." msgstr "เส้นผ่านศูนย์กลางของหัวฉีด" @@ -16547,20 +14080,14 @@ msgstr "เส้นผ่านศูนย์กลางของหัวฉ msgid "Configuration notes" msgstr "บันทึกการกำหนดค่า" -msgid "" -"You can put here your personal notes. This text will be added to the G-code " -"header comments." -msgstr "" -"คุณสามารถใส่บันทึกส่วนตัวของคุณได้ที่นี่ ข้อความนี้จะถูกเพิ่มลงในความคิดเห็นส่วนหัวของ G-code" +msgid "You can put here your personal notes. This text will be added to the G-code header comments." +msgstr "คุณสามารถใส่บันทึกส่วนตัวของคุณได้ที่นี่ ข้อความนี้จะถูกเพิ่มลงในความคิดเห็นส่วนหัวของ G-code" msgid "Host Type" msgstr "ประเภทโฮสต์" -msgid "" -"Orca Slicer can upload G-code files to a printer host. This field must " -"contain the kind of the host." -msgstr "" -"Orca Slicer สามารถอัปโหลดไฟล์ G-code ไปยังโฮสต์เครื่องพิมพ์ได้ ฟิลด์นี้จะต้องมีประเภทของโฮสต์" +msgid "Orca Slicer can upload G-code files to a printer host. This field must contain the kind of the host." +msgstr "Orca Slicer สามารถอัปโหลดไฟล์ G-code ไปยังโฮสต์เครื่องพิมพ์ได้ ฟิลด์นี้จะต้องมีประเภทของโฮสต์" msgid "Nozzle volume" msgstr "ปริมาณหัวฉีด" @@ -16583,63 +14110,34 @@ msgstr "ความยาวของท่อระบายความร้ msgid "High extruder current on filament swap" msgstr "กระแสไฟของชุดดันเส้นสูงในการเปลี่ยนเส้นพลาสติก" -msgid "" -"It may be beneficial to increase the extruder motor current during the " -"filament exchange sequence to allow for rapid ramming feed rates and to " -"overcome resistance when loading a filament with an ugly shaped tip." -msgstr "" -"อาจเป็นประโยชน์ในการเพิ่มกระแสมอเตอร์ของชุดดันเส้นในระหว่างลำดับการแลกเปลี่ยนเส้นพลาสติกเพื่อให้อัตราการป้อนชนอย่างรวดเร็ว " -"และเอาชนะความต้านทานเมื่อโหลดเส้นพลาสติกด้วยปลายที่มีรูปร่างน่าเกลียด" +msgid "It may be beneficial to increase the extruder motor current during the filament exchange sequence to allow for rapid ramming feed rates and to overcome resistance when loading a filament with an ugly shaped tip." +msgstr "อาจเป็นประโยชน์ในการเพิ่มกระแสมอเตอร์ของชุดดันเส้นในระหว่างลำดับการแลกเปลี่ยนเส้นพลาสติกเพื่อให้อัตราการป้อนชนอย่างรวดเร็ว และเอาชนะความต้านทานเมื่อโหลดเส้นพลาสติกด้วยปลายที่มีรูปร่างน่าเกลียด" msgid "Filament parking position" msgstr "ตำแหน่งจอดฟิลาเมนต์" -msgid "" -"Distance of the extruder tip from the position where the filament is parked " -"when unloaded. This should match the value in printer firmware." -msgstr "" -"ระยะห่างของปลายชุดดันเส้นจากตำแหน่งที่เส้นพลาสติกจอดอยู่เมื่อขนถ่ายออก " -"ซึ่งควรตรงกับค่าในเฟิร์มแวร์เครื่องพิมพ์" +msgid "Distance of the extruder tip from the position where the filament is parked when unloaded. This should match the value in printer firmware." +msgstr "ระยะห่างของปลายชุดดันเส้นจากตำแหน่งที่เส้นพลาสติกจอดอยู่เมื่อขนถ่ายออก ซึ่งควรตรงกับค่าในเฟิร์มแวร์เครื่องพิมพ์" msgid "Extra loading distance" msgstr "ระยะโหลดเพิ่มเติม" -msgid "" -"When set to zero, the distance the filament is moved from parking position " -"during load is exactly the same as it was moved back during unload. When " -"positive, it is loaded further, if negative, the loading move is shorter " -"than unloading." -msgstr "" -"เมื่อตั้งค่าเป็นศูนย์ " -"ระยะที่เส้นพลาสติกเคลื่อนจากตำแหน่งพักระหว่างโหลดจะเท่ากับระยะที่เคลื่อนกลับระหว่างถอดโหลดพอดี " -"หากเป็นค่าบวก เส้นพลาสติกจะถูกโหลดเข้าไปไกลขึ้น หากเป็นค่าลบ " -"การเคลื่อนที่ขณะโหลดจะสั้นกว่าตอนถอดโหลด" +msgid "When set to zero, the distance the filament is moved from parking position during load is exactly the same as it was moved back during unload. When positive, it is loaded further, if negative, the loading move is shorter than unloading." +msgstr "เมื่อตั้งค่าเป็นศูนย์ ระยะที่เส้นพลาสติกเคลื่อนจากตำแหน่งพักระหว่างโหลดจะเท่ากับระยะที่เคลื่อนกลับระหว่างถอดโหลดพอดี หากเป็นค่าบวก เส้นพลาสติกจะถูกโหลดเข้าไปไกลขึ้น หากเป็นค่าลบ การเคลื่อนที่ขณะโหลดจะสั้นกว่าตอนถอดโหลด" msgid "Start end points" msgstr "เริ่มต้นจุดสิ้นสุด" -msgid "" -"The start and end points which are from the cutter area to the excess chute." +msgid "The start and end points which are from the cutter area to the excess chute." msgstr "จุดเริ่มต้นและจุดสิ้นสุดตั้งแต่บริเวณเครื่องตัดถึงถังขยะ" msgid "Reduce infill retraction" msgstr "ลดการหดตัวของ ไส้ใน" -msgid "" -"Don't retract when the travel is entirely within an infill area. That means " -"the oozing can't been seen. This can reduce times of retraction for complex " -"model and save printing time, but make slicing and G-code generating slower. " -"Note that z-hop is also not performed in areas where retraction is skipped." -msgstr "" -"อย่าถอยกลับเมื่อการเดินทางอยู่ภายในพื้นที่ที่ไส้ในเข้าไปทั้งหมด " -"นั่นหมายความว่าไม่สามารถมองเห็นการรั่วไหลได้ " -"วิธีนี้จะช่วยลดเวลาในการดึงกลับสำหรับโมเดลที่ซับซ้อนและประหยัดเวลาในการพิมพ์ " -"แต่จะทำให้การแบ่งส่วนและการสร้าง G-code ช้าลง โปรดทราบว่า z-hop " -"จะไม่ดำเนินการในพื้นที่ที่มีการข้ามการถอนกลับ" +msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that z-hop is also not performed in areas where retraction is skipped." +msgstr "อย่าถอยกลับเมื่อการเดินทางอยู่ภายในพื้นที่ที่ไส้ในเข้าไปทั้งหมด นั่นหมายความว่าไม่สามารถมองเห็นการรั่วไหลได้ วิธีนี้จะช่วยลดเวลาในการดึงกลับสำหรับโมเดลที่ซับซ้อนและประหยัดเวลาในการพิมพ์ แต่จะทำให้การแบ่งส่วนและการสร้าง G-code ช้าลง โปรดทราบว่า z-hop จะไม่ดำเนินการในพื้นที่ที่มีการข้ามการถอนกลับ" -msgid "" -"This option will drop the temperature of the inactive extruders to prevent " -"oozing." +msgid "This option will drop the temperature of the inactive extruders to prevent oozing." msgstr "ตัวเลือกนี้จะทำให้อุณหภูมิของชุดดันเส้นที่ไม่ใช้งานลดลงเพื่อป้องกันการไหลซึม" msgid "Filename format" @@ -16657,33 +14155,21 @@ msgstr "ปรับเปลี่ยนรูปทรงเพื่อพิ msgid "Make overhangs printable - Maximum angle" msgstr "ทำให้ส่วนยื่นสามารถพิมพ์ได้ - มุมสูงสุด" -msgid "" -"Maximum angle of overhangs to allow after making more steep overhangs " -"printable.90° will not change the model at all and allow any overhang, while " -"0 will replace all overhangs with conical material." -msgstr "" -"มุมยื่นสูงสุดของระยะยื่นที่อนุญาตหลังจากพิมพ์ระยะยื่นที่ชันมากขึ้นแล้ว 90° " -"จะไม่เปลี่ยนโมเดลเลยและยอมให้มีระยะยื่นใดๆ ในขณะที่ 0 จะแทนที่ระยะยื่นทั้งหมดด้วยวัสดุทรงกรวย" +msgid "Maximum angle of overhangs to allow after making more steep overhangs printable.90° will not change the model at all and allow any overhang, while 0 will replace all overhangs with conical material." +msgstr "มุมยื่นสูงสุดของระยะยื่นที่อนุญาตหลังจากพิมพ์ระยะยื่นที่ชันมากขึ้นแล้ว 90° จะไม่เปลี่ยนโมเดลเลยและยอมให้มีระยะยื่นใดๆ ในขณะที่ 0 จะแทนที่ระยะยื่นทั้งหมดด้วยวัสดุทรงกรวย" msgid "Make overhangs printable - Hole area" msgstr "ทำให้ส่วนยื่นสามารถพิมพ์ได้ - พื้นที่รู" -msgid "" -"Maximum area of a hole in the base of the model before it's filled by " -"conical material. A value of 0 will fill all the holes in the model base." -msgstr "" -"พื้นที่สูงสุดของรูที่ฐานของแบบจำลองก่อนที่จะไส้ในด้วยวัสดุทรงกรวย ค่า 0 จะไส้ในเต็มทุกรูในฐานโมเดล" +msgid "Maximum area of a hole in the base of the model before it's filled by conical material. A value of 0 will fill all the holes in the model base." +msgstr "พื้นที่สูงสุดของรูที่ฐานของแบบจำลองก่อนที่จะไส้ในด้วยวัสดุทรงกรวย ค่า 0 จะไส้ในเต็มทุกรูในฐานโมเดล" msgid "Detect overhang walls" msgstr "ตรวจจับผนังส่วนยื่น" #, c-format, boost-format -msgid "" -"This detects the overhang percentage relative to line width and uses a " -"different speed to print. For 100%% overhang, bridging speed is used." -msgstr "" -"ตรวจจับเปอร์เซ็นต์ระยะยื่นที่สัมพันธ์กับความกว้างของเส้น และใช้ความเร็วที่แตกต่างกันในการพิมพ์ " -"สำหรับระยะยื่น 100%% จะใช้ความเร็วบริดจ์" +msgid "This detects the overhang percentage relative to line width and uses a different speed to print. For 100%% overhang, bridging speed is used." +msgstr "ตรวจจับเปอร์เซ็นต์ระยะยื่นที่สัมพันธ์กับความกว้างของเส้น และใช้ความเร็วที่แตกต่างกันในการพิมพ์ สำหรับระยะยื่น 100%% จะใช้ความเร็วบริดจ์" msgid "Outer walls" msgstr "ผนังชั้นนอก" @@ -16705,9 +14191,7 @@ msgstr "" "เส้นพลาสติกสำหรับพิมพ์ผนังชั้นใน\n" "\"ค่าเริ่มต้น\" ใช้เส้นพลาสติกของวัตถุ/ชิ้นส่วนที่ใช้งานอยู่" -msgid "" -"Line width of inner wall. If expressed as a %, it will be computed over the " -"nozzle diameter." +msgid "Line width of inner wall. If expressed as a %, it will be computed over the nozzle diameter." msgstr "ความกว้างของเส้นของผนังด้านใน หากแสดงเป็น % จะคำนวณตามเส้นผ่านศูนย์กลางของหัวฉีด" msgid "This is the speed for inner walls." @@ -16720,44 +14204,26 @@ msgid "Alternate extra wall" msgstr "ผนังเสริมสำรอง" msgid "" -"This setting adds an extra wall to every other layer. This way the infill " -"gets wedged vertically between the walls, resulting in stronger prints.\n" +"This setting adds an extra wall to every other layer. This way the infill gets wedged vertically between the walls, resulting in stronger prints.\n" "\n" -"When this option is enabled, the ensure vertical shell thickness option " -"needs to be disabled.\n" +"When this option is enabled, the ensure vertical shell thickness option needs to be disabled.\n" "\n" -"Using lightning infill together with this option is not recommended as there " -"is limited infill to anchor the extra perimeters to." +"Using lightning infill together with this option is not recommended as there is limited infill to anchor the extra perimeters to." msgstr "" -"การตั้งค่านี้จะเพิ่มผนังพิเศษแบบชั้นเว้นชั้น วิธีนี้จะทำให้ไส้ในถูกหนีบในแนวตั้งระหว่างผนัง " -"ส่งผลให้งานพิมพ์แข็งแรงขึ้น\n" +"การตั้งค่านี้จะเพิ่มผนังพิเศษแบบชั้นเว้นชั้น วิธีนี้จะทำให้ไส้ในถูกหนีบในแนวตั้งระหว่างผนัง ส่งผลให้งานพิมพ์แข็งแรงขึ้น\n" "\n" "เมื่อเปิดใช้งานตัวเลือกนี้ จะต้องปิดใช้งานตัวเลือกตรวจสอบความหนาของเปลือกแนวตั้ง\n" "\n" "ไม่แนะนำให้ใช้ไส้ในแบบสายฟ้าร่วมกับตัวเลือกนี้ เนื่องจากมีไส้ในจำกัดสำหรับยึดเส้นรอบวงเสริม" -msgid "" -"If you want to process the output G-code through custom scripts, just list " -"their absolute paths here. Separate multiple scripts with a semicolon. " -"Scripts will be passed the absolute path to the G-code file as the first " -"argument, and they can access the Orca Slicer config settings by reading " -"environment variables." -msgstr "" -"หากคุณต้องการประมวลผลเอาต์พุต G-code ผ่านสคริปต์ที่กำหนดเอง " -"เพียงระบุเส้นทางสัมบูรณ์ของสคริปต์ไว้ที่นี่ แยกสคริปต์หลายรายการด้วยเครื่องหมายอัฒภาค " -"สคริปต์จะถูกส่งผ่านเส้นทางสัมบูรณ์ไปยังไฟล์ G-code เป็นอาร์กิวเมนต์แรก " -"และสคริปต์เหล่านี้สามารถเข้าถึงการตั้งค่าการกำหนดค่า Orca Slicer " -"ได้โดยการอ่านตัวแปรสภาพแวดล้อม" +msgid "If you want to process the output G-code through custom scripts, just list their absolute paths here. Separate multiple scripts with a semicolon. Scripts will be passed the absolute path to the G-code file as the first argument, and they can access the Orca Slicer config settings by reading environment variables." +msgstr "หากคุณต้องการประมวลผลเอาต์พุต G-code ผ่านสคริปต์ที่กำหนดเอง เพียงระบุเส้นทางสัมบูรณ์ของสคริปต์ไว้ที่นี่ แยกสคริปต์หลายรายการด้วยเครื่องหมายอัฒภาค สคริปต์จะถูกส่งผ่านเส้นทางสัมบูรณ์ไปยังไฟล์ G-code เป็นอาร์กิวเมนต์แรก และสคริปต์เหล่านี้สามารถเข้าถึงการตั้งค่าการกำหนดค่า Orca Slicer ได้โดยการอ่านตัวแปรสภาพแวดล้อม" msgid "Change extrusion role G-code (process)" msgstr "เปลี่ยนบทบาทการอัดขึ้นรูป G-code (กระบวนการ)" -msgid "" -"This G-code is inserted when the extrusion role is changed. It runs after " -"the machine and filament extrusion role G-code." -msgstr "" -"G-code นี้จะถูกแทรกเมื่อบทบาทการอัดขึ้นรูปมีการเปลี่ยนแปลง " -"มันทำงานหลังจากบทบาทการอัดขึ้นรูปของเครื่องจักรและการอัดขึ้นรูปเส้นพลาสติก G-code" +msgid "This G-code is inserted when the extrusion role is changed. It runs after the machine and filament extrusion role G-code." +msgstr "G-code นี้จะถูกแทรกเมื่อบทบาทการอัดขึ้นรูปมีการเปลี่ยนแปลง มันทำงานหลังจากบทบาทการอัดขึ้นรูปของเครื่องจักรและการอัดขึ้นรูปเส้นพลาสติก G-code" msgid "Printer type" msgstr "ประเภทเครื่องพิมพ์" @@ -16777,12 +14243,8 @@ msgstr "ตัวแปรเครื่องพิมพ์" msgid "Raft contact Z distance" msgstr "ระยะสัมผัสฐานรองชิ้นงาน Z" -msgid "" -"Z gap between raft and object. If Support Top Z Distance is 0, this value is " -"ignored and the object is printed in direct contact with the raft (no gap)." -msgstr "" -"ช่องว่าง Z ระหว่างฐานรองชิ้นงานกับวัตถุ หากระยะ Z สูงสุดที่รองรับคือ 0 ค่านี้จะถูกละเว้น " -"และวัตถุจะถูกพิมพ์โดยสัมผัสโดยตรงกับฐานรองชิ้นงาน (ไม่มีช่องว่าง)" +msgid "Z gap between raft and object. If Support Top Z Distance is 0, this value is ignored and the object is printed in direct contact with the raft (no gap)." +msgstr "ช่องว่าง Z ระหว่างฐานรองชิ้นงานกับวัตถุ หากระยะ Z สูงสุดที่รองรับคือ 0 ค่านี้จะถูกละเว้น และวัตถุจะถูกพิมพ์โดยสัมผัสโดยตรงกับฐานรองชิ้นงาน (ไม่มีช่องว่าง)" msgid "Raft expansion" msgstr "การขยาย ฐานรองชิ้นงาน" @@ -16805,33 +14267,22 @@ msgstr "ขยายฐานรองชิ้นงานหรือชั้ msgid "Raft layers" msgstr "จำนวนชั้น ฐานรองชิ้นงาน" -msgid "" -"Object will be raised by this number of support layers. Use this function to " -"avoid warping when printing ABS." +msgid "Object will be raised by this number of support layers. Use this function to avoid warping when printing ABS." msgstr "วัตถุจะถูกยกขึ้นตามจำนวนชั้นรองรับนี้ ใช้ฟังก์ชันนี้เพื่อหลีกเลี่ยงการบิดงอเมื่อพิมพ์ ABS" -msgid "" -"The G-code path is generated after simplifying the contour of models to " -"avoid too many points and G-code lines. Smaller values mean higher " -"resolution and more time required to slice." -msgstr "" -"เส้นทาง G-code ถูกสร้างขึ้นหลังจากลดความซับซ้อนของรูปร่างของโมเดล เพื่อหลีกเลี่ยงจุดและเส้น G-" -"code ที่มากเกินไป ค่าที่น้อยลงหมายถึงความละเอียดสูงขึ้นและมีเวลามากขึ้นในการตัด" +msgid "The G-code path is generated after simplifying the contour of models to avoid too many points and G-code lines. Smaller values mean higher resolution and more time required to slice." +msgstr "เส้นทาง G-code ถูกสร้างขึ้นหลังจากลดความซับซ้อนของรูปร่างของโมเดล เพื่อหลีกเลี่ยงจุดและเส้น G-code ที่มากเกินไป ค่าที่น้อยลงหมายถึงความละเอียดสูงขึ้นและมีเวลามากขึ้นในการตัด" msgid "Travel distance threshold" msgstr "เกณฑ์ระยะทางการเดินทาง" -msgid "" -"Only trigger retraction when the travel distance is longer than this " -"threshold." +msgid "Only trigger retraction when the travel distance is longer than this threshold." msgstr "ทริกเกอร์การถอนกลับเมื่อระยะการเดินทางยาวกว่าเกณฑ์นี้เท่านั้น" msgid "Retract amount before wipe" msgstr "ถอนจำนวนก่อนเช็ด" -msgid "" -"This is the length of fast retraction before a wipe, relative to retraction " -"length." +msgid "This is the length of fast retraction before a wipe, relative to retraction length." msgstr "ความยาวของการดึงกลับอย่างรวดเร็วก่อนเช็ด สัมพันธ์กับความยาวการดึงกลับ" msgid "Retract on layer change" @@ -16843,32 +14294,19 @@ msgstr "บังคับให้ถอนกลับเมื่อเปล msgid "Retraction Length" msgstr "ระยะดึงกลับ" -msgid "" -"Some amount of material in extruder is pulled back to avoid ooze during long " -"travel. Set zero to disable retraction." -msgstr "" -"วัสดุบางส่วนในชุดดันเส้นถูกดึงกลับเพื่อหลีกเลี่ยงไม่ให้ซึ่มในระหว่างการเดินทางระยะไกล " -"ตั้งค่าเป็นศูนย์เพื่อปิดใช้งานการเพิกถอน" +msgid "Some amount of material in extruder is pulled back to avoid ooze during long travel. Set zero to disable retraction." +msgstr "วัสดุบางส่วนในชุดดันเส้นถูกดึงกลับเพื่อหลีกเลี่ยงไม่ให้ซึ่มในระหว่างการเดินทางระยะไกล ตั้งค่าเป็นศูนย์เพื่อปิดใช้งานการเพิกถอน" msgid "Long retraction when cut (beta)" msgstr "การถอยกลับยาวเมื่อตัด (เบต้า)" -msgid "" -"Experimental feature: Retracting and cutting off the filament at a longer " -"distance during changes to minimize purge. While this reduces flush " -"significantly, it may also raise the risk of nozzle clogs or other printing " -"problems." -msgstr "" -"คุณลักษณะการทดลอง: " -"การดึงกลับและตัดเส้นพลาสติกออกในระยะไกลระหว่างการเปลี่ยนแปลงเพื่อลดการไล่เส้น " -"แม้ว่าวิธีนี้จะช่วยลดไล่เส้นได้อย่างมาก แต่ก็อาจเพิ่มความเสี่ยงที่หัวฉีดอุดตันหรือปัญหาการพิมพ์อื่นๆ อีกด้วย" +msgid "Experimental feature: Retracting and cutting off the filament at a longer distance during changes to minimize purge. While this reduces flush significantly, it may also raise the risk of nozzle clogs or other printing problems." +msgstr "คุณลักษณะการทดลอง: การดึงกลับและตัดเส้นพลาสติกออกในระยะไกลระหว่างการเปลี่ยนแปลงเพื่อลดการไล่เส้น แม้ว่าวิธีนี้จะช่วยลดไล่เส้นได้อย่างมาก แต่ก็อาจเพิ่มความเสี่ยงที่หัวฉีดอุดตันหรือปัญหาการพิมพ์อื่นๆ อีกด้วย" msgid "Retraction distance when cut" msgstr "ระยะการดึงกลับเมื่อตัด" -msgid "" -"Experimental feature: Retraction length before cutting off during filament " -"change." +msgid "Experimental feature: Retraction length before cutting off during filament change." msgstr "คุณลักษณะการทดลอง: ความยาวการดึงกลับก่อนตัดออกระหว่างการเปลี่ยนเส้นพลาสติก" msgid "Long retraction when extruder change" @@ -16880,33 +14318,20 @@ msgstr "ระยะการดึงกลับเมื่อชุดดั msgid "Z-hop height" msgstr "ความสูงยกแกน Z" -msgid "" -"Whenever there is a retraction, the nozzle is lifted a little to create " -"clearance between the nozzle and the print. This prevents the nozzle from " -"hitting the print when traveling more. Using spiral lines to lift z can " -"prevent stringing." -msgstr "" -"เมื่อใดก็ตามที่การดึงกลับเสร็จสิ้น หัวฉีดจะถูกยกขึ้นเล็กน้อยเพื่อสร้างระยะห่างระหว่างหัวฉีดและงานพิมพ์ " -"ป้องกันไม่ให้หัวฉีดชนกับงานพิมพ์ขณะเคลื่อนตัว การใช้เส้นเกลียวเพื่อยก Z สามารถป้องกันการร้อยสายได้" +msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift z can prevent stringing." +msgstr "เมื่อใดก็ตามที่การดึงกลับเสร็จสิ้น หัวฉีดจะถูกยกขึ้นเล็กน้อยเพื่อสร้างระยะห่างระหว่างหัวฉีดและงานพิมพ์ ป้องกันไม่ให้หัวฉีดชนกับงานพิมพ์ขณะเคลื่อนตัว การใช้เส้นเกลียวเพื่อยก Z สามารถป้องกันการร้อยสายได้" msgid "Z-hop lower boundary" msgstr "ขอบเขตล่างของ Z-hop" -msgid "" -"Z-hop will only come into effect when Z is above this value and is below the " -"parameter: \"Z-hop upper boundary\"." -msgstr "" -"Z-hop จะมีผลเฉพาะเมื่อ Z อยู่เหนือค่านี้และต่ำกว่าพารามิเตอร์: \"Z-hop upper boundary\"" +msgid "Z-hop will only come into effect when Z is above this value and is below the parameter: \"Z-hop upper boundary\"." +msgstr "Z-hop จะมีผลเฉพาะเมื่อ Z อยู่เหนือค่านี้และต่ำกว่าพารามิเตอร์: \"Z-hop upper boundary\"" msgid "Z-hop upper boundary" msgstr "ขอบเขตบนของ Z-hop" -msgid "" -"If this value is positive, Z-hop will only come into effect when Z is above " -"the parameter: \"Z-hop lower boundary\" and is below this value." -msgstr "" -"หากค่านี้เป็นค่าบวก Z-hop จะมีผลเฉพาะเมื่อ Z อยู่เหนือพารามิเตอร์: \"Z-hop lower boundary" -"\" และอยู่ต่ำกว่าค่านี้" +msgid "If this value is positive, Z-hop will only come into effect when Z is above the parameter: \"Z-hop lower boundary\" and is below this value." +msgstr "หากค่านี้เป็นค่าบวก Z-hop จะมีผลเฉพาะเมื่อ Z อยู่เหนือพารามิเตอร์: \"Z-hop lower boundary\" และอยู่ต่ำกว่าค่านี้" msgid "Z-hop type" msgstr "ประเภท Z-hop" @@ -16923,38 +14348,26 @@ msgstr "เกลียว" msgid "Traveling angle" msgstr "มุมการเดินทาง" -msgid "" -"Traveling angle for Slope and Spiral Z-hop type. Setting it to 90° results " -"in Normal Lift." -msgstr "" -"มุมการเคลื่อนที่สำหรับประเภท Slope และ Spiral Z-hop การตั้งค่าเป็น 90° จะทำให้ได้ Normal " -"Lift" +msgid "Traveling angle for Slope and Spiral Z-hop type. Setting it to 90° results in Normal Lift." +msgstr "มุมการเคลื่อนที่สำหรับประเภท Slope และ Spiral Z-hop การตั้งค่าเป็น 90° จะทำให้ได้ Normal Lift" msgid "Only lift Z above" msgstr "ยก Z ขึ้นด้านบนเท่านั้น" -msgid "" -"If you set this to a positive value, Z lift will only take place above the " -"specified absolute Z." +msgid "If you set this to a positive value, Z lift will only take place above the specified absolute Z." msgstr "หากคุณตั้งค่านี้เป็นค่าบวก การยก Z จะเกิดขึ้นเหนือค่า Z สัมบูรณ์ที่ระบุเท่านั้น" msgid "Only lift Z below" msgstr "ยก Z ด้านล่างเท่านั้น" -msgid "" -"If you set this to a positive value, Z lift will only take place below the " -"specified absolute Z." +msgid "If you set this to a positive value, Z lift will only take place below the specified absolute Z." msgstr "หากคุณตั้งค่านี้เป็นค่าบวก การยก Z จะเกิดขึ้นต่ำกว่าค่า Z สัมบูรณ์ที่ระบุเท่านั้น" msgid "On surfaces" msgstr "บนพื้นผิว" -msgid "" -"Enforce Z-Hop behavior. This setting is impacted by the above settings (Only " -"lift Z above/below)." -msgstr "" -"บังคับใช้พฤติกรรม Z-Hop การตั้งค่านี้ได้รับผลกระทบจากการตั้งค่าด้านบน (เฉพาะการยก Z ด้านบน/" -"ล่างเท่านั้น)" +msgid "Enforce Z-Hop behavior. This setting is impacted by the above settings (Only lift Z above/below)." +msgstr "บังคับใช้พฤติกรรม Z-Hop การตั้งค่านี้ได้รับผลกระทบจากการตั้งค่าด้านบน (เฉพาะการยก Z ด้านบน/ล่างเท่านั้น)" msgid "All Surfaces" msgstr "พื้นผิวทั้งหมด" @@ -16977,16 +14390,10 @@ msgstr "โบว์เดน" msgid "Extra length on restart" msgstr "ความยาวพิเศษเมื่อรีสตาร์ท" -msgid "" -"When the retraction is compensated after the travel move, the extruder will " -"push this additional amount of filament. This setting is rarely needed." -msgstr "" -"เมื่อชดเชยการดึงกลับหลังการเคลื่อนที่เดินทาง ชุดดันเส้นจะดันเส้นพลาสติกเพิ่มเติมในปริมาณนี้ " -"การตั้งค่านี้ไม่ค่อยจำเป็น" +msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." +msgstr "เมื่อชดเชยการดึงกลับหลังการเคลื่อนที่เดินทาง ชุดดันเส้นจะดันเส้นพลาสติกเพิ่มเติมในปริมาณนี้ การตั้งค่านี้ไม่ค่อยจำเป็น" -msgid "" -"When the retraction is compensated after changing tool, the extruder will " -"push this additional amount of filament." +msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "เมื่อชดเชยการดึงกลับหลังเปลี่ยนเครื่องมือ ชุดดันเส้นจะดันเส้นพลาสติกเพิ่มเติมในปริมาณนี้" msgid "Retraction speed" @@ -16998,20 +14405,14 @@ msgstr "ความเร็วในการดึงเส้นพลาส msgid "Deretraction speed" msgstr "ความเร็วในการถอนกลับ" -msgid "" -"Speed for reloading filament into the nozzle. Zero means same speed of " -"retraction." +msgid "Speed for reloading filament into the nozzle. Zero means same speed of retraction." msgstr "ความเร็วในการบรรจุเส้นพลาสติกลงในหัวฉีด ศูนย์หมายถึงความเร็วการถอยกลับเท่ากัน" msgid "Use firmware retraction" msgstr "ใช้การเพิกถอนเฟิร์มแวร์" -msgid "" -"This experimental setting uses G10 and G11 commands to have the firmware " -"handle the retraction. This is only supported in recent Marlin." -msgstr "" -"การตั้งค่าทดลองนี้ใช้คำสั่ง G10 และ G11 เพื่อให้เฟิร์มแวร์จัดการกับการเพิกถอน สิ่งนี้รองรับใน " -"Marlin ล่าสุดเท่านั้น" +msgid "This experimental setting uses G10 and G11 commands to have the firmware handle the retraction. This is only supported in recent Marlin." +msgstr "การตั้งค่าทดลองนี้ใช้คำสั่ง G10 และ G11 เพื่อให้เฟิร์มแวร์จัดการกับการเพิกถอน สิ่งนี้รองรับใน Marlin ล่าสุดเท่านั้น" msgid "Show auto-calibration marks" msgstr "แสดงเครื่องหมายปรับเทียบอัตโนมัติ" @@ -17019,8 +14420,7 @@ msgstr "แสดงเครื่องหมายปรับเทียบ msgid "Disable set remaining print time" msgstr "ปิดใช้งานการตั้งค่าเวลาการพิมพ์ที่เหลืออยู่" -msgid "" -"Disable generating of the M73: Set remaining print time in the final G-code." +msgid "Disable generating of the M73: Set remaining print time in the final G-code." msgstr "ปิดการใช้งานการสร้าง M73: ตั้งเวลาการพิมพ์ที่เหลืออยู่ใน G-code สุดท้าย" msgid "Seam position" @@ -17044,24 +14444,18 @@ msgstr "สุ่ม" msgid "Staggered inner seams" msgstr "รอยตะเข็บด้านในเซ" -msgid "" -"This option causes the inner seams to be shifted backwards based on their " -"depth, forming a zigzag pattern." +msgid "This option causes the inner seams to be shifted backwards based on their depth, forming a zigzag pattern." msgstr "ตัวเลือกนี้จะทำให้รอยตะเข็บด้านในเลื่อนไปข้างหลังตามความลึก ทำให้เกิดลวดลายซิกแซก" msgid "Seam gap" msgstr "ช่องว่างรอยตะเข็บ" msgid "" -"In order to reduce the visibility of the seam in a closed loop extrusion, " -"the loop is interrupted and shortened by a specified amount.\n" -"This amount can be specified in millimeters or as a percentage of the " -"current extruder diameter. The default value for this parameter is 10%." +"In order to reduce the visibility of the seam in a closed loop extrusion, the loop is interrupted and shortened by a specified amount.\n" +"This amount can be specified in millimeters or as a percentage of the current extruder diameter. The default value for this parameter is 10%." msgstr "" -"เพื่อลดการมองเห็นรอยตะเข็บในการอัดขึ้นรูปแบบวงปิด " -"วงจะถูกขัดจังหวะและทำให้สั้นลงตามจำนวนที่ระบุ\n" -"ปริมาณนี้สามารถระบุได้ในหน่วยมิลลิเมตรหรือเป็นเปอร์เซ็นต์ของเส้นผ่านศูนย์กลางของชุดดันเส้นในปัจจุบัน " -"ค่าเริ่มต้นสำหรับพารามิเตอร์นี้คือ 10%" +"เพื่อลดการมองเห็นรอยตะเข็บในการอัดขึ้นรูปแบบวงปิด วงจะถูกขัดจังหวะและทำให้สั้นลงตามจำนวนที่ระบุ\n" +"ปริมาณนี้สามารถระบุได้ในหน่วยมิลลิเมตรหรือเป็นเปอร์เซ็นต์ของเส้นผ่านศูนย์กลางของชุดดันเส้นในปัจจุบัน ค่าเริ่มต้นสำหรับพารามิเตอร์นี้คือ 10%" msgid "Scarf joint seam (beta)" msgstr "รอยต่อผ้าพันคอ (เบต้า)" @@ -17072,63 +14466,31 @@ msgstr "ใช้ข้อต่อผ้าพันคอเพื่อลด msgid "Conditional scarf joint" msgstr "ข้อต่อผ้าพันคอแบบมีเงื่อนไข" -msgid "" -"Apply scarf joints only to smooth perimeters where traditional seams do not " -"conceal the seams at sharp corners effectively." -msgstr "" -"ใช้ข้อต่อผ้าพันคอกับขอบเรียบเท่านั้น " -"โดยที่รอยตะเข็บแบบเดิมไม่สามารถปกปิดรอยตะเข็บที่มุมแหลมคมได้อย่างมีประสิทธิภาพ" +msgid "Apply scarf joints only to smooth perimeters where traditional seams do not conceal the seams at sharp corners effectively." +msgstr "ใช้ข้อต่อผ้าพันคอกับขอบเรียบเท่านั้น โดยที่รอยตะเข็บแบบเดิมไม่สามารถปกปิดรอยตะเข็บที่มุมแหลมคมได้อย่างมีประสิทธิภาพ" msgid "Conditional angle threshold" msgstr "เกณฑ์มุมแบบมีเงื่อนไข" msgid "" -"This option sets the threshold angle for applying a conditional scarf joint " -"seam.\n" -"If the maximum angle within the perimeter loop exceeds this value " -"(indicating the absence of sharp corners), a scarf joint seam will be used. " -"The default value is 155°." +"This option sets the threshold angle for applying a conditional scarf joint seam.\n" +"If the maximum angle within the perimeter loop exceeds this value (indicating the absence of sharp corners), a scarf joint seam will be used. The default value is 155°." msgstr "" "ตัวเลือกนี้จะกำหนดมุมเกณฑ์สำหรับการใช้รอยตะเข็บข้อต่อผ้าพันคอแบบมีเงื่อนไข\n" -"หากมุมสูงสุดภายในวงรอบปริมณฑลเกินค่านี้ (แสดงว่าไม่มีมุมแหลมคม) จะใช้รอยตะเข็บข้อต่อผ้าพันคอ " -"ค่าเริ่มต้นคือ 155°" +"หากมุมสูงสุดภายในวงรอบปริมณฑลเกินค่านี้ (แสดงว่าไม่มีมุมแหลมคม) จะใช้รอยตะเข็บข้อต่อผ้าพันคอ ค่าเริ่มต้นคือ 155°" msgid "Conditional overhang threshold" msgstr "เกณฑ์ระยะยื่นแบบมีเงื่อนไข" #, no-c-format, no-boost-format -msgid "" -"This option determines the overhang threshold for the application of scarf " -"joint seams. If the unsupported portion of the perimeter is less than this " -"threshold, scarf joint seams will be applied. The default threshold is set " -"at 40% of the external wall's width. Due to performance considerations, the " -"degree of overhang is estimated." -msgstr "" -"ตัวเลือกนี้จะกำหนดเกณฑ์ส่วนยื่นสำหรับการใช้รอยตะเข็บข้อต่อผ้าพันคอ " -"หากส่วนที่ไม่ได้รับส่วนรองรับของเส้นรอบวงน้อยกว่าเกณฑ์นี้ จะมีการเย็บรอยตะเข็บผ้าพันคอ " -"เกณฑ์เริ่มต้นตั้งไว้ที่ 40% ของความกว้างของผนังภายนอก เมื่อพิจารณาถึงประสิทธิภาพแล้ว " -"ระดับของระยะยื่นจึงถูกประมาณไว้" +msgid "This option determines the overhang threshold for the application of scarf joint seams. If the unsupported portion of the perimeter is less than this threshold, scarf joint seams will be applied. The default threshold is set at 40% of the external wall's width. Due to performance considerations, the degree of overhang is estimated." +msgstr "ตัวเลือกนี้จะกำหนดเกณฑ์ส่วนยื่นสำหรับการใช้รอยตะเข็บข้อต่อผ้าพันคอ หากส่วนที่ไม่ได้รับส่วนรองรับของเส้นรอบวงน้อยกว่าเกณฑ์นี้ จะมีการเย็บรอยตะเข็บผ้าพันคอ เกณฑ์เริ่มต้นตั้งไว้ที่ 40% ของความกว้างของผนังภายนอก เมื่อพิจารณาถึงประสิทธิภาพแล้ว ระดับของระยะยื่นจึงถูกประมาณไว้" msgid "Scarf joint speed" msgstr "ความเร็วของข้อต่อผ้าพันคอ" -msgid "" -"This option sets the printing speed for scarf joints. It is recommended to " -"print scarf joints at a slow speed (less than 100 mm/s). It's also advisable " -"to enable 'Extrusion rate smoothing' if the set speed varies significantly " -"from the speed of the outer or inner walls. If the speed specified here is " -"higher than the speed of the outer or inner walls, the printer will default " -"to the slower of the two speeds. When specified as a percentage (e.g., 80%), " -"the speed is calculated based on the respective outer or inner wall speed. " -"The default value is set to 100%." -msgstr "" -"ตัวเลือกนี้จะตั้งค่าความเร็วในการพิมพ์สำหรับข้อต่อผ้าพันคอ " -"ขอแนะนำให้พิมพ์ข้อต่อผ้าพันคอด้วยความเร็วต่ำ (น้อยกว่า 100 มม./วินาที) ขอแนะนำให้เปิดใช้งาน " -"'การปรับอัตราการอัดรีดให้เรียบ' " -"หากความเร็วที่ตั้งไว้แตกต่างอย่างมากจากความเร็วของผนังด้านนอกหรือด้านใน " -"หากความเร็วที่ระบุที่นี่สูงกว่าความเร็วของผนังด้านนอกหรือด้านใน " -"เครื่องพิมพ์จะตั้งค่าเริ่มต้นไว้ที่ความเร็วที่ช้ากว่าทั้งสอง เมื่อระบุเป็นเปอร์เซ็นต์ (เช่น 80%) " -"ความเร็วจะคำนวณตามความเร็วผนังด้านนอกหรือด้านในตามลำดับ ค่าเริ่มต้นตั้งไว้ที่ 100%" +msgid "This option sets the printing speed for scarf joints. It is recommended to print scarf joints at a slow speed (less than 100 mm/s). It's also advisable to enable 'Extrusion rate smoothing' if the set speed varies significantly from the speed of the outer or inner walls. If the speed specified here is higher than the speed of the outer or inner walls, the printer will default to the slower of the two speeds. When specified as a percentage (e.g., 80%), the speed is calculated based on the respective outer or inner wall speed. The default value is set to 100%." +msgstr "ตัวเลือกนี้จะตั้งค่าความเร็วในการพิมพ์สำหรับข้อต่อผ้าพันคอ ขอแนะนำให้พิมพ์ข้อต่อผ้าพันคอด้วยความเร็วต่ำ (น้อยกว่า 100 มม./วินาที) ขอแนะนำให้เปิดใช้งาน 'การปรับอัตราการอัดรีดให้เรียบ' หากความเร็วที่ตั้งไว้แตกต่างอย่างมากจากความเร็วของผนังด้านนอกหรือด้านใน หากความเร็วที่ระบุที่นี่สูงกว่าความเร็วของผนังด้านนอกหรือด้านใน เครื่องพิมพ์จะตั้งค่าเริ่มต้นไว้ที่ความเร็วที่ช้ากว่าทั้งสอง เมื่อระบุเป็นเปอร์เซ็นต์ (เช่น 80%) ความเร็วจะคำนวณตามความเร็วผนังด้านนอกหรือด้านในตามลำดับ ค่าเริ่มต้นตั้งไว้ที่ 100%" msgid "Scarf joint flow ratio" msgstr "อัตราการไหลของข้อต่อผ้าพันคอ" @@ -17141,12 +14503,10 @@ msgstr "ความสูงเริ่มต้นของผ้าพัน msgid "" "Start height of the scarf.\n" -"This amount can be specified in millimeters or as a percentage of the " -"current layer height. The default value for this parameter is 0." +"This amount can be specified in millimeters or as a percentage of the current layer height. The default value for this parameter is 0." msgstr "" "เริ่มต้นความสูงของผ้าพันคอ\n" -"จำนวนนี้สามารถระบุได้ในหน่วยมิลลิเมตรหรือเป็นเปอร์เซ็นต์ของความสูงของเลเยอร์ปัจจุบัน " -"ค่าเริ่มต้นสำหรับพารามิเตอร์นี้คือ 0" +"จำนวนนี้สามารถระบุได้ในหน่วยมิลลิเมตรหรือเป็นเปอร์เซ็นต์ของความสูงของเลเยอร์ปัจจุบัน ค่าเริ่มต้นสำหรับพารามิเตอร์นี้คือ 0" msgid "Scarf around entire wall" msgstr "ผ้าพันคอพันรอบผนังทั้งหมด" @@ -17157,9 +14517,7 @@ msgstr "ผ้าพันคอยาวตลอดความยาวขอ msgid "Scarf length" msgstr "ความยาวผ้าพันคอ" -msgid "" -"Length of the scarf. Setting this parameter to zero effectively disables the " -"scarf." +msgid "Length of the scarf. Setting this parameter to zero effectively disables the scarf." msgstr "ความยาวของผ้าพันคอ. การตั้งค่าพารามิเตอร์นี้เป็นศูนย์จะปิดใช้ผ้าพันคออย่างมีประสิทธิภาพ" msgid "Scarf steps" @@ -17177,60 +14535,32 @@ msgstr "ใช้ข้อต่อผ้าพันคอสำหรับผ msgid "Role base wipe speed" msgstr "ความเร็วในการล้างฐานบทบาท" -msgid "" -"The wipe speed is determined by the speed of the current extrusion role. e." -"g. if a wipe action is executed immediately following an outer wall " -"extrusion, the speed of the outer wall extrusion will be utilized for the " -"wipe action." -msgstr "" -"ความเร็วในการเช็ดถูกกำหนดโดยความเร็วของบทบาทการอัดขึ้นรูปในปัจจุบัน เช่น " -"หากการดำเนินการเช็ดถูกดำเนินการทันทีหลังจากการอัดขึ้นรูปผนังด้านนอก " -"ความเร็วของการอัดขึ้นรูปผนังด้านนอกจะถูกใช้สำหรับการดำเนินการเช็ด" +msgid "The wipe speed is determined by the speed of the current extrusion role. e.g. if a wipe action is executed immediately following an outer wall extrusion, the speed of the outer wall extrusion will be utilized for the wipe action." +msgstr "ความเร็วในการเช็ดถูกกำหนดโดยความเร็วของบทบาทการอัดขึ้นรูปในปัจจุบัน เช่น หากการดำเนินการเช็ดถูกดำเนินการทันทีหลังจากการอัดขึ้นรูปผนังด้านนอก ความเร็วของการอัดขึ้นรูปผนังด้านนอกจะถูกใช้สำหรับการดำเนินการเช็ด" msgid "Wipe on loops" msgstr "เช็ดบนลูป" -msgid "" -"To minimize the visibility of the seam in a closed loop extrusion, a small " -"inward movement is executed before the extruder leaves the loop." -msgstr "" -"เพื่อลดการมองเห็นรอยต่อในการอัดรีดแบบวงปิด " -"จะมีการเคลื่อนตัวเข้าด้านในเล็กน้อยก่อนที่ชุดดันเส้นจะออกจากวง" +msgid "To minimize the visibility of the seam in a closed loop extrusion, a small inward movement is executed before the extruder leaves the loop." +msgstr "เพื่อลดการมองเห็นรอยต่อในการอัดรีดแบบวงปิด จะมีการเคลื่อนตัวเข้าด้านในเล็กน้อยก่อนที่ชุดดันเส้นจะออกจากวง" msgid "Wipe before external loop" msgstr "เช็ดก่อนวนรอบภายนอก" msgid "" -"To minimize visibility of potential overextrusion at the start of an " -"external perimeter when printing with Outer/Inner or Inner/Outer/Inner wall " -"print order, the de-retraction is performed slightly on the inside from the " -"start of the external perimeter. That way any potential over extrusion is " -"hidden from the outside surface.\n" +"To minimize visibility of potential overextrusion at the start of an external perimeter when printing with Outer/Inner or Inner/Outer/Inner wall print order, the de-retraction is performed slightly on the inside from the start of the external perimeter. That way any potential over extrusion is hidden from the outside surface.\n" "\n" -"This is useful when printing with Outer/Inner or Inner/Outer/Inner wall " -"print order as in these modes it is more likely an external perimeter is " -"printed immediately after a de-retraction move." +"This is useful when printing with Outer/Inner or Inner/Outer/Inner wall print order as in these modes it is more likely an external perimeter is printed immediately after a de-retraction move." msgstr "" -"เพื่อลดการมองเห็นการอัดขึ้นรูปมากเกินไปที่อาจเกิดขึ้นที่จุดเริ่มต้นของขอบเขตภายนอกเมื่อพิมพ์ด้วยลำดับการพิมพ์ด้านนอก/" -"ด้านในหรือด้านใน/ด้านนอก/ด้านใน " -"การยกเลิกการดึงกลับจะดำเนินการเล็กน้อยจากด้านในจากจุดเริ่มต้นของขอบเขตภายนอก " -"ด้วยวิธีนี้ศักยภาพในการอัดขึ้นรูปจะถูกซ่อนจากพื้นผิวด้านนอก\n" +"เพื่อลดการมองเห็นการอัดขึ้นรูปมากเกินไปที่อาจเกิดขึ้นที่จุดเริ่มต้นของขอบเขตภายนอกเมื่อพิมพ์ด้วยลำดับการพิมพ์ด้านนอก/ด้านในหรือด้านใน/ด้านนอก/ด้านใน การยกเลิกการดึงกลับจะดำเนินการเล็กน้อยจากด้านในจากจุดเริ่มต้นของขอบเขตภายนอก ด้วยวิธีนี้ศักยภาพในการอัดขึ้นรูปจะถูกซ่อนจากพื้นผิวด้านนอก\n" "\n" -"สิ่งนี้มีประโยชน์เมื่อพิมพ์ด้วยลำดับการพิมพ์ด้านนอก/ด้านในหรือด้านใน/ด้านนอก/ด้านใน " -"เนื่องจากในโหมดเหล่านี้ มีแนวโน้มว่าปริมณฑลภายนอกจะถูกพิมพ์ทันทีหลังจากการยกเลิกการถอยกลับ" +"สิ่งนี้มีประโยชน์เมื่อพิมพ์ด้วยลำดับการพิมพ์ด้านนอก/ด้านในหรือด้านใน/ด้านนอก/ด้านใน เนื่องจากในโหมดเหล่านี้ มีแนวโน้มว่าปริมณฑลภายนอกจะถูกพิมพ์ทันทีหลังจากการยกเลิกการถอยกลับ" msgid "Wipe speed" msgstr "ความเร็วเช็ดหัวฉีด" -msgid "" -"The wipe speed is determined by the speed setting specified in this " -"configuration. If the value is expressed as a percentage (e.g. 80%), it will " -"be calculated based on the travel speed setting above. The default value for " -"this parameter is 80%." -msgstr "" -"ความเร็วในการเช็ดถูกกำหนดโดยการตั้งค่าความเร็วที่ระบุในการกำหนดค่านี้ " -"หากค่าแสดงเป็นเปอร์เซ็นต์ (เช่น 80%) ระบบจะคำนวณตามการตั้งค่าความเร็วการเดินทางด้านบน " -"ค่าเริ่มต้นสำหรับพารามิเตอร์นี้คือ 80%" +msgid "The wipe speed is determined by the speed setting specified in this configuration. If the value is expressed as a percentage (e.g. 80%), it will be calculated based on the travel speed setting above. The default value for this parameter is 80%." +msgstr "ความเร็วในการเช็ดถูกกำหนดโดยการตั้งค่าความเร็วที่ระบุในการกำหนดค่านี้ หากค่าแสดงเป็นเปอร์เซ็นต์ (เช่น 80%) ระบบจะคำนวณตามการตั้งค่าความเร็วการเดินทางด้านบน ค่าเริ่มต้นสำหรับพารามิเตอร์นี้คือ 80%" msgid "Skirt distance" msgstr "ระยะห่างของเส้นล้อมชิ้นงาน" @@ -17241,9 +14571,7 @@ msgstr "ระยะห่างจากเส้นล้อมชิ้นง msgid "Skirt start point" msgstr "จุดเริ่มต้นเส้นล้อมชิ้นงาน" -msgid "" -"Angle from the object center to skirt start point. Zero is the most right " -"position, counter clockwise is positive angle." +msgid "Angle from the object center to skirt start point. Zero is the most right position, counter clockwise is positive angle." msgstr "มุมจากศูนย์กลางวัตถุถึงจุดเริ่มต้นสเกิร์ต ศูนย์คือตำแหน่งที่ถูกต้องที่สุด ทวนเข็มนาฬิกาคือมุมบวก" msgid "Skirt height" @@ -17255,36 +14583,22 @@ msgstr "เส้นล้อมชิ้นงานกี่ชั้น.. โ msgid "Single loop after first layer" msgstr "วนรอบเดียวหลังจากชั้นแรก" -msgid "" -"Limits the skirt/draft shield loops to one wall after the first layer. This " -"is useful, on occasion, to conserve filament but may cause the draft shield/" -"skirt to warp / crack." -msgstr "" -"จำกัดห่วงสเกิร์ต/โล่ครอบไว้ที่ผนังด้านหนึ่งหลังจากชั้นแรก " -"สิ่งนี้มีประโยชน์ในบางครั้งเพื่ออนุรักษ์เส้นพลาสติก แต่อาจทำให้โครง/เส้นล้อมชิ้นงานบิดเบี้ยว/แตกร้าวได้" +msgid "Limits the skirt/draft shield loops to one wall after the first layer. This is useful, on occasion, to conserve filament but may cause the draft shield/skirt to warp / crack." +msgstr "จำกัดห่วงสเกิร์ต/โล่ครอบไว้ที่ผนังด้านหนึ่งหลังจากชั้นแรก สิ่งนี้มีประโยชน์ในบางครั้งเพื่ออนุรักษ์เส้นพลาสติก แต่อาจทำให้โครง/เส้นล้อมชิ้นงานบิดเบี้ยว/แตกร้าวได้" msgid "Draft shield" msgstr "โล่ร่าง" msgid "" -"A draft shield is useful to protect an ABS or ASA print from warping and " -"detaching from print bed due to wind draft. It is usually needed only with " -"open frame printers, i.e. without an enclosure.\n" +"A draft shield is useful to protect an ABS or ASA print from warping and detaching from print bed due to wind draft. It is usually needed only with open frame printers, i.e. without an enclosure.\n" "\n" -"Enabled = skirt is as tall as the highest printed object. Otherwise 'Skirt " -"height' is used.\n" -"Note: With the draft shield active, the skirt will be printed at skirt " -"distance from the object. Therefore, if brims are active it may intersect " -"with them. To avoid this, increase the skirt distance value.\n" +"Enabled = skirt is as tall as the highest printed object. Otherwise 'Skirt height' is used.\n" +"Note: With the draft shield active, the skirt will be printed at skirt distance from the object. Therefore, if brims are active it may intersect with them. To avoid this, increase the skirt distance value.\n" msgstr "" -"แผงครอบมีประโยชน์ในการปกป้องงานพิมพ์ ABS หรือ ASA " -"จากการบิดงอและการหลุดออกจากฐานพิมพ์เนื่องจากกระแสลม " -"โดยทั่วไปจำเป็นต้องใช้กับเครื่องพิมพ์แบบเปิดเฟรมเท่านั้น กล่าวคือ ไม่มีกล่องหุ้ม\n" +"แผงครอบมีประโยชน์ในการปกป้องงานพิมพ์ ABS หรือ ASA จากการบิดงอและการหลุดออกจากฐานพิมพ์เนื่องจากกระแสลม โดยทั่วไปจำเป็นต้องใช้กับเครื่องพิมพ์แบบเปิดเฟรมเท่านั้น กล่าวคือ ไม่มีกล่องหุ้ม\n" "\n" "Enabled = เส้นล้อมชิ้นงานสูงเท่ากับวัตถุที่พิมพ์สูงสุด มิฉะนั้น จะใช้ 'ความสูงของเส้นล้อมชิ้นงาน'\n" -"หมายเหตุ: เมื่อใช้งานดราฟชีลด์ เส้นล้อมชิ้นงานจะถูกพิมพ์ที่ระยะห่างจากเส้นล้อมชิ้นงานจากวัตถุ " -"ดังนั้นหากขอบยึดชิ้นงานยังทำงานอยู่ ขอบยึดชิ้นงานอาจตัดกัน เพื่อหลีกเลี่ยงปัญหานี้ " -"ให้เพิ่มค่าระยะห่างของเส้นล้อมชิ้นงาน\n" +"หมายเหตุ: เมื่อใช้งานดราฟชีลด์ เส้นล้อมชิ้นงานจะถูกพิมพ์ที่ระยะห่างจากเส้นล้อมชิ้นงานจากวัตถุ ดังนั้นหากขอบยึดชิ้นงานยังทำงานอยู่ ขอบยึดชิ้นงานอาจตัดกัน เพื่อหลีกเลี่ยงปัญหานี้ ให้เพิ่มค่าระยะห่างของเส้นล้อมชิ้นงาน\n" msgid "Enabled" msgstr "เปิดใช้" @@ -17292,9 +14606,7 @@ msgstr "เปิดใช้" msgid "Skirt type" msgstr "ประเภทเส้นล้อมชิ้นงาน" -msgid "" -"Combined - single skirt for all objects, Per object - individual object " -"skirt." +msgid "Combined - single skirt for all objects, Per object - individual object skirt." msgstr "รวม - เส้นล้อมชิ้นงานเดี่ยวสำหรับวัตถุทั้งหมด ต่อวัตถุ - เส้นล้อมชิ้นงานวัตถุแต่ละรายการ" msgid "Per object" @@ -17303,49 +14615,36 @@ msgstr "ต่อวัตถุ" msgid "Skirt loops" msgstr "ห่วงเส้นล้อมชิ้นงาน" -msgid "" -"This is the number of loops for the skirt. 0 means the skirt is disabled." +msgid "This is the number of loops for the skirt. 0 means the skirt is disabled." msgstr "จำนวนห่วงสำหรับเส้นล้อมชิ้นงาน ศูนย์หมายถึงปิดการใช้งานเส้นล้อมชิ้นงาน" msgid "Skirt speed" msgstr "ความเร็ว เส้นล้อมชิ้นงาน" msgid "Speed of skirt, in mm/s. Zero means use default layer extrusion speed." -msgstr "" -"ความเร็วของเส้นล้อมชิ้นงาน มีหน่วยเป็น mm/s ศูนย์หมายถึงใช้ความเร็วการอัดรีดเลเยอร์เริ่มต้น" +msgstr "ความเร็วของเส้นล้อมชิ้นงาน มีหน่วยเป็น mm/s ศูนย์หมายถึงใช้ความเร็วการอัดรีดเลเยอร์เริ่มต้น" msgid "Skirt minimum extrusion length" msgstr "ความยาวการอัดรีดขั้นต่ำของเส้นล้อมชิ้นงาน" msgid "" -"Minimum filament extrusion length in mm when printing the skirt. Zero means " -"this feature is disabled.\n" +"Minimum filament extrusion length in mm when printing the skirt. Zero means this feature is disabled.\n" "\n" -"Using a non-zero value is useful if the printer is set up to print without a " -"prime line.\n" -"Final number of loops is not taking into account while arranging or " -"validating objects distance. Increase loop number in such case." +"Using a non-zero value is useful if the printer is set up to print without a prime line.\n" +"Final number of loops is not taking into account while arranging or validating objects distance. Increase loop number in such case." msgstr "" -"ความยาวการอัดขึ้นรูปเส้นพลาสติกขั้นต่ำเป็นมม. เมื่อพิมพ์เส้นล้อมชิ้นงาน " -"ศูนย์หมายความว่าคุณลักษณะนี้ถูกปิดใช้งาน\n" +"ความยาวการอัดขึ้นรูปเส้นพลาสติกขั้นต่ำเป็นมม. เมื่อพิมพ์เส้นล้อมชิ้นงาน ศูนย์หมายความว่าคุณลักษณะนี้ถูกปิดใช้งาน\n" "\n" "การใช้ค่าที่ไม่ใช่ศูนย์จะมีประโยชน์หากเครื่องพิมพ์ได้รับการตั้งค่าให้พิมพ์โดยไม่มีเส้นสำคัญ\n" "จำนวนลูปสุดท้ายไม่ได้คำนึงถึงขณะจัดเรียงหรือตรวจสอบระยะห่างของวัตถุ เพิ่มจำนวนลูปในกรณีเช่นนี้" -msgid "" -"The printing speed in exported G-code will be slowed down when the estimated " -"layer time is shorter than this value in order to get better cooling for " -"these layers." -msgstr "" -"ความเร็วในการพิมพ์ใน G-code ที่ส่งออกจะลดลงเมื่อเวลาเลเยอร์โดยประมาณสั้นกว่าค่านี้ " -"เพื่อให้เลเยอร์เหล่านี้เย็นลงได้ดีขึ้น" +msgid "The printing speed in exported G-code will be slowed down when the estimated layer time is shorter than this value in order to get better cooling for these layers." +msgstr "ความเร็วในการพิมพ์ใน G-code ที่ส่งออกจะลดลงเมื่อเวลาเลเยอร์โดยประมาณสั้นกว่าค่านี้ เพื่อให้เลเยอร์เหล่านี้เย็นลงได้ดีขึ้น" msgid "Minimum sparse infill threshold" msgstr "เกณฑ์ไส้ในแบบกระจัดกระจายขั้นต่ำ" -msgid "" -"Sparse infill areas which are smaller than this threshold value are replaced " -"by internal solid infill." +msgid "Sparse infill areas which are smaller than this threshold value are replaced by internal solid infill." msgstr "พื้นที่ไส้ในแบบกระจัดกระจายที่เล็กกว่าค่าเกณฑ์นี้จะถูกแทนที่ด้วยไส้ในแบบทึบภายใน" msgid "" @@ -17369,89 +14668,44 @@ msgstr "" "เส้นพลาสติกสำหรับพิมพ์พื้นผิวด้านล่าง\n" "\"ค่าเริ่มต้น\" ใช้เส้นพลาสติกของวัตถุ/ชิ้นส่วนที่ใช้งานอยู่" -msgid "" -"Line width of internal solid infill. If expressed as a %, it will be " -"computed over the nozzle diameter." -msgstr "" -"ความกว้างของเส้นของ ไส้ใน ทึบภายใน หากแสดงเป็น % จะคำนวณตามเส้นผ่านศูนย์กลางของหัวฉีด" +msgid "Line width of internal solid infill. If expressed as a %, it will be computed over the nozzle diameter." +msgstr "ความกว้างของเส้นของ ไส้ใน ทึบภายใน หากแสดงเป็น % จะคำนวณตามเส้นผ่านศูนย์กลางของหัวฉีด" -msgid "" -"This is the speed for internal solid infill, not including the top or bottom " -"surface." +msgid "This is the speed for internal solid infill, not including the top or bottom surface." msgstr "ความเร็วของไส้ในของแข็งภายใน ไม่ใช่พื้นผิวด้านบนและด้านล่าง" -msgid "" -"This enables spiraling, which smooths out the Z moves of the outer contour " -"and turns a solid model into a single walled print with solid bottom layers. " -"The final generated model has no seam." -msgstr "" -"Spiralize จะทำให้การเคลื่อนที่ Z ของเส้นขอบด้านนอกเรียบเนียนขึ้น " -"และเปลี่ยนโมเดลทึบให้เป็นภาพพิมพ์ผนังเดี่ยวพร้อมชั้นล่างสุดทึบ โมเดลที่สร้างขึ้นขั้นสุดท้ายไม่มีรอยตะเข็บ" +msgid "This enables spiraling, which smooths out the Z moves of the outer contour and turns a solid model into a single walled print with solid bottom layers. The final generated model has no seam." +msgstr "Spiralize จะทำให้การเคลื่อนที่ Z ของเส้นขอบด้านนอกเรียบเนียนขึ้น และเปลี่ยนโมเดลทึบให้เป็นภาพพิมพ์ผนังเดี่ยวพร้อมชั้นล่างสุดทึบ โมเดลที่สร้างขึ้นขั้นสุดท้ายไม่มีรอยตะเข็บ" msgid "Smooth Spiral" msgstr "เกลียวเรียบ" -msgid "" -"Smooth Spiral smooths out X and Y moves as well, resulting in no visible " -"seam at all, even in the XY directions on walls that are not vertical." -msgstr "" -"Smooth Spiral ช่วยให้การเคลื่อนที่ของ X และ Y เป็นไปอย่างราบรื่น " -"ส่งผลให้มองไม่เห็นรอยตะเข็บเลย แม้แต่ในทิศทาง XY บนผนังที่ไม่ใช่แนวตั้งก็ตาม" +msgid "Smooth Spiral smooths out X and Y moves as well, resulting in no visible seam at all, even in the XY directions on walls that are not vertical." +msgstr "Smooth Spiral ช่วยให้การเคลื่อนที่ของ X และ Y เป็นไปอย่างราบรื่น ส่งผลให้มองไม่เห็นรอยตะเข็บเลย แม้แต่ในทิศทาง XY บนผนังที่ไม่ใช่แนวตั้งก็ตาม" msgid "Max XY Smoothing" msgstr "การปรับให้เรียบสูงสุด XY" #, no-c-format, no-boost-format -msgid "" -"Maximum distance to move points in XY to try to achieve a smooth spiral. If " -"expressed as a %, it will be computed over nozzle diameter." -msgstr "" -"ระยะทางสูงสุดเพื่อย้ายจุดใน XY เพื่อพยายามให้ได้เกลียวเรียบ หากแสดงเป็น % " -"จะคำนวณตามเส้นผ่านศูนย์กลางของหัวฉีด" +msgid "Maximum distance to move points in XY to try to achieve a smooth spiral. If expressed as a %, it will be computed over nozzle diameter." +msgstr "ระยะทางสูงสุดเพื่อย้ายจุดใน XY เพื่อพยายามให้ได้เกลียวเรียบ หากแสดงเป็น % จะคำนวณตามเส้นผ่านศูนย์กลางของหัวฉีด" msgid "Spiral starting flow ratio" msgstr "อัตราการไหลเริ่มต้นของเกลียว" #, no-c-format, no-boost-format -msgid "" -"Sets the starting flow ratio while transitioning from the last bottom layer " -"to the spiral. Normally the spiral transition scales the flow ratio from 0% " -"to 100% during the first loop which can in some cases lead to under " -"extrusion at the start of the spiral." -msgstr "" -"ตั้งค่าอัตราส่วนการไหลเริ่มต้นขณะเปลี่ยนจากชั้นล่างสุดสุดท้ายเป็นเกลียว " -"โดยปกติการเปลี่ยนผ่านของเกลียวจะปรับขนาดอัตราส่วนการไหลจาก 0% เป็น 100% ในระหว่างลูปแรก " -"ซึ่งในบางกรณีอาจนำไปสู่การรีดขึ้นรูปที่จุดเริ่มต้นของเกลียว" +msgid "Sets the starting flow ratio while transitioning from the last bottom layer to the spiral. Normally the spiral transition scales the flow ratio from 0% to 100% during the first loop which can in some cases lead to under extrusion at the start of the spiral." +msgstr "ตั้งค่าอัตราส่วนการไหลเริ่มต้นขณะเปลี่ยนจากชั้นล่างสุดสุดท้ายเป็นเกลียว โดยปกติการเปลี่ยนผ่านของเกลียวจะปรับขนาดอัตราส่วนการไหลจาก 0% เป็น 100% ในระหว่างลูปแรก ซึ่งในบางกรณีอาจนำไปสู่การรีดขึ้นรูปที่จุดเริ่มต้นของเกลียว" msgid "Spiral finishing flow ratio" msgstr "อัตราการไหลการตกแต่งเกลียว" #, no-c-format, no-boost-format -msgid "" -"Sets the finishing flow ratio while ending the spiral. Normally the spiral " -"transition scales the flow ratio from 100% to 0% during the last loop which " -"can in some cases lead to under extrusion at the end of the spiral." -msgstr "" -"ตั้งค่าอัตราส่วนการไหลขั้นสุดท้ายขณะสิ้นสุดเกลียว " -"โดยปกติการเปลี่ยนผ่านของเกลียวจะปรับขนาดอัตราส่วนการไหลจาก 100% เป็น 0% " -"ในระหว่างลูปสุดท้าย ซึ่งในบางกรณีอาจนำไปสู่การรีดขึ้นรูปที่ปลายเกลียว" +msgid "Sets the finishing flow ratio while ending the spiral. Normally the spiral transition scales the flow ratio from 100% to 0% during the last loop which can in some cases lead to under extrusion at the end of the spiral." +msgstr "ตั้งค่าอัตราส่วนการไหลขั้นสุดท้ายขณะสิ้นสุดเกลียว โดยปกติการเปลี่ยนผ่านของเกลียวจะปรับขนาดอัตราส่วนการไหลจาก 100% เป็น 0% ในระหว่างลูปสุดท้าย ซึ่งในบางกรณีอาจนำไปสู่การรีดขึ้นรูปที่ปลายเกลียว" -msgid "" -"If smooth or traditional mode is selected, a timelapse video will be " -"generated for each print. After each layer is printed, a snapshot is taken " -"with the chamber camera. All of these snapshots are composed into a " -"timelapse video when printing completes. If smooth mode is selected, the " -"toolhead will move to the excess chute after each layer is printed and then " -"take a snapshot. Since the melt filament may leak from the nozzle during the " -"process of taking a snapshot, a prime tower is required for smooth mode to " -"wipe the nozzle." -msgstr "" -"หากเลือกโหมดเรียบหรือโหมดดั้งเดิม วิดีโอไทม์แลปส์จะถูกสร้างขึ้นสำหรับการพิมพ์แต่ละครั้ง " -"หลังจากพิมพ์แต่ละชั้นแล้ว กล้องจะถ่ายภาพสแนปช็อตด้วยกล้องแชมเบอร์ " -"สแน็ปช็อตทั้งหมดนี้จะถูกประกอบเป็นวิดีโอไทม์แลปส์เมื่อการพิมพ์เสร็จสิ้น หากเลือกโหมดเรียบ " -"หัวเครื่องมือจะย้ายไปยังรางส่วนเกินหลังจากพิมพ์แต่ละเลเยอร์แล้วจึงถ่ายภาพสแน็ปช็อต " -"เนื่องจากเส้นพลาสติกที่หลอมละลายอาจรั่วไหลออกจากหัวฉีดในระหว่างขั้นตอนการถ่ายภาพ " -"จึงจำเป็นต้องมีไพรม์ทาวเวอร์เพื่อให้โหมดราบรื่นในการเช็ดหัวฉีด" +msgid "If smooth or traditional mode is selected, a timelapse video will be generated for each print. After each layer is printed, a snapshot is taken with the chamber camera. All of these snapshots are composed into a timelapse video when printing completes. If smooth mode is selected, the toolhead will move to the excess chute after each layer is printed and then take a snapshot. Since the melt filament may leak from the nozzle during the process of taking a snapshot, a prime tower is required for smooth mode to wipe the nozzle." +msgstr "หากเลือกโหมดเรียบหรือโหมดดั้งเดิม วิดีโอไทม์แลปส์จะถูกสร้างขึ้นสำหรับการพิมพ์แต่ละครั้ง หลังจากพิมพ์แต่ละชั้นแล้ว กล้องจะถ่ายภาพสแนปช็อตด้วยกล้องแชมเบอร์ สแน็ปช็อตทั้งหมดนี้จะถูกประกอบเป็นวิดีโอไทม์แลปส์เมื่อการพิมพ์เสร็จสิ้น หากเลือกโหมดเรียบ หัวเครื่องมือจะย้ายไปยังรางส่วนเกินหลังจากพิมพ์แต่ละเลเยอร์แล้วจึงถ่ายภาพสแน็ปช็อต เนื่องจากเส้นพลาสติกที่หลอมละลายอาจรั่วไหลออกจากหัวฉีดในระหว่างขั้นตอนการถ่ายภาพ จึงจำเป็นต้องมีไพรม์ทาวเวอร์เพื่อให้โหมดราบรื่นในการเช็ดหัวฉีด" msgid "Traditional" msgstr "แบบดั้งเดิม" @@ -17460,13 +14714,8 @@ msgid "Temperature variation" msgstr "การเปลี่ยนแปลงของอุณหภูมิ" #. TRN PrintSettings : "Ooze prevention" > "Temperature variation" -msgid "" -"Temperature difference to be applied when an extruder is not active. The " -"value is not used when 'idle_temperature' in filament settings is set to non-" -"zero value." -msgstr "" -"ความแตกต่างของอุณหภูมิที่จะใช้เมื่อชุดดันเส้นไม่ทำงาน ค่าจะไม่ถูกใช้เมื่อตั้งค่า " -"'idle_temperature' ในการตั้งค่าเส้นพลาสติกเป็นค่าที่ไม่ใช่ศูนย์" +msgid "Temperature difference to be applied when an extruder is not active. The value is not used when 'idle_temperature' in filament settings is set to non-zero value." +msgstr "ความแตกต่างของอุณหภูมิที่จะใช้เมื่อชุดดันเส้นไม่ทำงาน ค่าจะไม่ถูกใช้เมื่อตั้งค่า 'idle_temperature' ในการตั้งค่าเส้นพลาสติกเป็นค่าที่ไม่ใช่ศูนย์" msgid "∆℃" msgstr "∆℃" @@ -17474,36 +14723,17 @@ msgstr "∆℃" msgid "Preheat time" msgstr "เวลาอุ่นเครื่อง" -msgid "" -"To reduce the waiting time after tool change, Orca can preheat the next tool " -"while the current tool is still in use. This setting specifies the time in " -"seconds to preheat the next tool. Orca will insert a M104 command to preheat " -"the tool in advance." -msgstr "" -"เพื่อลดเวลารอหลังจากเปลี่ยนเครื่องมือ Orca " -"สามารถอุ่นเครื่องมือถัดไปในขณะที่เครื่องมือปัจจุบันยังคงใช้งานอยู่ " -"การตั้งค่านี้ระบุเวลาเป็นวินาทีเพื่ออุ่นเครื่องมือถัดไป Orca จะแทรกคำสั่ง M104 เพื่ออุ่นเครื่องล่วงหน้า" +msgid "To reduce the waiting time after tool change, Orca can preheat the next tool while the current tool is still in use. This setting specifies the time in seconds to preheat the next tool. Orca will insert a M104 command to preheat the tool in advance." +msgstr "เพื่อลดเวลารอหลังจากเปลี่ยนเครื่องมือ Orca สามารถอุ่นเครื่องมือถัดไปในขณะที่เครื่องมือปัจจุบันยังคงใช้งานอยู่ การตั้งค่านี้ระบุเวลาเป็นวินาทีเพื่ออุ่นเครื่องมือถัดไป Orca จะแทรกคำสั่ง M104 เพื่ออุ่นเครื่องล่วงหน้า" msgid "Preheat steps" msgstr "อุ่นเครื่องตามขั้นตอน" -msgid "" -"Insert multiple preheat commands (e.g. M104.1). Only useful for Prusa XL. " -"For other printers, please set it to 1." -msgstr "" -"ใส่คำสั่งอุ่นล่วงหน้าหลายคำสั่ง (เช่น M104.1) มีประโยชน์สำหรับ Prusa XL เท่านั้น " -"สำหรับเครื่องพิมพ์อื่นๆ โปรดตั้งค่าเป็น 1" +msgid "Insert multiple preheat commands (e.g. M104.1). Only useful for Prusa XL. For other printers, please set it to 1." +msgstr "ใส่คำสั่งอุ่นล่วงหน้าหลายคำสั่ง (เช่น M104.1) มีประโยชน์สำหรับ Prusa XL เท่านั้น สำหรับเครื่องพิมพ์อื่นๆ โปรดตั้งค่าเป็น 1" -msgid "" -"G-code written at the very top of the output file, before any other content. " -"Useful for adding metadata that printer firmware reads from the first lines " -"of the file (e.g. estimated print time, filament usage). Supports " -"placeholders like {print_time_sec} and {used_filament_length}." -msgstr "" -"G-code เขียนไว้ที่ด้านบนสุดของไฟล์เอาต์พุต ก่อนเนื้อหาอื่นๆ " -"มีประโยชน์สำหรับการเพิ่มข้อมูลเมตาที่เฟิร์มแวร์เครื่องพิมพ์อ่านจากบรรทัดแรกของไฟล์ (เช่น " -"เวลาในการพิมพ์โดยประมาณ การใช้เส้นพลาสติก) รองรับตัวยึดตำแหน่ง เช่น {print_time_sec} " -"และ {used_filament_length}" +msgid "G-code written at the very top of the output file, before any other content. Useful for adding metadata that printer firmware reads from the first lines of the file (e.g. estimated print time, filament usage). Supports placeholders like {print_time_sec} and {used_filament_length}." +msgstr "G-code เขียนไว้ที่ด้านบนสุดของไฟล์เอาต์พุต ก่อนเนื้อหาอื่นๆ มีประโยชน์สำหรับการเพิ่มข้อมูลเมตาที่เฟิร์มแวร์เครื่องพิมพ์อ่านจากบรรทัดแรกของไฟล์ (เช่น เวลาในการพิมพ์โดยประมาณ การใช้เส้นพลาสติก) รองรับตัวยึดตำแหน่ง เช่น {print_time_sec} และ {used_filament_length}" msgid "Start G-code" msgstr "เริ่ม G-code" @@ -17523,30 +14753,14 @@ msgstr "ใช้หัวฉีดเดี่ยวเพื่อพิมพ msgid "Manual Filament Change" msgstr "การเปลี่ยนเส้นพลาสติกด้วยตนเอง" -msgid "" -"Enable this option to omit the custom Change filament G-code only at the " -"beginning of the print. The tool change command (e.g., T0) will be skipped " -"throughout the entire print. This is useful for manual multi-material " -"printing, where we use M600/PAUSE to trigger the manual filament change " -"action." -msgstr "" -"เปิดใช้งานตัวเลือกนี้เพื่อละเว้น G-code " -"เปลี่ยนฟิลาเมนต์แบบกำหนดเองเฉพาะตอนเริ่มต้นการพิมพ์เท่านั้น คำสั่งเปลี่ยนเครื่องมือ (เช่น T0) " -"จะถูกข้ามไปตลอดการพิมพ์ทั้งหมด สิ่งนี้มีประโยชน์สำหรับการพิมพ์หลายวัสดุด้วยตนเอง โดยที่เราใช้ " -"M600/PAUSE เพื่อกระตุ้นการดำเนินการเปลี่ยนเส้นพลาสติกด้วยตนเอง" +msgid "Enable this option to omit the custom Change filament G-code only at the beginning of the print. The tool change command (e.g., T0) will be skipped throughout the entire print. This is useful for manual multi-material printing, where we use M600/PAUSE to trigger the manual filament change action." +msgstr "เปิดใช้งานตัวเลือกนี้เพื่อละเว้น G-code เปลี่ยนฟิลาเมนต์แบบกำหนดเองเฉพาะตอนเริ่มต้นการพิมพ์เท่านั้น คำสั่งเปลี่ยนเครื่องมือ (เช่น T0) จะถูกข้ามไปตลอดการพิมพ์ทั้งหมด สิ่งนี้มีประโยชน์สำหรับการพิมพ์หลายวัสดุด้วยตนเอง โดยที่เราใช้ M600/PAUSE เพื่อกระตุ้นการดำเนินการเปลี่ยนเส้นพลาสติกด้วยตนเอง" msgid "Wipe tower type" msgstr "ชนิดทาวเวอร์เช็ด" -msgid "" -"Choose the wipe tower implementation for multi-material prints. Type 1 is " -"recommended for Bambu and Qidi printers with a filament cutter. Type 2 " -"offers better compatibility with multi-tool and MMU printers and provide " -"overall better compatibility." -msgstr "" -"เลือกการใช้งานไวด์ทาวเวอร์สำหรับการพิมพ์แบบหลายวัสดุ แนะนำให้ใช้ประเภท 1 สำหรับเครื่องพิมพ์ " -"Bambu และ Qidi ที่มีเครื่องตัดเส้นพลาสติก Type 2 " -"ให้ความเข้ากันได้ที่ดีกว่ากับเครื่องพิมพ์หลายเครื่องมือและ MMU และให้ความเข้ากันได้โดยรวมดีขึ้น" +msgid "Choose the wipe tower implementation for multi-material prints. Type 1 is recommended for Bambu and Qidi printers with a filament cutter. Type 2 offers better compatibility with multi-tool and MMU printers and provide overall better compatibility." +msgstr "เลือกการใช้งานไวด์ทาวเวอร์สำหรับการพิมพ์แบบหลายวัสดุ แนะนำให้ใช้ประเภท 1 สำหรับเครื่องพิมพ์ Bambu และ Qidi ที่มีเครื่องตัดเส้นพลาสติก Type 2 ให้ความเข้ากันได้ที่ดีกว่ากับเครื่องพิมพ์หลายเครื่องมือและ MMU และให้ความเข้ากันได้โดยรวมดีขึ้น" msgid "Type 1" msgstr "ประเภทที่ 1" @@ -17566,63 +14780,31 @@ msgstr "เปิดใช้งานการอัดกระแทกเส msgid "Tool change on wipe tower" msgstr "การเปลี่ยนเครื่องมือบนไวด์ทาวเวอร์" -msgid "" -"Force the toolhead to travel to the wipe tower before issuing the tool " -"change command (Tx). Only relevant for multi-extruder (multi-toolhead) " -"printers using a Type 2 wipe tower. By default Orca skips the travel on " -"multi-toolhead machines because the firmware handles the head swap, which " -"can result in the Tx command being issued above the printed part. Enable " -"this option if you want the tool change to always be issued above the wipe " -"tower instead." -msgstr "" -"บังคับให้หัวเครื่องมือเคลื่อนที่ไปที่หอเช็ดก่อนที่จะออกคำสั่งเปลี่ยนเครื่องมือ (Tx) " -"เกี่ยวข้องเฉพาะกับเครื่องพิมพ์ที่มีชุดดันเส้นหลายเครื่อง (หลายหัวเครื่องมือ) ที่ใช้แผ่นเช็ดแบบ Type 2 " -"ตามค่าเริ่มต้น Orca " -"จะข้ามการเดินทางบนเครื่องที่มีหัวเครื่องมือหลายหัวเนื่องจากเฟิร์มแวร์จัดการการสลับหัว " -"ซึ่งอาจส่งผลให้มีการออกคำสั่ง Tx เหนือส่วนที่พิมพ์ " -"เปิดใช้งานตัวเลือกนี้หากคุณต้องการให้ทำการเปลี่ยนแปลงเครื่องมือเหนือหอเช็ดแทนเสมอ" +msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." +msgstr "บังคับให้หัวเครื่องมือเคลื่อนที่ไปที่หอเช็ดก่อนที่จะออกคำสั่งเปลี่ยนเครื่องมือ (Tx) เกี่ยวข้องเฉพาะกับเครื่องพิมพ์ที่มีชุดดันเส้นหลายเครื่อง (หลายหัวเครื่องมือ) ที่ใช้แผ่นเช็ดแบบ Type 2 ตามค่าเริ่มต้น Orca จะข้ามการเดินทางบนเครื่องที่มีหัวเครื่องมือหลายหัวเนื่องจากเฟิร์มแวร์จัดการการสลับหัว ซึ่งอาจส่งผลให้มีการออกคำสั่ง Tx เหนือส่วนที่พิมพ์ เปิดใช้งานตัวเลือกนี้หากคุณต้องการให้ทำการเปลี่ยนแปลงเครื่องมือเหนือหอเช็ดแทนเสมอ" msgid "No sparse layers (beta)" msgstr "ไม่มีชั้นกระจัดกระจาย (เบต้า)" -msgid "" -"If enabled, the wipe tower will not be printed on layers with no tool " -"changes. On layers with a tool change, extruder will travel downward to " -"print the wipe tower. User is responsible for ensuring there is no collision " -"with the print." -msgstr "" -"หากเปิดใช้งาน หอเช็ดจะไม่ถูกพิมพ์บนเลเยอร์โดยไม่มีการเปลี่ยนแปลงเครื่องมือ " -"บนเลเยอร์ที่มีการเปลี่ยนเครื่องมือ ชุดดันเส้นจะเคลื่อนลงด้านล่างเพื่อพิมพ์ไวด์ทาวเวอร์ " -"ผู้ใช้มีหน้าที่รับผิดชอบในการตรวจสอบให้แน่ใจว่าไม่มีการชนกันกับงานพิมพ์" +msgid "If enabled, the wipe tower will not be printed on layers with no tool changes. On layers with a tool change, extruder will travel downward to print the wipe tower. User is responsible for ensuring there is no collision with the print." +msgstr "หากเปิดใช้งาน หอเช็ดจะไม่ถูกพิมพ์บนเลเยอร์โดยไม่มีการเปลี่ยนแปลงเครื่องมือ บนเลเยอร์ที่มีการเปลี่ยนเครื่องมือ ชุดดันเส้นจะเคลื่อนลงด้านล่างเพื่อพิมพ์ไวด์ทาวเวอร์ ผู้ใช้มีหน้าที่รับผิดชอบในการตรวจสอบให้แน่ใจว่าไม่มีการชนกันกับงานพิมพ์" msgid "Prime all printing extruders" msgstr "ใช้ชุดดันเส้นการพิมพ์ทั้งหมด" -msgid "" -"If enabled, all printing extruders will be primed at the front edge of the " -"print bed at the start of the print." -msgstr "" -"หากเปิดใช้งาน ชุดดันเส้นการพิมพ์ทั้งหมดจะถูกลงสีพื้นที่ขอบด้านหน้าของฐานพิมพ์เมื่อเริ่มต้นการพิมพ์" +msgid "If enabled, all printing extruders will be primed at the front edge of the print bed at the start of the print." +msgstr "หากเปิดใช้งาน ชุดดันเส้นการพิมพ์ทั้งหมดจะถูกลงสีพื้นที่ขอบด้านหน้าของฐานพิมพ์เมื่อเริ่มต้นการพิมพ์" msgid "Slice gap closing radius" msgstr "รัศมีการปิดช่องว่างของ Slice" -msgid "" -"Cracks smaller than 2x gap closing radius are being filled during the " -"triangle mesh slicing. The gap closing operation may reduce the final print " -"resolution, therefore it is advisable to keep the value reasonably low." -msgstr "" -"รอยแตกร้าวที่มีขนาดเล็กกว่า 2 " -"เท่าของรัศมีการปิดช่องว่างจะถูกไส้ในเต็มระหว่างการตัดตาข่ายสามเหลี่ยม " -"การดำเนินการปิดช่องว่างอาจลดความละเอียดในการพิมพ์ขั้นสุดท้าย " -"ดังนั้นจึงแนะนำให้รักษาค่าให้ต่ำพอสมควร" +msgid "Cracks smaller than 2x gap closing radius are being filled during the triangle mesh slicing. The gap closing operation may reduce the final print resolution, therefore it is advisable to keep the value reasonably low." +msgstr "รอยแตกร้าวที่มีขนาดเล็กกว่า 2 เท่าของรัศมีการปิดช่องว่างจะถูกไส้ในเต็มระหว่างการตัดตาข่ายสามเหลี่ยม การดำเนินการปิดช่องว่างอาจลดความละเอียดในการพิมพ์ขั้นสุดท้าย ดังนั้นจึงแนะนำให้รักษาค่าให้ต่ำพอสมควร" msgid "Slicing Mode" msgstr "โหมดการแบ่งส่วน" -msgid "" -"Use \"Even-odd\" for 3DLabPrint airplane models. Use \"Close holes\" to " -"close all holes in the model." +msgid "Use \"Even-odd\" for 3DLabPrint airplane models. Use \"Close holes\" to close all holes in the model." msgstr "ใช้ \"เลขคู่\" สำหรับโมเดลเครื่องบิน 3DLabPrint ใช้ \"ปิดรู\" เพื่อปิดรูทั้งหมดในโมเดล" msgid "Regular" @@ -17637,15 +14819,8 @@ msgstr "ปิดรู" msgid "Z offset" msgstr "ออฟเซ็ต Z" -msgid "" -"This value will be added (or subtracted) from all the Z coordinates in the " -"output G-code. It is used to compensate for bad Z endstop position: for " -"example, if your endstop zero actually leaves the nozzle 0.3mm far from the " -"print bed, set this to -0.3 (or fix your endstop)." -msgstr "" -"ค่านี้จะถูกบวก (หรือลบ) จากพิกัด Z ทั้งหมดใน G-code เอาท์พุต ใช้เพื่อชดเชยตำแหน่งปลาย Z " -"ที่ไม่ดี: ตัวอย่างเช่น หากศูนย์ปลายสุดของคุณปล่อยหัวฉีดให้ห่างจากฐานพิมพ์ 0.3 มม. ให้ตั้งค่านี้เป็น " -"-0.3 (หรือแก้ไขจุดปลายสุดของคุณ)" +msgid "This value will be added (or subtracted) from all the Z coordinates in the output G-code. It is used to compensate for bad Z endstop position: for example, if your endstop zero actually leaves the nozzle 0.3mm far from the print bed, set this to -0.3 (or fix your endstop)." +msgstr "ค่านี้จะถูกบวก (หรือลบ) จากพิกัด Z ทั้งหมดใน G-code เอาท์พุต ใช้เพื่อชดเชยตำแหน่งปลาย Z ที่ไม่ดี: ตัวอย่างเช่น หากศูนย์ปลายสุดของคุณปล่อยหัวฉีดให้ห่างจากฐานพิมพ์ 0.3 มม. ให้ตั้งค่านี้เป็น -0.3 (หรือแก้ไขจุดปลายสุดของคุณ)" msgid "Enable support" msgstr "เปิดใช้ส่วนรองรับ" @@ -17653,13 +14828,8 @@ msgstr "เปิดใช้ส่วนรองรับ" msgid "This enables support generation." msgstr "เปิดใช้การสร้างส่วนรองรับ" -msgid "" -"Normal (auto) and Tree (auto) are used to generate support automatically. If " -"Normal (manual) or Tree (manual) is selected, only support enforcers are " -"generated." -msgstr "" -"ปกติ (อัตโนมัติ) และ Tree (อัตโนมัติ) ใช้เพื่อสร้างส่วนรองรับโดยอัตโนมัติ หากเลือก ปกติ " -"(ด้วยตนเอง) หรือ ต้นไม้ (ด้วยตนเอง) จะมีการสร้างเฉพาะกำหนดส่วนรองรับเท่านั้น" +msgid "Normal (auto) and Tree (auto) are used to generate support automatically. If Normal (manual) or Tree (manual) is selected, only support enforcers are generated." +msgstr "ปกติ (อัตโนมัติ) และ Tree (อัตโนมัติ) ใช้เพื่อสร้างส่วนรองรับโดยอัตโนมัติ หากเลือก ปกติ (ด้วยตนเอง) หรือ ต้นไม้ (ด้วยตนเอง) จะมีการสร้างเฉพาะกำหนดส่วนรองรับเท่านั้น" msgid "Normal (auto)" msgstr "ปกติ (อัตโนมัติ)" @@ -17700,9 +14870,7 @@ msgstr "อย่าสร้างส่วนรองรับบนพื้ msgid "Support critical regions only" msgstr "รองรับภูมิภาคที่สำคัญเท่านั้น" -msgid "" -"Only create support for critical regions including sharp tail, cantilever, " -"etc." +msgid "Only create support for critical regions including sharp tail, cantilever, etc." msgstr "สร้างการรองรับเฉพาะบริเวณที่สำคัญเท่านั้น เช่น หางแหลม คานยื่น เป็นต้น" msgid "Ignore small overhangs" @@ -17720,44 +14888,30 @@ msgstr "ช่องว่าง Z ระหว่างด้านบนขอ msgid "Bottom Z distance" msgstr "ระยะ Z ล่าง" -msgid "" -"Z gap between the object and the support bottom. If Support Top Z Distance " -"is 0 and the bottom has interface layers, this value is ignored and the " -"support is printed in direct contact with the object (no gap)." -msgstr "" -"ช่องว่าง Z ระหว่างวัตถุและฐานรองรับ หากระยะ Z สูงสุดที่รองรับคือ 0 " -"และด้านล่างมีเลเยอร์อินเทอร์เฟซ ค่านี้จะถูกละเว้น และส่วนรองรับจะถูกพิมพ์โดยสัมผัสโดยตรงกับวัตถุ " -"(ไม่มีช่องว่าง)" +msgid "Z gap between the object and the support bottom. If Support Top Z Distance is 0 and the bottom has interface layers, this value is ignored and the support is printed in direct contact with the object (no gap)." +msgstr "ช่องว่าง Z ระหว่างวัตถุและฐานรองรับ หากระยะ Z สูงสุดที่รองรับคือ 0 และด้านล่างมีเลเยอร์อินเทอร์เฟซ ค่านี้จะถูกละเว้น และส่วนรองรับจะถูกพิมพ์โดยสัมผัสโดยตรงกับวัตถุ (ไม่มีช่องว่าง)" msgid "Support/raft base" msgstr "ส่วนรองรับ/ฐานฐานรองชิ้นงาน" msgid "" "Filament to print support base and raft.\n" -"\"Default\" means no specific filament for support and current filament is " -"used." -msgstr "" -"เส้นพลาสติกพิมพ์ฐานรองรับและฐานรองชิ้นงาน \"ค่าเริ่มต้น\" " -"หมายถึงไม่มีการใช้เส้นพลาสติกเฉพาะเพื่อรองรับและใช้เส้นพลาสติกปัจจุบัน" +"\"Default\" means no specific filament for support and current filament is used." +msgstr "เส้นพลาสติกพิมพ์ฐานรองรับและฐานรองชิ้นงาน \"ค่าเริ่มต้น\" หมายถึงไม่มีการใช้เส้นพลาสติกเฉพาะเพื่อรองรับและใช้เส้นพลาสติกปัจจุบัน" msgid "Avoid interface filament for base" msgstr "หลีกเลี่ยงเส้นพลาสติกอินเทอร์เฟซสำหรับฐาน" -msgid "" -"Avoid using support interface filament to print support base if possible." +msgid "Avoid using support interface filament to print support base if possible." msgstr "หลีกเลี่ยงการใช้เส้นพลาสติกส่วนต่อประสานเพื่อพิมพ์ฐานรองรับหากเป็นไปได้" -msgid "" -"Line width of support. If expressed as a %, it will be computed over the " -"nozzle diameter." +msgid "Line width of support. If expressed as a %, it will be computed over the nozzle diameter." msgstr "ความกว้างของเส้นรองรับ หากแสดงเป็น % จะคำนวณตามเส้นผ่านศูนย์กลางของหัวฉีด" msgid "Loop pattern interface" msgstr "รูปแบบการใช้อินเทอร์เฟซแบบวนซ้ำ" -msgid "" -"This covers the top contact layer of the supports with loops. It is disabled " -"by default." +msgid "This covers the top contact layer of the supports with loops. It is disabled by default." msgstr "ครอบคลุมชั้นสัมผัสด้านบนของส่วนรองรับด้วยลูป ปิดใช้งานตามค่าเริ่มต้น" msgid "Support/raft interface" @@ -17765,11 +14919,8 @@ msgstr "ส่วนต่อประสานส่วนรองรับ / msgid "" "Filament to print support interface.\n" -"\"Default\" means no specific filament for support interface and current " -"filament is used." -msgstr "" -"เส้นพลาสติกสำหรับพิมพ์ส่วนต่อประสานส่วนรองรับ \"ค่าเริ่มต้น\" " -"หมายความว่าไม่มีการใช้ฟิลาเมนต์เฉพาะสำหรับอินเทอร์เฟซรองรับและมีการใช้ฟิลาเมนต์ปัจจุบัน" +"\"Default\" means no specific filament for support interface and current filament is used." +msgstr "เส้นพลาสติกสำหรับพิมพ์ส่วนต่อประสานส่วนรองรับ \"ค่าเริ่มต้น\" หมายความว่าไม่มีการใช้ฟิลาเมนต์เฉพาะสำหรับอินเทอร์เฟซรองรับและมีการใช้ฟิลาเมนต์ปัจจุบัน" msgid "Top interface layers" msgstr "เลเยอร์อินเทอร์เฟซบนสุด" @@ -17811,22 +14962,15 @@ msgstr "รูปแบบฐาน" msgid "" "Line pattern of support.\n" "\n" -"The Default option for Tree supports is Hollow, which means no base pattern. " -"For other support types, the Default option is the Rectilinear pattern.\n" +"The Default option for Tree supports is Hollow, which means no base pattern. For other support types, the Default option is the Rectilinear pattern.\n" "\n" -"NOTE: For Organic supports, the two walls are supported only with the Hollow/" -"Default base pattern. The Lightning base pattern is supported only by Tree " -"Slim/Strong/Hybrid supports. For the other support types, the Rectilinear " -"will be used instead of Lightning." +"NOTE: For Organic supports, the two walls are supported only with the Hollow/Default base pattern. The Lightning base pattern is supported only by Tree Slim/Strong/Hybrid supports. For the other support types, the Rectilinear will be used instead of Lightning." msgstr "" "รูปแบบเส้นของส่วนรองรับ\n" "\n" -"ตัวเลือกเริ่มต้นสำหรับส่วนรองรับ Tree คือ Hollow ซึ่งหมายความว่าไม่มีรูปแบบพื้นฐาน " -"สำหรับส่วนรองรับประเภทอื่นๆ ตัวเลือกเริ่มต้นคือรูปแบบเส้นตรง\n" +"ตัวเลือกเริ่มต้นสำหรับส่วนรองรับ Tree คือ Hollow ซึ่งหมายความว่าไม่มีรูปแบบพื้นฐาน สำหรับส่วนรองรับประเภทอื่นๆ ตัวเลือกเริ่มต้นคือรูปแบบเส้นตรง\n" "\n" -"หมายเหตุ: สำหรับส่วนรองรับทั่วไป ผนังทั้งสองได้รับการรองรับเฉพาะรูปแบบฐานกลวง/" -"ค่าเริ่มต้นเท่านั้น รูปแบบฐาน Lightning รองรับเฉพาะ Tree Slim/Strong/Hybrid เท่านั้น " -"สำหรับส่วนรองรับประเภทอื่นๆ Rectilinear จะถูกใช้แทน Lightning" +"หมายเหตุ: สำหรับส่วนรองรับทั่วไป ผนังทั้งสองได้รับการรองรับเฉพาะรูปแบบฐานกลวง/ค่าเริ่มต้นเท่านั้น รูปแบบฐาน Lightning รองรับเฉพาะ Tree Slim/Strong/Hybrid เท่านั้น สำหรับส่วนรองรับประเภทอื่นๆ Rectilinear จะถูกใช้แทน Lightning" msgid "Rectilinear grid" msgstr "ตารางเป็นเส้นตรง" @@ -17837,13 +14981,8 @@ msgstr "กลวง" msgid "Interface pattern" msgstr "รูปแบบอินเทอร์เฟซ" -msgid "" -"This is the line pattern for support interfaces. The default pattern for non-" -"soluble support interfaces is Rectilinear while the default pattern for " -"soluble support interfaces is Concentric." -msgstr "" -"รูปแบบเส้นของอินเทอร์เฟซส่วนรองรับ รูปแบบเริ่มต้นสำหรับอินเทอร์เฟซส่วนรองรับที่ไม่ละลายน้ำคือ " -"Rectilinear ในขณะที่รูปแบบเริ่มต้นสำหรับอินเทอร์เฟซส่วนรองรับที่ละลายน้ำได้คือ Concentric" +msgid "This is the line pattern for support interfaces. The default pattern for non-soluble support interfaces is Rectilinear while the default pattern for soluble support interfaces is Concentric." +msgstr "รูปแบบเส้นของอินเทอร์เฟซส่วนรองรับ รูปแบบเริ่มต้นสำหรับอินเทอร์เฟซส่วนรองรับที่ไม่ละลายน้ำคือ Rectilinear ในขณะที่รูปแบบเริ่มต้นสำหรับอินเทอร์เฟซส่วนรองรับที่ละลายน้ำได้คือ Concentric" msgid "Rectilinear Interlaced" msgstr "เป็นเส้นตรงอินเทอร์เลซ" @@ -17864,20 +15003,11 @@ msgid "This is the speed for support." msgstr "ความเร็วของส่วนรองรับ" msgid "" -"Style and shape of the support. For normal support, projecting the supports " -"into a regular grid will create more stable supports (default), while snug " -"support towers will save material and reduce object scarring.\n" -"For tree support, slim and organic style will merge branches more " -"aggressively and save a lot of material (default organic), while hybrid " -"style will create similar structure to normal support under large flat " -"overhangs." +"Style and shape of the support. For normal support, projecting the supports into a regular grid will create more stable supports (default), while snug support towers will save material and reduce object scarring.\n" +"For tree support, slim and organic style will merge branches more aggressively and save a lot of material (default organic), while hybrid style will create similar structure to normal support under large flat overhangs." msgstr "" -"สไตล์และรูปทรงของส่วนรองรับ สำหรับการรองรับตามปกติ " -"การฉายส่วนรองรับลงในตารางปกติจะสร้างการรองรับที่มีความเสถียรมากขึ้น (ค่าเริ่มต้น) " -"ในขณะที่เสารองรับที่สบายจะช่วยประหยัดวัสดุและลดการเกิดแผลเป็นของวัตถุ\n" -"สำหรับการรองรับแบบต้นไม้ รูปแบบที่เพรียวบางและออร์แกนิกจะผสานกิ่งก้านเข้าด้วยกันอย่างดุดันยิ่งขึ้น " -"และประหยัดวัสดุได้มาก (ค่าเริ่มต้นแบบออร์แกนิก) " -"ในขณะที่รูปแบบไฮบริดจะสร้างโครงสร้างที่คล้ายกันกับการรองรับปกติภายใต้ส่วนยื่นแบนขนาดใหญ่" +"สไตล์และรูปทรงของส่วนรองรับ สำหรับการรองรับตามปกติ การฉายส่วนรองรับลงในตารางปกติจะสร้างการรองรับที่มีความเสถียรมากขึ้น (ค่าเริ่มต้น) ในขณะที่เสารองรับที่สบายจะช่วยประหยัดวัสดุและลดการเกิดแผลเป็นของวัตถุ\n" +"สำหรับการรองรับแบบต้นไม้ รูปแบบที่เพรียวบางและออร์แกนิกจะผสานกิ่งก้านเข้าด้วยกันอย่างดุดันยิ่งขึ้น และประหยัดวัสดุได้มาก (ค่าเริ่มต้นแบบออร์แกนิก) ในขณะที่รูปแบบไฮบริดจะสร้างโครงสร้างที่คล้ายกันกับการรองรับปกติภายใต้ส่วนยื่นแบนขนาดใหญ่" msgid "Default (Grid/Organic)" msgstr "ค่าเริ่มต้น (กริด/ออร์แกนิก)" @@ -17900,92 +15030,55 @@ msgstr "ต้นไม้ไฮบริด" msgid "Independent support layer height" msgstr "ความสูงของชั้นรองรับอิสระ" -msgid "" -"Support layer uses layer height independent with object layer. This is to " -"support customizing Z-gap and save print time. This option will be invalid " -"when the prime tower is enabled." -msgstr "" -"เลเยอร์ส่วนรองรับใช้ความสูงของเลเยอร์ที่เป็นอิสระจากเลเยอร์วัตถุ เพื่อรองรับการปรับแต่ง Z-gap " -"และประหยัดเวลาในการพิมพ์ ตัวเลือกนี้จะไม่ถูกต้องเมื่อเปิดใช้งานไพรม์ทาวเวอร์" +msgid "Support layer uses layer height independent with object layer. This is to support customizing Z-gap and save print time. This option will be invalid when the prime tower is enabled." +msgstr "เลเยอร์ส่วนรองรับใช้ความสูงของเลเยอร์ที่เป็นอิสระจากเลเยอร์วัตถุ เพื่อรองรับการปรับแต่ง Z-gap และประหยัดเวลาในการพิมพ์ ตัวเลือกนี้จะไม่ถูกต้องเมื่อเปิดใช้งานไพรม์ทาวเวอร์" msgid "Threshold angle" msgstr "มุมเกณฑ์" msgid "" -"Support will be generated for overhangs whose slope angle is below the " -"threshold. The smaller this value is, the steeper the overhang that can be " -"printed without support.\n" -"Note: If set to 0, normal supports use the Threshold overlap instead, while " -"tree supports fall back to a default value of 30." +"Support will be generated for overhangs whose slope angle is below the threshold. The smaller this value is, the steeper the overhang that can be printed without support.\n" +"Note: If set to 0, normal supports use the Threshold overlap instead, while tree supports fall back to a default value of 30." msgstr "" -"ส่วนรองรับจะถูกสร้างขึ้นสำหรับส่วนยื่นที่มีมุมลาดต่ำกว่าเกณฑ์ ยิ่งค่านี้น้อยลง " -"ระยะยื่นที่สามารถพิมพ์โดยไม่ต้องรองรับก็จะยิ่งชันมากขึ้น\n" -"หมายเหตุ: หากตั้งค่าเป็น 0 ส่วนรองรับปกติจะใช้ Threshold ทับซ้อนกันแทน " -"ในขณะที่ส่วนรองรับแบบทรีถอยกลับไปเป็นค่าเริ่มต้นที่ 30" +"ส่วนรองรับจะถูกสร้างขึ้นสำหรับส่วนยื่นที่มีมุมลาดต่ำกว่าเกณฑ์ ยิ่งค่านี้น้อยลง ระยะยื่นที่สามารถพิมพ์โดยไม่ต้องรองรับก็จะยิ่งชันมากขึ้น\n" +"หมายเหตุ: หากตั้งค่าเป็น 0 ส่วนรองรับปกติจะใช้ Threshold ทับซ้อนกันแทน ในขณะที่ส่วนรองรับแบบทรีถอยกลับไปเป็นค่าเริ่มต้นที่ 30" msgid "Threshold overlap" msgstr "การทับซ้อนกันของเกณฑ์" -msgid "" -"If threshold angle is zero, support will be generated for overhangs whose " -"overlap is below the threshold. The smaller this value is, the steeper the " -"overhang that can be printed without support." -msgstr "" -"หากมุมเกณฑ์เป็นศูนย์ ระบบจะสร้างส่วนรองรับสำหรับส่วนยื่นที่มีการทับซ้อนกันต่ำกว่าเกณฑ์ ยิ่งค่านี้น้อยลง " -"ระยะยื่นที่สามารถพิมพ์โดยไม่ต้องรองรับก็จะยิ่งชันมากขึ้น" +msgid "If threshold angle is zero, support will be generated for overhangs whose overlap is below the threshold. The smaller this value is, the steeper the overhang that can be printed without support." +msgstr "หากมุมเกณฑ์เป็นศูนย์ ระบบจะสร้างส่วนรองรับสำหรับส่วนยื่นที่มีการทับซ้อนกันต่ำกว่าเกณฑ์ ยิ่งค่านี้น้อยลง ระยะยื่นที่สามารถพิมพ์โดยไม่ต้องรองรับก็จะยิ่งชันมากขึ้น" msgid "Tree support branch angle" msgstr "มุมกิ่งกิ่งรองรับต้นไม้" -msgid "" -"This setting determines the maximum overhang angle that the branches of tree " -"support are allowed to make. If the angle is increased, the branches can be " -"printed more horizontally, allowing them to reach farther." -msgstr "" -"การตั้งค่านี้จะกำหนดมุมยื่นสูงสุดที่อนุญาตให้กิ่งก้านของต้นไม้รองรับได้ หากมุมเพิ่มขึ้น " -"กิ่งก้านสามารถพิมพ์ในแนวนอนได้มากขึ้น เพื่อให้สามารถขยายได้ไกลขึ้น" +msgid "This setting determines the maximum overhang angle that the branches of tree support are allowed to make. If the angle is increased, the branches can be printed more horizontally, allowing them to reach farther." +msgstr "การตั้งค่านี้จะกำหนดมุมยื่นสูงสุดที่อนุญาตให้กิ่งก้านของต้นไม้รองรับได้ หากมุมเพิ่มขึ้น กิ่งก้านสามารถพิมพ์ในแนวนอนได้มากขึ้น เพื่อให้สามารถขยายได้ไกลขึ้น" msgid "Preferred Branch Angle" msgstr "มุมสาขาที่ต้องการ" #. TRN PrintSettings: "Organic supports" > "Preferred Branch Angle" -msgid "" -"The preferred angle of the branches, when they do not have to avoid the " -"model. Use a lower angle to make them more vertical and more stable. Use a " -"higher angle for branches to merge faster." -msgstr "" -"มุมที่ต้องการของกิ่งก้านเมื่อไม่จำเป็นต้องหลีกเลี่ยงโมเดล " -"ใช้มุมที่ต่ำกว่าเพื่อทำให้เป็นแนวตั้งมากขึ้นและมั่นคงยิ่งขึ้น ใช้มุมที่สูงขึ้นเพื่อให้กิ่งก้านผสานเร็วขึ้น" +msgid "The preferred angle of the branches, when they do not have to avoid the model. Use a lower angle to make them more vertical and more stable. Use a higher angle for branches to merge faster." +msgstr "มุมที่ต้องการของกิ่งก้านเมื่อไม่จำเป็นต้องหลีกเลี่ยงโมเดล ใช้มุมที่ต่ำกว่าเพื่อทำให้เป็นแนวตั้งมากขึ้นและมั่นคงยิ่งขึ้น ใช้มุมที่สูงขึ้นเพื่อให้กิ่งก้านผสานเร็วขึ้น" msgid "Tree support branch distance" msgstr "ต้นไม้รองรับระยะห่างกิ่งก้าน" -msgid "" -"This setting determines the distance between neighboring tree support nodes." +msgid "This setting determines the distance between neighboring tree support nodes." msgstr "การตั้งค่านี้กำหนดระยะห่างระหว่างโหนดส่วนรองรับทรีที่อยู่ใกล้เคียง" msgid "Branch Density" msgstr "ความหนาแน่นของสาขา" #. TRN PrintSettings: "Organic supports" > "Branch Density" -msgid "" -"Adjusts the density of the support structure used to generate the tips of " -"the branches. A higher value results in better overhangs but the supports " -"are harder to remove, thus it is recommended to enable top support " -"interfaces instead of a high branch density value if dense interfaces are " -"needed." -msgstr "" -"ปรับความหนาแน่นของโครงสร้างรองรับที่ใช้สร้างส่วนปลายของกิ่งก้าน " -"ค่าที่สูงกว่าส่งผลให้มีระยะยื่นที่ดีกว่า แต่ส่วนรองรับจะถอดออกได้ยาก " -"ดังนั้นจึงแนะนำให้เปิดใช้งานอินเทอร์เฟซส่วนรองรับด้านบนแทนค่าความหนาแน่นของสาขาที่สูง " -"หากจำเป็นต้องใช้อินเทอร์เฟซที่หนาแน่น" +msgid "Adjusts the density of the support structure used to generate the tips of the branches. A higher value results in better overhangs but the supports are harder to remove, thus it is recommended to enable top support interfaces instead of a high branch density value if dense interfaces are needed." +msgstr "ปรับความหนาแน่นของโครงสร้างรองรับที่ใช้สร้างส่วนปลายของกิ่งก้าน ค่าที่สูงกว่าส่งผลให้มีระยะยื่นที่ดีกว่า แต่ส่วนรองรับจะถอดออกได้ยาก ดังนั้นจึงแนะนำให้เปิดใช้งานอินเทอร์เฟซส่วนรองรับด้านบนแทนค่าความหนาแน่นของสาขาที่สูง หากจำเป็นต้องใช้อินเทอร์เฟซที่หนาแน่น" msgid "Auto brim width" msgstr "ความกว้างขอบยึดชิ้นงานอัตโนมัติ" -msgid "" -"Enabling this option means the width of the brim for tree support will be " -"automatically calculated." +msgid "Enabling this option means the width of the brim for tree support will be automatically calculated." msgstr "การเปิดใช้งานตัวเลือกนี้หมายความว่าความกว้างของขอบเพื่อรองรับต้นไม้จะถูกคำนวณโดยอัตโนมัติ" msgid "Tree support brim width" @@ -18012,44 +15105,26 @@ msgid "Branch Diameter Angle" msgstr "มุมเส้นผ่านศูนย์กลางสาขา" #. TRN PrintSettings: "Organic supports" > "Branch Diameter Angle" -msgid "" -"The angle of the branches' diameter as they gradually become thicker towards " -"the bottom. An angle of 0 will cause the branches to have uniform thickness " -"over their length. A bit of an angle can increase stability of the organic " -"support." -msgstr "" -"มุมของเส้นผ่านศูนย์กลางกิ่งจะค่อยๆหนาขึ้นจนถึงด้านล่าง มุม 0 " -"จะทำให้กิ่งมีความหนาสม่ำเสมอตลอดความยาว " -"มุมเล็กน้อยสามารถเพิ่มความเสถียรของส่วนรองรับแบบออร์แกนิกได้" +msgid "The angle of the branches' diameter as they gradually become thicker towards the bottom. An angle of 0 will cause the branches to have uniform thickness over their length. A bit of an angle can increase stability of the organic support." +msgstr "มุมของเส้นผ่านศูนย์กลางกิ่งจะค่อยๆหนาขึ้นจนถึงด้านล่าง มุม 0 จะทำให้กิ่งมีความหนาสม่ำเสมอตลอดความยาว มุมเล็กน้อยสามารถเพิ่มความเสถียรของส่วนรองรับแบบออร์แกนิกได้" msgid "Support wall loops" msgstr "รองรับห่วงติดผนัง" -msgid "" -"This setting specifies the count of support walls in the range of [0,2]. 0 " -"means auto." +msgid "This setting specifies the count of support walls in the range of [0,2]. 0 means auto." msgstr "การตั้งค่านี้ระบุจำนวนผนังรองรับในช่วง [0,2] 0 หมายถึงอัตโนมัติ" msgid "Tree support with infill" msgstr "ส่วนรองรับต้นไม้พร้อมไส้ใน" -msgid "" -"This setting specifies whether to add infill inside large hollows of tree " -"support." +msgid "This setting specifies whether to add infill inside large hollows of tree support." msgstr "การตั้งค่านี้ระบุว่าจะเพิ่มไส้ในภายในโพรงต้นไม้ขนาดใหญ่หรือไม่" msgid "Ironing Support Interface" msgstr "อินเทอร์เฟซรองรับการรีดผิว" -msgid "" -"Ironing is using small flow to print on same height of support interface " -"again to make it more smooth. This setting controls whether support " -"interface being ironed. When enabled, support interface will be extruded as " -"solid too." -msgstr "" -"การรีดผิวใช้การไหลเพียงเล็กน้อยเพื่อพิมพ์บนส่วนรองรับที่มีความสูงเท่ากันอีกครั้งเพื่อให้เรียบยิ่งขึ้น " -"การตั้งค่านี้ควบคุมว่าจะรีดอินเทอร์เฟซส่วนรองรับหรือไม่ เมื่อเปิดใช้งาน " -"อินเทอร์เฟซส่วนรองรับจะถูกอัดออกมาเป็นของแข็งเช่นกัน" +msgid "Ironing is using small flow to print on same height of support interface again to make it more smooth. This setting controls whether support interface being ironed. When enabled, support interface will be extruded as solid too." +msgstr "การรีดผิวใช้การไหลเพียงเล็กน้อยเพื่อพิมพ์บนส่วนรองรับที่มีความสูงเท่ากันอีกครั้งเพื่อให้เรียบยิ่งขึ้น การตั้งค่านี้ควบคุมว่าจะรีดอินเทอร์เฟซส่วนรองรับหรือไม่ เมื่อเปิดใช้งาน อินเทอร์เฟซส่วนรองรับจะถูกอัดออกมาเป็นของแข็งเช่นกัน" msgid "Support Ironing Pattern" msgstr "รองรับรูปแบบการรีดผิว" @@ -18057,14 +15132,8 @@ msgstr "รองรับรูปแบบการรีดผิว" msgid "Support Ironing flow" msgstr "รองรับกระแสการรีด" -msgid "" -"The amount of material to extrude during ironing. Relative to flow of normal " -"support interface layer height. Too high value results in overextrusion on " -"the surface." -msgstr "" -"ปริมาณวัสดุที่จะพ่นออกมาระหว่างรีดผิว " -"สัมพันธ์กับโฟลว์ของความสูงของเลเยอร์อินเทอร์เฟซส่วนรองรับปกติ " -"ค่าที่สูงเกินไปส่งผลให้เกิดการอัดขึ้นรูปมากเกินไปบนพื้นผิว" +msgid "The amount of material to extrude during ironing. Relative to flow of normal support interface layer height. Too high value results in overextrusion on the surface." +msgstr "ปริมาณวัสดุที่จะพ่นออกมาระหว่างรีดผิว สัมพันธ์กับโฟลว์ของความสูงของเลเยอร์อินเทอร์เฟซส่วนรองรับปกติ ค่าที่สูงเกินไปส่งผลให้เกิดการอัดขึ้นรูปมากเกินไปบนพื้นผิว" msgid "Support Ironing line spacing" msgstr "รองรับระยะห่างระหว่างสายรีดผิว" @@ -18073,62 +15142,31 @@ msgid "Activate temperature control" msgstr "เปิดใช้งานการควบคุมอุณหภูมิ" msgid "" -"Enable this option for automated chamber temperature control. This option " -"activates the emitting of an M191 command before the \"machine_start_gcode" -"\"\n" -" which sets the chamber temperature and waits until it is reached. In " -"addition, it emits an M141 command at the end of the print to turn off the " -"chamber heater, if present.\n" +"Enable this option for automated chamber temperature control. This option activates the emitting of an M191 command before the \"machine_start_gcode\"\n" +" which sets the chamber temperature and waits until it is reached. In addition, it emits an M141 command at the end of the print to turn off the chamber heater, if present.\n" "\n" -"This option relies on the firmware supporting the M191 and M141 commands " -"either via macros or natively and is usually used when an active chamber " -"heater is installed." +"This option relies on the firmware supporting the M191 and M141 commands either via macros or natively and is usually used when an active chamber heater is installed." msgstr "" -"เปิดใช้งานตัวเลือกนี้สำหรับการควบคุมอุณหภูมิห้องอัตโนมัติ ตัวเลือกนี้จะเปิดใช้งานการส่งคำสั่ง M191 " -"ก่อน \"machine_start_gcode\"\n" -" ซึ่งจะตั้งอุณหภูมิห้องอบและรอจนกว่าจะถึงอุณหภูมิห้อง นอกจากนี้ ยังส่งคำสั่ง M141 " -"เมื่อสิ้นสุดการพิมพ์เพื่อปิดเครื่องทำความร้อนในห้อง (หากมี)\n" +"เปิดใช้งานตัวเลือกนี้สำหรับการควบคุมอุณหภูมิห้องอัตโนมัติ ตัวเลือกนี้จะเปิดใช้งานการส่งคำสั่ง M191 ก่อน \"machine_start_gcode\"\n" +" ซึ่งจะตั้งอุณหภูมิห้องอบและรอจนกว่าจะถึงอุณหภูมิห้อง นอกจากนี้ ยังส่งคำสั่ง M141 เมื่อสิ้นสุดการพิมพ์เพื่อปิดเครื่องทำความร้อนในห้อง (หากมี)\n" "\n" -"ตัวเลือกนี้ขึ้นอยู่กับเฟิร์มแวร์ที่รองรับคำสั่ง M191 และ M141 ไม่ว่าจะผ่านทางมาโครหรือแบบเนทิฟ " -"และโดยปกติจะใช้เมื่อมีการติดตั้งเครื่องทำความร้อนในห้องแบบแอคทีฟ" +"ตัวเลือกนี้ขึ้นอยู่กับเฟิร์มแวร์ที่รองรับคำสั่ง M191 และ M141 ไม่ว่าจะผ่านทางมาโครหรือแบบเนทิฟ และโดยปกติจะใช้เมื่อมีการติดตั้งเครื่องทำความร้อนในห้องแบบแอคทีฟ" msgid "Chamber temperature" msgstr "อุณหภูมิห้องพิมพ์" msgid "" -"For high-temperature materials like ABS, ASA, PC, and PA, a higher chamber " -"temperature can help suppress or reduce warping and potentially lead to " -"higher interlayer bonding strength. However, at the same time, a higher " -"chamber temperature will reduce the efficiency of air filtration for ABS and " -"ASA.\n" +"For high-temperature materials like ABS, ASA, PC, and PA, a higher chamber temperature can help suppress or reduce warping and potentially lead to higher interlayer bonding strength. However, at the same time, a higher chamber temperature will reduce the efficiency of air filtration for ABS and ASA.\n" "\n" -"For PLA, PETG, TPU, PVA, and other low-temperature materials, this option " -"should be disabled (set to 0) as the chamber temperature should be low to " -"avoid extruder clogging caused by material softening at the heat break.\n" +"For PLA, PETG, TPU, PVA, and other low-temperature materials, this option should be disabled (set to 0) as the chamber temperature should be low to avoid extruder clogging caused by material softening at the heat break.\n" "\n" -"If enabled, this parameter also sets a G-code variable named " -"chamber_temperature, which can be used to pass the desired chamber " -"temperature to your print start macro, or a heat soak macro like this: " -"PRINT_START (other variables) CHAMBER_TEMP=[chamber_temperature]. This may " -"be useful if your printer does not support M141/M191 commands, or if you " -"desire to handle heat soaking in the print start macro if no active chamber " -"heater is installed." +"If enabled, this parameter also sets a G-code variable named chamber_temperature, which can be used to pass the desired chamber temperature to your print start macro, or a heat soak macro like this: PRINT_START (other variables) CHAMBER_TEMP=[chamber_temperature]. This may be useful if your printer does not support M141/M191 commands, or if you desire to handle heat soaking in the print start macro if no active chamber heater is installed." msgstr "" -"สำหรับวัสดุที่มีอุณหภูมิสูง เช่น ABS, ASA, PC และ PA " -"อุณหภูมิห้องที่สูงขึ้นสามารถช่วยลดหรือลดการบิดงอ " -"และอาจนำไปสู่ความแข็งแรงการยึดเกาะระหว่างชั้นที่สูงขึ้น อย่างไรก็ตาม ในเวลาเดียวกัน " -"อุณหภูมิห้องที่สูงขึ้นจะลดประสิทธิภาพการกรองอากาศสำหรับ ABS และ ASA\n" +"สำหรับวัสดุที่มีอุณหภูมิสูง เช่น ABS, ASA, PC และ PA อุณหภูมิห้องที่สูงขึ้นสามารถช่วยลดหรือลดการบิดงอ และอาจนำไปสู่ความแข็งแรงการยึดเกาะระหว่างชั้นที่สูงขึ้น อย่างไรก็ตาม ในเวลาเดียวกัน อุณหภูมิห้องที่สูงขึ้นจะลดประสิทธิภาพการกรองอากาศสำหรับ ABS และ ASA\n" "\n" -"สำหรับ PLA, PETG, TPU, PVA และวัสดุที่มีอุณหภูมิต่ำอื่นๆ ควรปิดใช้งานตัวเลือกนี้ (ตั้งค่าเป็น 0) " -"เนื่องจากอุณหภูมิห้องควรต่ำเพื่อหลีกเลี่ยงการอุดตันของชุดดันเส้นที่เกิดจากวัสดุอ่อนตัวเมื่อความร้อนแตก\n" +"สำหรับ PLA, PETG, TPU, PVA และวัสดุที่มีอุณหภูมิต่ำอื่นๆ ควรปิดใช้งานตัวเลือกนี้ (ตั้งค่าเป็น 0) เนื่องจากอุณหภูมิห้องควรต่ำเพื่อหลีกเลี่ยงการอุดตันของชุดดันเส้นที่เกิดจากวัสดุอ่อนตัวเมื่อความร้อนแตก\n" "\n" -"หากเปิดใช้งาน พารามิเตอร์นี้จะตั้งค่าตัวแปร G-code ชื่อ Chamber_temperature " -"ซึ่งสามารถใช้เพื่อส่งอุณหภูมิห้องเพาะเลี้ยงที่ต้องการไปยังมาโครเริ่มการพิมพ์ของคุณ " -"หรือมาโครความร้อนแช่เช่นนี้: PRINT_START (ตัวแปรอื่นๆ) " -"CHAMBER_TEMP=[chamber_temperature] " -"วิธีนี้อาจเป็นประโยชน์หากเครื่องพิมพ์ของคุณไม่รองรับคำสั่ง M141/M191 " -"หรือหากคุณต้องการจัดการกับความร้อนที่แช่อยู่ในมาโครเริ่มการพิมพ์ " -"หากไม่มีการติดตั้งเครื่องทำความร้อนในห้องที่ใช้งานอยู่" +"หากเปิดใช้งาน พารามิเตอร์นี้จะตั้งค่าตัวแปร G-code ชื่อ Chamber_temperature ซึ่งสามารถใช้เพื่อส่งอุณหภูมิห้องเพาะเลี้ยงที่ต้องการไปยังมาโครเริ่มการพิมพ์ของคุณ หรือมาโครความร้อนแช่เช่นนี้: PRINT_START (ตัวแปรอื่นๆ) CHAMBER_TEMP=[chamber_temperature] วิธีนี้อาจเป็นประโยชน์หากเครื่องพิมพ์ของคุณไม่รองรับคำสั่ง M141/M191 หรือหากคุณต้องการจัดการกับความร้อนที่แช่อยู่ในมาโครเริ่มการพิมพ์ หากไม่มีการติดตั้งเครื่องทำความร้อนในห้องที่ใช้งานอยู่" msgid "Nozzle temperature after the first layer" msgstr "อุณหภูมิหัวฉีดสำหรับชั้นหลังจากอันแรก" @@ -18136,18 +15174,11 @@ msgstr "อุณหภูมิหัวฉีดสำหรับชั้น msgid "Detect thin walls" msgstr "ตรวจจับผนังบาง" -msgid "" -"This detects thin walls which can’t contain two lines and uses a single line " -"to print. It may not print as well because it’s not a closed loop." -msgstr "" -"ตรวจจับผนังบางๆ ที่ไม่สามารถมีความกว้างสองบรรทัดได้ และใช้บรรทัดเดียวในการพิมพ์ " -"อาจจะพิมพ์ได้ไม่ดีนักเพราะไม่ใช่วงปิด" +msgid "This detects thin walls which can’t contain two lines and uses a single line to print. It may not print as well because it’s not a closed loop." +msgstr "ตรวจจับผนังบางๆ ที่ไม่สามารถมีความกว้างสองบรรทัดได้ และใช้บรรทัดเดียวในการพิมพ์ อาจจะพิมพ์ได้ไม่ดีนักเพราะไม่ใช่วงปิด" -msgid "" -"This G-code is inserted when filament is changed, including T commands to " -"trigger tool change." -msgstr "" -"รหัส G นี้จะถูกแทรกเมื่อมีการเปลี่ยนเส้นพลาสติก รวมถึงคำสั่ง T เพื่อกระตุ้นการเปลี่ยนเครื่องมือ" +msgid "This G-code is inserted when filament is changed, including T commands to trigger tool change." +msgstr "รหัส G นี้จะถูกแทรกเมื่อมีการเปลี่ยนเส้นพลาสติก รวมถึงคำสั่ง T เพื่อกระตุ้นการเปลี่ยนเครื่องมือ" msgid "This G-code is inserted when the extrusion role is changed." msgstr "G-code นี้จะถูกแทรกเมื่อบทบาทการอัดขึ้นรูปมีการเปลี่ยนแปลง" @@ -18155,16 +15186,11 @@ msgstr "G-code นี้จะถูกแทรกเมื่อบทบา msgid "Change extrusion role G-code (filament)" msgstr "เปลี่ยนบทบาทการอัดขึ้นรูป G-code (เส้นพลาสติก)" -msgid "" -"This G-code is inserted when the extrusion role is changed for the active " -"filament." +msgid "This G-code is inserted when the extrusion role is changed for the active filament." msgstr "รหัส G นี้จะถูกแทรกเมื่อมีการเปลี่ยนบทบาทการอัดขึ้นรูปสำหรับเส้นพลาสติกที่ใช้งานอยู่" -msgid "" -"Line width for top surfaces. If expressed as a %, it will be computed over " -"the nozzle diameter." -msgstr "" -"ความกว้างของเส้นสำหรับพื้นผิวด้านบน หากแสดงเป็น % จะคำนวณตามเส้นผ่านศูนย์กลางของหัวฉีด" +msgid "Line width for top surfaces. If expressed as a %, it will be computed over the nozzle diameter." +msgstr "ความกว้างของเส้นสำหรับพื้นผิวด้านบน หากแสดงเป็น % จะคำนวณตามเส้นผ่านศูนย์กลางของหัวฉีด" msgid "This is the speed for solid top surface infill." msgstr "ความเร็วของไส้ในพื้นผิวด้านบนซึ่งเป็นของแข็ง" @@ -18172,54 +15198,29 @@ msgstr "ความเร็วของไส้ในพื้นผิวด msgid "Top shell layers" msgstr "เลเยอร์ผนังด้านบน" -msgid "" -"This is the number of solid layers of top shell, including the top surface " -"layer. When the thickness calculated by this value is thinner than the top " -"shell thickness, the top shell layers will be increased" -msgstr "" -"นี่คือจำนวนชั้นทึบของเปลือกด้านบน รวมถึงชั้นผิวด้านบนด้วย " -"เมื่อความหนาที่คำนวณโดยค่านี้บางกว่าความหนาของเปลือกด้านบน ชั้นเปลือกด้านบนจะเพิ่มขึ้น" +msgid "This is the number of solid layers of top shell, including the top surface layer. When the thickness calculated by this value is thinner than the top shell thickness, the top shell layers will be increased" +msgstr "นี่คือจำนวนชั้นทึบของเปลือกด้านบน รวมถึงชั้นผิวด้านบนด้วย เมื่อความหนาที่คำนวณโดยค่านี้บางกว่าความหนาของเปลือกด้านบน ชั้นเปลือกด้านบนจะเพิ่มขึ้น" msgid "Top shell thickness" msgstr "ความหนาผนังด้านบน" -msgid "" -"The number of top solid layers is increased when slicing if the thickness " -"calculated by top shell layers is thinner than this value. This can avoid " -"having too thin a shell when layer height is small. 0 means that this " -"setting is disabled and thickness of top shell is determined simply by the " -"number of top shell layers." -msgstr "" -"จำนวนชั้นทึบด้านบนจะเพิ่มขึ้นเมื่อสไลซ์หากความหนาที่คำนวณโดยชั้นเปลือกด้านบนบางกว่าค่านี้ " -"วิธีนี้สามารถหลีกเลี่ยงไม่ให้เปลือกบางเกินไปเมื่อชั้นมีความสูงน้อย 0 " -"หมายความว่าการตั้งค่านี้ถูกปิดใช้งาน " -"และความหนาของเปลือกด้านบนถูกกำหนดโดยชั้นเปลือกด้านบนอย่างแน่นอน" +msgid "The number of top solid layers is increased when slicing if the thickness calculated by top shell layers is thinner than this value. This can avoid having too thin a shell when layer height is small. 0 means that this setting is disabled and thickness of top shell is determined simply by the number of top shell layers." +msgstr "จำนวนชั้นทึบด้านบนจะเพิ่มขึ้นเมื่อสไลซ์หากความหนาที่คำนวณโดยชั้นเปลือกด้านบนบางกว่าค่านี้ วิธีนี้สามารถหลีกเลี่ยงไม่ให้เปลือกบางเกินไปเมื่อชั้นมีความสูงน้อย 0 หมายความว่าการตั้งค่านี้ถูกปิดใช้งาน และความหนาของเปลือกด้านบนถูกกำหนดโดยชั้นเปลือกด้านบนอย่างแน่นอน" msgid "Top surface density" msgstr "ความหนาแน่นผิวด้านบน" -msgid "" -"Density of top surface layer. A value of 100% creates a fully solid, smooth " -"top layer. Reducing this value results in a textured top surface, according " -"to the chosen top surface pattern. A value of 0% will result in only the " -"walls on the top layer being created. Intended for aesthetic or functional " -"purposes, not to fix issues such as over-extrusion." -msgstr "" -"ความหนาแน่นของชั้นผิวด้านบน ค่า 100% จะสร้างชั้นบนสุดที่เรียบและแข็งเต็มที่ " -"การลดค่านี้ส่งผลให้พื้นผิวด้านบนมีพื้นผิวตามรูปแบบพื้นผิวด้านบนที่เลือก ค่า 0% " -"จะส่งผลให้มีการสร้างเฉพาะผนังชั้นบนสุดเท่านั้น มีวัตถุประสงค์เพื่อความสวยงามหรือการใช้งาน " -"ไม่ใช่เพื่อแก้ไขปัญหา เช่น การอัดขึ้นรูปมากเกินไป" +msgid "Density of top surface layer. A value of 100% creates a fully solid, smooth top layer. Reducing this value results in a textured top surface, according to the chosen top surface pattern. A value of 0% will result in only the walls on the top layer being created. Intended for aesthetic or functional purposes, not to fix issues such as over-extrusion." +msgstr "ความหนาแน่นของชั้นผิวด้านบน ค่า 100% จะสร้างชั้นบนสุดที่เรียบและแข็งเต็มที่ การลดค่านี้ส่งผลให้พื้นผิวด้านบนมีพื้นผิวตามรูปแบบพื้นผิวด้านบนที่เลือก ค่า 0% จะส่งผลให้มีการสร้างเฉพาะผนังชั้นบนสุดเท่านั้น มีวัตถุประสงค์เพื่อความสวยงามหรือการใช้งาน ไม่ใช่เพื่อแก้ไขปัญหา เช่น การอัดขึ้นรูปมากเกินไป" msgid "Bottom surface density" msgstr "ความหนาแน่นผิวด้านล่าง" msgid "" -"Density of the bottom surface layer. Intended for aesthetic or functional " -"purposes, not to fix issues such as over-extrusion.\n" +"Density of the bottom surface layer. Intended for aesthetic or functional purposes, not to fix issues such as over-extrusion.\n" "WARNING: Lowering this value may negatively affect bed adhesion." msgstr "" -"ความหนาแน่นของชั้นผิวด้านล่าง มีวัตถุประสงค์เพื่อความสวยงามหรือการใช้งาน ไม่ใช่เพื่อแก้ไขปัญหา " -"เช่น การอัดขึ้นรูปมากเกินไป\n" +"ความหนาแน่นของชั้นผิวด้านล่าง มีวัตถุประสงค์เพื่อความสวยงามหรือการใช้งาน ไม่ใช่เพื่อแก้ไขปัญหา เช่น การอัดขึ้นรูปมากเกินไป\n" "คำเตือน: การลดค่านี้อาจส่งผลเสียต่อการยึดเกาะของฐานพิมพ์" msgid "This is the speed at which traveling is done." @@ -18228,13 +15229,8 @@ msgstr "ความเร็วที่ใช้ในการเคลื่ msgid "Wipe while retracting" msgstr "เช็ดขณะดึงกลับ" -msgid "" -"This moves the nozzle along the last extrusion path when retracting to clean " -"any leaked material on the nozzle. This can minimize blobs when printing a " -"new part after traveling." -msgstr "" -"เลื่อนหัวฉีดไปตามเส้นทางการอัดขึ้นรูปสุดท้ายเมื่อถอยกลับเพื่อทำความสะอาดวัสดุที่รั่วไหลบนหัวฉีด " -"วิธีนี้จะช่วยลดรอยหยดเมื่อพิมพ์ชิ้นส่วนใหม่หลังการเดินทาง" +msgid "This moves the nozzle along the last extrusion path when retracting to clean any leaked material on the nozzle. This can minimize blobs when printing a new part after traveling." +msgstr "เลื่อนหัวฉีดไปตามเส้นทางการอัดขึ้นรูปสุดท้ายเมื่อถอยกลับเพื่อทำความสะอาดวัสดุที่รั่วไหลบนหัวฉีด วิธีนี้จะช่วยลดรอยหยดเมื่อพิมพ์ชิ้นส่วนใหม่หลังการเดินทาง" msgid "Wipe distance" msgstr "ระยะเช็ดหัวฉีด" @@ -18242,28 +15238,18 @@ msgstr "ระยะเช็ดหัวฉีด" msgid "" "Describe how long the nozzle will move along the last path when retracting.\n" "\n" -"Depending on how long the wipe operation lasts, how fast and long the " -"extruder/filament retraction settings are, a retraction move may be needed " -"to retract the remaining filament.\n" +"Depending on how long the wipe operation lasts, how fast and long the extruder/filament retraction settings are, a retraction move may be needed to retract the remaining filament.\n" "\n" -"Setting a value in the retract amount before wipe setting below will perform " -"any excess retraction before the wipe, else it will be performed after." +"Setting a value in the retract amount before wipe setting below will perform any excess retraction before the wipe, else it will be performed after." msgstr "" "อธิบายว่าหัวฉีดจะเคลื่อนที่ไปตามเส้นทางสุดท้ายเมื่อหดกลับนานแค่ไหน\n" "\n" -"ขึ้นอยู่กับระยะเวลาของการเช็ด การตั้งค่าการดึงกลับของเส้นพลาสติก/" -"เส้นพลาสติกจะเร็วและนานเพียงใด อาจจำเป็นต้องดึงเส้นพลาสติกกลับเพื่อดึงเส้นพลาสติกที่เหลืออยู่\n" +"ขึ้นอยู่กับระยะเวลาของการเช็ด การตั้งค่าการดึงกลับของเส้นพลาสติก/เส้นพลาสติกจะเร็วและนานเพียงใด อาจจำเป็นต้องดึงเส้นพลาสติกกลับเพื่อดึงเส้นพลาสติกที่เหลืออยู่\n" "\n" -"การตั้งค่าในจำนวนการถอนก่อนการล้างการตั้งค่าด้านล่างจะทำการถอนส่วนที่เกินก่อนการล้าง " -"มิฉะนั้นจะดำเนินการหลังจากนั้น" +"การตั้งค่าในจำนวนการถอนก่อนการล้างการตั้งค่าด้านล่างจะทำการถอนส่วนที่เกินก่อนการล้าง มิฉะนั้นจะดำเนินการหลังจากนั้น" -msgid "" -"The wiping tower can be used to clean up residue on the nozzle and stabilize " -"the chamber pressure inside the nozzle in order to avoid appearance defects " -"when printing objects." -msgstr "" -"หอเช็ดสามารถใช้เพื่อทำความสะอาดสิ่งตกค้างบนหัวฉีด และทำให้แรงดันในห้องภายในหัวฉีดคงที่ " -"เพื่อหลีกเลี่ยงข้อบกพร่องในลักษณะที่ปรากฏเมื่อพิมพ์วัตถุ" +msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." +msgstr "หอเช็ดสามารถใช้เพื่อทำความสะอาดสิ่งตกค้างบนหัวฉีด และทำให้แรงดันในห้องภายในหัวฉีดคงที่ เพื่อหลีกเลี่ยงข้อบกพร่องในลักษณะที่ปรากฏเมื่อพิมพ์วัตถุ" msgid "Internal ribs" msgstr "ซี่โครงภายใน" @@ -18277,9 +15263,7 @@ msgstr "ปริมาตรการไล่เส้น" msgid "Flush multiplier" msgstr "ตัวคูณการไล่เส้น" -msgid "" -"The actual flushing volumes is equal to the flush multiplier value " -"multiplied by the flushing volumes in the table." +msgid "The actual flushing volumes is equal to the flush multiplier value multiplied by the flushing volumes in the table." msgstr "ปริมาตรการไล่เส้นตามจริงจะเท่ากับตัวคูณการไล่เส้นคูณด้วยปริมาตรการไล่เส้นในตาราง" msgid "Prime volume" @@ -18297,60 +15281,36 @@ msgstr "เช็ดมุมการหมุนของทาวเวอร msgid "Wipe tower rotation angle with respect to X axis." msgstr "เช็ดมุมการหมุนของทาวเวอร์ตามแกน X" -msgid "" -"Brim width of prime tower, negative number means auto calculated width based " -"on the height of prime tower." -msgstr "" -"ความกว้างขอบของหอคอยหลัก " -"ตัวเลขติดลบหมายถึงความกว้างที่คำนวณโดยอัตโนมัติตามความสูงของหอคอยหลัก" +msgid "Brim width of prime tower, negative number means auto calculated width based on the height of prime tower." +msgstr "ความกว้างขอบของหอคอยหลัก ตัวเลขติดลบหมายถึงความกว้างที่คำนวณโดยอัตโนมัติตามความสูงของหอคอยหลัก" msgid "Stabilization cone apex angle" msgstr "มุมเอเพ็กซ์ของกรวยป้องกันการสั่นไหว" -msgid "" -"Angle at the apex of the cone that is used to stabilize the wipe tower. " -"Larger angle means wider base." +msgid "Angle at the apex of the cone that is used to stabilize the wipe tower. Larger angle means wider base." msgstr "มุมที่ปลายกรวยที่ใช้เพื่อรักษาเสถียรภาพของหอเช็ด มุมที่ใหญ่ขึ้นหมายถึงฐานที่กว้างขึ้น" msgid "Maximum wipe tower print speed" msgstr "ความเร็วการพิมพ์ไวต์ทาวเวอร์สูงสุด" msgid "" -"The maximum print speed when purging in the wipe tower and printing the wipe " -"tower sparse layers. When purging, if the sparse infill speed or calculated " -"speed from the filament max volumetric speed is lower, the lowest will be " -"used instead.\n" +"The maximum print speed when purging in the wipe tower and printing the wipe tower sparse layers. When purging, if the sparse infill speed or calculated speed from the filament max volumetric speed is lower, the lowest will be used instead.\n" "\n" -"When printing the sparse layers, if the internal perimeter speed or " -"calculated speed from the filament max volumetric speed is lower, the lowest " -"will be used instead.\n" +"When printing the sparse layers, if the internal perimeter speed or calculated speed from the filament max volumetric speed is lower, the lowest will be used instead.\n" "\n" -"Increasing this speed may affect the tower's stability as well as increase " -"the force with which the nozzle collides with any blobs that may have formed " -"on the wipe tower.\n" +"Increasing this speed may affect the tower's stability as well as increase the force with which the nozzle collides with any blobs that may have formed on the wipe tower.\n" "\n" -"Before increasing this parameter beyond the default of 90 mm/s, make sure " -"your printer can reliably bridge at the increased speeds and that ooze when " -"tool changing is well controlled.\n" +"Before increasing this parameter beyond the default of 90 mm/s, make sure your printer can reliably bridge at the increased speeds and that ooze when tool changing is well controlled.\n" "\n" -"For the wipe tower external perimeters the internal perimeter speed is used " -"regardless of this setting." +"For the wipe tower external perimeters the internal perimeter speed is used regardless of this setting." msgstr "" -"ความเร็วในการพิมพ์สูงสุดเมื่อทำการล้างข้อมูลใน Wipe Tower และการพิมพ์ Wipe Tower " -"บางเลเยอร์ เมื่อทำการไล่ล้าง " -"หากความเร็วไส้ในแบบโปร่งหรือความเร็วที่คำนวณจากความเร็วปริมาตรสูงสุดของเส้นพลาสติกต่ำกว่า " -"ความเร็วต่ำสุดจะถูกนำมาใช้แทน\n" +"ความเร็วในการพิมพ์สูงสุดเมื่อทำการล้างข้อมูลใน Wipe Tower และการพิมพ์ Wipe Tower บางเลเยอร์ เมื่อทำการไล่ล้าง หากความเร็วไส้ในแบบโปร่งหรือความเร็วที่คำนวณจากความเร็วปริมาตรสูงสุดของเส้นพลาสติกต่ำกว่า ความเร็วต่ำสุดจะถูกนำมาใช้แทน\n" "\n" -"เมื่อพิมพ์ชั้นเบาบาง " -"หากความเร็วเส้นรอบวงภายในหรือความเร็วที่คำนวณจากความเร็วปริมาตรสูงสุดของเส้นพลาสติกต่ำกว่า " -"ความเร็วต่ำสุดจะถูกนำมาใช้แทน\n" +"เมื่อพิมพ์ชั้นเบาบาง หากความเร็วเส้นรอบวงภายในหรือความเร็วที่คำนวณจากความเร็วปริมาตรสูงสุดของเส้นพลาสติกต่ำกว่า ความเร็วต่ำสุดจะถูกนำมาใช้แทน\n" "\n" -"การเพิ่มความเร็วนี้อาจส่งผลต่อเสถียรภาพของทาวเวอร์ รวมทั้งเพิ่มแรงที่หัวฉีดชนกับหยดใดๆ " -"ที่อาจก่อตัวบนทาวเวอร์เช็ด\n" +"การเพิ่มความเร็วนี้อาจส่งผลต่อเสถียรภาพของทาวเวอร์ รวมทั้งเพิ่มแรงที่หัวฉีดชนกับหยดใดๆ ที่อาจก่อตัวบนทาวเวอร์เช็ด\n" "\n" -"ก่อนที่จะเพิ่มพารามิเตอร์นี้เกินกว่าค่าเริ่มต้นที่ 90 มม./วินาที " -"ตรวจสอบให้แน่ใจว่าเครื่องพิมพ์ของคุณสามารถเชื่อมต่อที่ความเร็วที่เพิ่มขึ้นได้อย่างน่าเชื่อถือ " -"และจะมีการควบคุมอย่างดีเมื่อเปลี่ยนเครื่องมือ\n" +"ก่อนที่จะเพิ่มพารามิเตอร์นี้เกินกว่าค่าเริ่มต้นที่ 90 มม./วินาที ตรวจสอบให้แน่ใจว่าเครื่องพิมพ์ของคุณสามารถเชื่อมต่อที่ความเร็วที่เพิ่มขึ้นได้อย่างน่าเชื่อถือ และจะมีการควบคุมอย่างดีเมื่อเปลี่ยนเครื่องมือ\n" "\n" "สำหรับปริมณฑลภายนอกของไวต์ทาวเวอร์ ความเร็วของปริมณฑลภายในจะถูกใช้โดยไม่คำนึงถึงการตั้งค่านี้" @@ -18359,10 +15319,8 @@ msgstr "ชนิดติดผนัง" msgid "" "Wipe tower outer wall type.\n" -"1. Rectangle: The default wall type, a rectangle with fixed width and " -"height.\n" -"2. Cone: A cone with a fillet at the bottom to help stabilize the wipe " -"tower.\n" +"1. Rectangle: The default wall type, a rectangle with fixed width and height.\n" +"2. Cone: A cone with a fillet at the bottom to help stabilize the wipe tower.\n" "3. Rib: Adds four ribs to the tower wall for enhanced stability." msgstr "" "เช็ดทาวเวอร์ชนิดผนังด้านนอก\n" @@ -18379,13 +15337,8 @@ msgstr "ซี่โครง" msgid "Extra rib length" msgstr "ความยาวซี่โครงพิเศษ" -msgid "" -"Positive values can increase the size of the rib wall, while negative values " -"can reduce the size. However, the size of the rib wall can not be smaller " -"than that determined by the cleaning volume." -msgstr "" -"ค่าบวกสามารถเพิ่มขนาดของผนังซี่โครงได้ ในขณะที่ค่าลบสามารถลดขนาดได้ อย่างไรก็ตาม " -"ขนาดของผนังซี่โครงต้องไม่เล็กกว่าที่กำหนดโดยปริมาตรการทำความสะอาด" +msgid "Positive values can increase the size of the rib wall, while negative values can reduce the size. However, the size of the rib wall can not be smaller than that determined by the cleaning volume." +msgstr "ค่าบวกสามารถเพิ่มขนาดของผนังซี่โครงได้ ในขณะที่ค่าลบสามารถลดขนาดได้ อย่างไรก็ตาม ขนาดของผนังซี่โครงต้องไม่เล็กกว่าที่กำหนดโดยปริมาตรการทำความสะอาด" msgid "Rib width" msgstr "ความกว้างของซี่โครง" @@ -18399,23 +15352,14 @@ msgstr "ผนังเนื้อ" msgid "The wall of prime tower will fillet." msgstr "ผนังของไพร์มทาวเวอร์จะแล่เป็นเนื้อเดียวกัน" -msgid "" -"The extruder to use when printing perimeter of the wipe tower. Set to 0 to " -"use the one that is available (non-soluble would be preferred)." -msgstr "" -"ชุดดันเส้นที่จะใช้ในการพิมพ์ปริมณฑลของหอเช็ด ตั้งค่าเป็น 0 เพื่อใช้อันที่มีอยู่ " -"(แนะนำให้ใช้แบบไม่ละลายน้ำ)" +msgid "The extruder to use when printing perimeter of the wipe tower. Set to 0 to use the one that is available (non-soluble would be preferred)." +msgstr "ชุดดันเส้นที่จะใช้ในการพิมพ์ปริมณฑลของหอเช็ด ตั้งค่าเป็น 0 เพื่อใช้อันที่มีอยู่ (แนะนำให้ใช้แบบไม่ละลายน้ำ)" msgid "Purging volumes - load/unload volumes" msgstr "การล้างไดรฟ์ข้อมูล - โหลด/ยกเลิกการโหลดไดรฟ์ข้อมูล" -msgid "" -"This vector saves required volumes to change from/to each tool used on the " -"wipe tower. These values are used to simplify creation of the full purging " -"volumes below." -msgstr "" -"เวกเตอร์นี้จะบันทึกปริมาณที่ต้องการเพื่อเปลี่ยนจาก/ไปยังแต่ละเครื่องมือที่ใช้บนไวด์ทาวเวอร์ " -"ค่าเหล่านี้ใช้เพื่อทำให้การสร้างวอลุ่มการล้างข้อมูลทั้งหมดด้านล่างง่ายขึ้น" +msgid "This vector saves required volumes to change from/to each tool used on the wipe tower. These values are used to simplify creation of the full purging volumes below." +msgstr "เวกเตอร์นี้จะบันทึกปริมาณที่ต้องการเพื่อเปลี่ยนจาก/ไปยังแต่ละเครื่องมือที่ใช้บนไวด์ทาวเวอร์ ค่าเหล่านี้ใช้เพื่อทำให้การสร้างวอลุ่มการล้างข้อมูลทั้งหมดด้านล่างง่ายขึ้น" msgid "Skip points" msgstr "ข้ามจุด" @@ -18426,21 +15370,14 @@ msgstr "ผนังของไพร์มทาวเวอร์จะข้ msgid "Enable tower interface features" msgstr "เปิดใช้งานคุณสมบัติอินเทอร์เฟซแบบทาวเวอร์" -msgid "" -"Enable optimized prime tower interface behavior when different materials " -"meet." +msgid "Enable optimized prime tower interface behavior when different materials meet." msgstr "เปิดใช้งานพฤติกรรมอินเทอร์เฟซของไพรม์ทาวเวอร์ที่ได้รับการปรับให้เหมาะสมเมื่อวัสดุที่แตกต่างกันมาบรรจบกัน" msgid "Cool down from interface boost during prime tower" msgstr "เย็นลงจากการเพิ่มอินเทอร์เฟซระหว่างหอคอยหลัก" -msgid "" -"When interface-layer temperature boost is active, set the nozzle back to " -"print temperature at the start of the prime tower so it cools down during " -"the tower." -msgstr "" -"เมื่อเปิดใช้งานการเพิ่มอุณหภูมิของชั้นอินเทอร์เฟซ " -"ให้ตั้งค่าหัวฉีดกลับไปเป็นอุณหภูมิการพิมพ์ที่จุดเริ่มต้นของไพรม์ทาวเวอร์ เพื่อให้เย็นลงระหว่างทาวเวอร์" +msgid "When interface-layer temperature boost is active, set the nozzle back to print temperature at the start of the prime tower so it cools down during the tower." +msgstr "เมื่อเปิดใช้งานการเพิ่มอุณหภูมิของชั้นอินเทอร์เฟซ ให้ตั้งค่าหัวฉีดกลับไปเป็นอุณหภูมิการพิมพ์ที่จุดเริ่มต้นของไพรม์ทาวเวอร์ เพื่อให้เย็นลงระหว่างทาวเวอร์" msgid "Infill gap" msgstr "การเติมช่องว่าง" @@ -18448,31 +15385,14 @@ msgstr "การเติมช่องว่าง" msgid "Infill gap." msgstr "การเติมช่องว่าง." -msgid "" -"Purging after filament change will be done inside objects' infills. This may " -"lower the amount of waste and decrease the print time. If the walls are " -"printed with transparent filament, the mixed color infill will be visible. " -"It will not take effect unless the prime tower is enabled." -msgstr "" -"การล้างหลังจากเปลี่ยนเส้นพลาสติกจะดำเนินการภายในส่วนไส้ในของวัตถุ " -"สิ่งนี้อาจลดปริมาณขยะและลดเวลาในการพิมพ์ หากผนังพิมพ์ด้วยเส้นพลาสติกโปร่งใส " -"จะเห็นไส้ในสีผสมไว้ด้านนอก มันจะไม่มีผลเว้นแต่จะเปิดใช้งานไพรม์ทาวเวอร์" +msgid "Purging after filament change will be done inside objects' infills. This may lower the amount of waste and decrease the print time. If the walls are printed with transparent filament, the mixed color infill will be visible. It will not take effect unless the prime tower is enabled." +msgstr "การล้างหลังจากเปลี่ยนเส้นพลาสติกจะดำเนินการภายในส่วนไส้ในของวัตถุ สิ่งนี้อาจลดปริมาณขยะและลดเวลาในการพิมพ์ หากผนังพิมพ์ด้วยเส้นพลาสติกโปร่งใส จะเห็นไส้ในสีผสมไว้ด้านนอก มันจะไม่มีผลเว้นแต่จะเปิดใช้งานไพรม์ทาวเวอร์" -msgid "" -"Purging after filament change will be done inside objects' support. This may " -"lower the amount of waste and decrease the print time. It will not take " -"effect unless a prime tower is enabled." -msgstr "" -"การล้างข้อมูลหลังจากเปลี่ยนเส้นพลาสติกจะดำเนินการภายในส่วนรองรับของวัตถุ " -"สิ่งนี้อาจลดปริมาณขยะและลดเวลาในการพิมพ์ มันจะไม่มีผลเว้นแต่จะเปิดใช้งานไพรม์ทาวเวอร์" +msgid "Purging after filament change will be done inside objects' support. This may lower the amount of waste and decrease the print time. It will not take effect unless a prime tower is enabled." +msgstr "การล้างข้อมูลหลังจากเปลี่ยนเส้นพลาสติกจะดำเนินการภายในส่วนรองรับของวัตถุ สิ่งนี้อาจลดปริมาณขยะและลดเวลาในการพิมพ์ มันจะไม่มีผลเว้นแต่จะเปิดใช้งานไพรม์ทาวเวอร์" -msgid "" -"This object will be used to purge the nozzle after a filament change to save " -"filament and decrease the print time. Colors of the objects will be mixed as " -"a result. It will not take effect unless the prime tower is enabled." -msgstr "" -"วัตถุนี้จะใช้ในการล้างหัวฉีดหลังจากเปลี่ยนเส้นพลาสติกเพื่อประหยัดเส้นพลาสติกและลดเวลาในการพิมพ์ " -"สีของวัตถุจะผสมกัน มันจะไม่มีผลเว้นแต่จะเปิดใช้งานไพรม์ทาวเวอร์" +msgid "This object will be used to purge the nozzle after a filament change to save filament and decrease the print time. Colors of the objects will be mixed as a result. It will not take effect unless the prime tower is enabled." +msgstr "วัตถุนี้จะใช้ในการล้างหัวฉีดหลังจากเปลี่ยนเส้นพลาสติกเพื่อประหยัดเส้นพลาสติกและลดเวลาในการพิมพ์ สีของวัตถุจะผสมกัน มันจะไม่มีผลเว้นแต่จะเปิดใช้งานไพรม์ทาวเวอร์" msgid "Maximal bridging distance" msgstr "ระยะเชื่อมต่อสูงสุด" @@ -18489,61 +15409,35 @@ msgstr "ระยะห่างของเส้นไล่ล้างบน msgid "Extra flow for purging" msgstr "กระแสพิเศษสำหรับการล้าง" -msgid "" -"Extra flow used for the purging lines on the wipe tower. This makes the " -"purging lines thicker or narrower than they normally would be. The spacing " -"is adjusted automatically." -msgstr "" -"การไหลพิเศษที่ใช้สำหรับท่อไล่ล้างบนหอเช็ด ซึ่งจะทำให้เส้นการล้างหนาหรือแคบกว่าปกติ " -"ระยะห่างจะถูกปรับโดยอัตโนมัติ" +msgid "Extra flow used for the purging lines on the wipe tower. This makes the purging lines thicker or narrower than they normally would be. The spacing is adjusted automatically." +msgstr "การไหลพิเศษที่ใช้สำหรับท่อไล่ล้างบนหอเช็ด ซึ่งจะทำให้เส้นการล้างหนาหรือแคบกว่าปกติ ระยะห่างจะถูกปรับโดยอัตโนมัติ" msgid "Idle temperature" msgstr "อุณหภูมิว่าง" -msgid "" -"Nozzle temperature when the tool is currently not used in multi-tool setups. " -"This is only used when 'Ooze prevention' is active in Print Settings. Set to " -"0 to disable." -msgstr "" -"อุณหภูมิหัวฉีดเมื่อไม่ได้ใช้เครื่องมือในการตั้งค่าหลายเครื่องมือ ใช้เฉพาะเมื่อมีการเปิดใช้งาน " -"'การป้องกันน้ำซึม' ในการตั้งค่าการพิมพ์ ตั้งค่าเป็น 0 เพื่อปิดใช้งาน" +msgid "Nozzle temperature when the tool is currently not used in multi-tool setups. This is only used when 'Ooze prevention' is active in Print Settings. Set to 0 to disable." +msgstr "อุณหภูมิหัวฉีดเมื่อไม่ได้ใช้เครื่องมือในการตั้งค่าหลายเครื่องมือ ใช้เฉพาะเมื่อมีการเปิดใช้งาน 'การป้องกันน้ำซึม' ในการตั้งค่าการพิมพ์ ตั้งค่าเป็น 0 เพื่อปิดใช้งาน" msgid "X-Y hole compensation" msgstr "การชดเชยรู X-Y" -msgid "" -"Holes in objects will expand or contract in the XY plane by the set value. " -"Positive values make holes bigger and negative values make holes smaller. " -"This function is used to adjust sizes slightly when objects have assembly " -"issues." -msgstr "" -"รูในวัตถุจะขยายหรือหดตัวในระนาบ XY ตามค่าที่กำหนดค่าไว้ ค่าบวกจะทำให้รูมีขนาดใหญ่ขึ้น " -"ค่าลบจะทำให้รูเล็กลง ฟังก์ชันนี้ใช้เพื่อปรับขนาดเล็กน้อยเมื่อวัตถุมีปัญหาในการประกอบ" +msgid "Holes in objects will expand or contract in the XY plane by the set value. Positive values make holes bigger and negative values make holes smaller. This function is used to adjust sizes slightly when objects have assembly issues." +msgstr "รูในวัตถุจะขยายหรือหดตัวในระนาบ XY ตามค่าที่กำหนดค่าไว้ ค่าบวกจะทำให้รูมีขนาดใหญ่ขึ้น ค่าลบจะทำให้รูเล็กลง ฟังก์ชันนี้ใช้เพื่อปรับขนาดเล็กน้อยเมื่อวัตถุมีปัญหาในการประกอบ" msgid "X-Y contour compensation" msgstr "การชดเชยรูปร่าง X-Y" -msgid "" -"Contours of objects will expand or contract in the XY plane by the set " -"value. Positive values make contours bigger and negative values make " -"contours smaller. This function is used to adjust sizes slightly when " -"objects have assembly issues." -msgstr "" -"รูปทรงของวัตถุจะขยายหรือหดตัวในระนาบ XY ตามค่าที่กำหนดค่าไว้ " -"ค่าบวกจะทำให้เส้นขอบมีขนาดใหญ่ขึ้น ค่าลบจะทำให้เส้นขอบมีขนาดเล็กลง " -"ฟังก์ชันนี้ใช้เพื่อปรับขนาดเล็กน้อยเมื่อวัตถุมีปัญหาในการประกอบ" +msgid "Contours of objects will expand or contract in the XY plane by the set value. Positive values make contours bigger and negative values make contours smaller. This function is used to adjust sizes slightly when objects have assembly issues." +msgstr "รูปทรงของวัตถุจะขยายหรือหดตัวในระนาบ XY ตามค่าที่กำหนดค่าไว้ ค่าบวกจะทำให้เส้นขอบมีขนาดใหญ่ขึ้น ค่าลบจะทำให้เส้นขอบมีขนาดเล็กลง ฟังก์ชันนี้ใช้เพื่อปรับขนาดเล็กน้อยเมื่อวัตถุมีปัญหาในการประกอบ" msgid "Convert holes to polyholes" msgstr "แปลงรูเป็นโพลีโฮล" msgid "" -"Search for almost-circular holes that span more than one layer and convert " -"the geometry to polyholes. Use the nozzle size and the (biggest) diameter to " -"compute the polyhole.\n" +"Search for almost-circular holes that span more than one layer and convert the geometry to polyholes. Use the nozzle size and the (biggest) diameter to compute the polyhole.\n" "See http://hydraraptor.blogspot.com/2011/02/polyholes.html" msgstr "" -"ค้นหารูเกือบเป็นวงกลมที่ขยายมากกว่าหนึ่งชั้น และแปลงรูปทรงเป็นโพลีโฮล " -"ใช้ขนาดหัวฉีดและเส้นผ่านศูนย์กลาง (ใหญ่ที่สุด) เพื่อคำนวณรูโพลี\n" +"ค้นหารูเกือบเป็นวงกลมที่ขยายมากกว่าหนึ่งชั้น และแปลงรูปทรงเป็นโพลีโฮล ใช้ขนาดหัวฉีดและเส้นผ่านศูนย์กลาง (ใหญ่ที่สุด) เพื่อคำนวณรูโพลี\n" "ดูhttp://hydraraptor.blogspot.com/2011/02/polyholes.html" msgid "Polyhole detection margin" @@ -18552,14 +15446,11 @@ msgstr "ขอบการตรวจจับโพลีโฮล" #, no-c-format, no-boost-format msgid "" "Maximum defection of a point to the estimated radius of the circle.\n" -"As cylinders are often exported as triangles of varying size, points may not " -"be on the circle circumference. This setting allows you some leeway to " -"broaden the detection.\n" +"As cylinders are often exported as triangles of varying size, points may not be on the circle circumference. This setting allows you some leeway to broaden the detection.\n" "In mm or in % of the radius." msgstr "" "การเบี่ยงเบนสูงสุดของจุดจนถึงรัศมีโดยประมาณของวงกลม\n" -"เนื่องจากทรงกระบอกมักถูกส่งออกเป็นรูปสามเหลี่ยมที่มีขนาดต่างกัน จุดต่างๆ " -"อาจไม่อยู่บนเส้นรอบวงของวงกลม การตั้งค่านี้ช่วยให้คุณมีเวลามากขึ้นในการขยายการตรวจจับ\n" +"เนื่องจากทรงกระบอกมักถูกส่งออกเป็นรูปสามเหลี่ยมที่มีขนาดต่างกัน จุดต่างๆ อาจไม่อยู่บนเส้นรอบวงของวงกลม การตั้งค่านี้ช่วยให้คุณมีเวลามากขึ้นในการขยายการตรวจจับ\n" "เป็นมิลลิเมตรหรือเป็น % ของรัศมี" msgid "Polyhole twist" @@ -18571,44 +15462,23 @@ msgstr "หมุนโพลีโฮลทุกชั้น" msgid "G-code thumbnails" msgstr "ภาพขนาดย่อ G-code" -msgid "" -"Picture sizes to be stored into a .gcode and .sl1 / .sl1s files, in the " -"following format: \"XxY, XxY, ...\"" -msgstr "" -"ขนาดรูปภาพที่จะจัดเก็บไว้ในไฟล์ .gcode และ .sl1 / .sl1s ในรูปแบบต่อไปนี้: \"XxY, " -"XxY, ...\"" +msgid "Picture sizes to be stored into a .gcode and .sl1 / .sl1s files, in the following format: \"XxY, XxY, ...\"" +msgstr "ขนาดรูปภาพที่จะจัดเก็บไว้ในไฟล์ .gcode และ .sl1 / .sl1s ในรูปแบบต่อไปนี้: \"XxY, XxY, ...\"" msgid "Format of G-code thumbnails" msgstr "รูปแบบของภาพขนาดย่อ G-code" -msgid "" -"Format of G-code thumbnails: PNG for best quality, JPG for smallest size, " -"QOI for low memory firmware." -msgstr "" -"รูปแบบของภาพขนาดย่อ G-code: PNG สำหรับคุณภาพที่ดีที่สุด, JPG สำหรับขนาดที่เล็กที่สุด, QOI " -"สำหรับเฟิร์มแวร์หน่วยความจำเหลือน้อย" +msgid "Format of G-code thumbnails: PNG for best quality, JPG for smallest size, QOI for low memory firmware." +msgstr "รูปแบบของภาพขนาดย่อ G-code: PNG สำหรับคุณภาพที่ดีที่สุด, JPG สำหรับขนาดที่เล็กที่สุด, QOI สำหรับเฟิร์มแวร์หน่วยความจำเหลือน้อย" msgid "Use relative E distances" msgstr "ใช้ระยะห่าง E สัมพัทธ์" -msgid "" -"Relative extrusion is recommended when using \"label_objects\" option. Some " -"extruders work better with this option unchecked (absolute extrusion mode). " -"Wipe tower is only compatible with relative mode. It is recommended on most " -"printers. Default is checked." -msgstr "" -"แนะนำให้ใช้การอัดขึ้นรูปแบบสัมพัทธ์เมื่อใช้ตัวเลือก \"label_objects\" " -"ชุดดันเส้นบางชนิดทำงานได้ดีกว่าหากไม่ได้เลือกตัวเลือกนี้ (โหมดการอัดขึ้นรูปสัมบูรณ์) Wipe Tower " -"ใช้งานได้กับโหมดสัมพัทธ์เท่านั้น แนะนำให้ใช้กับเครื่องพิมพ์ส่วนใหญ่ เลือกค่าเริ่มต้นแล้ว" +msgid "Relative extrusion is recommended when using \"label_objects\" option. Some extruders work better with this option unchecked (absolute extrusion mode). Wipe tower is only compatible with relative mode. It is recommended on most printers. Default is checked." +msgstr "แนะนำให้ใช้การอัดขึ้นรูปแบบสัมพัทธ์เมื่อใช้ตัวเลือก \"label_objects\" ชุดดันเส้นบางชนิดทำงานได้ดีกว่าหากไม่ได้เลือกตัวเลือกนี้ (โหมดการอัดขึ้นรูปสัมบูรณ์) Wipe Tower ใช้งานได้กับโหมดสัมพัทธ์เท่านั้น แนะนำให้ใช้กับเครื่องพิมพ์ส่วนใหญ่ เลือกค่าเริ่มต้นแล้ว" -msgid "" -"The classic wall generator produces walls with constant extrusion width and " -"for very thin areas, gap-fill is used. The Arachne engine produces walls " -"with variable extrusion width." -msgstr "" -"เครื่องกำเนิดผนังแบบคลาสสิกสร้างผนังที่มีความกว้างของการอัดขึ้นรูปคงที่ " -"และสำหรับพื้นที่ที่บางมากจะใช้การเติมช่องว่าง เครื่องยนต์ Arachne " -"สร้างผนังที่มีความกว้างของการอัดขึ้นรูปที่หลากหลาย" +msgid "The classic wall generator produces walls with constant extrusion width and for very thin areas, gap-fill is used. The Arachne engine produces walls with variable extrusion width." +msgstr "เครื่องกำเนิดผนังแบบคลาสสิกสร้างผนังที่มีความกว้างของการอัดขึ้นรูปคงที่ และสำหรับพื้นที่ที่บางมากจะใช้การเติมช่องว่าง เครื่องยนต์ Arachne สร้างผนังที่มีความกว้างของการอัดขึ้นรูปที่หลากหลาย" msgid "Arachne" msgstr "อารัคเน่" @@ -18616,154 +15486,74 @@ msgstr "อารัคเน่" msgid "Wall transition length" msgstr "ความยาวการเปลี่ยนผนัง" -msgid "" -"When transitioning between different numbers of walls as the part becomes " -"thinner, a certain amount of space is allotted to split or join the wall " -"segments. It's expressed as a percentage over nozzle diameter." -msgstr "" -"เมื่อเปลี่ยนระหว่างผนังจำนวนต่างๆ เมื่อชิ้นส่วนบางลง " -"จะมีการแบ่งพื้นที่จำนวนหนึ่งเพื่อแยกหรือรวมส่วนของผนัง " -"โดยแสดงเป็นเปอร์เซ็นต์ของเส้นผ่านศูนย์กลางของหัวฉีด" +msgid "When transitioning between different numbers of walls as the part becomes thinner, a certain amount of space is allotted to split or join the wall segments. It's expressed as a percentage over nozzle diameter." +msgstr "เมื่อเปลี่ยนระหว่างผนังจำนวนต่างๆ เมื่อชิ้นส่วนบางลง จะมีการแบ่งพื้นที่จำนวนหนึ่งเพื่อแยกหรือรวมส่วนของผนัง โดยแสดงเป็นเปอร์เซ็นต์ของเส้นผ่านศูนย์กลางของหัวฉีด" msgid "Wall transitioning filter margin" msgstr "ขอบตัวกรองการเปลี่ยนผนัง" -msgid "" -"Prevent transitioning back and forth between one extra wall and one less. " -"This margin extends the range of extrusion widths which follow to [Minimum " -"wall width - margin, 2 * Minimum wall width + margin]. Increasing this " -"margin reduces the number of transitions, which reduces the number of " -"extrusion starts/stops and travel time. However, large extrusion width " -"variation can lead to under- or overextrusion problems. It's expressed as a " -"percentage over nozzle diameter." -msgstr "" -"ป้องกันการเปลี่ยนไปมาระหว่างผนังพิเศษหนึ่งอันและน้อยกว่าหนึ่งอัน " -"ขอบนี้จะขยายช่วงความกว้างของการอัดขึ้นรูปซึ่งเป็นไปตาม [ความกว้างของผนังขั้นต่ำ - ขอบ 2 * " -"ความกว้างของผนังขั้นต่ำ + ขอบ] การเพิ่มระยะขอบนี้จะลดจำนวนการเปลี่ยน ซึ่งจะลดจำนวนการเริ่ม/" -"หยุดการอัดขึ้นรูปและเวลาการเดินทาง อย่างไรก็ตาม " -"ความกว้างของการอัดขึ้นรูปที่หลากหลายอาจทำให้เกิดปัญหาการอัดขึ้นรูปน้อยเกินไปหรือมากเกินไปได้ " -"โดยแสดงเป็นเปอร์เซ็นต์ของเส้นผ่านศูนย์กลางของหัวฉีด" +msgid "Prevent transitioning back and forth between one extra wall and one less. This margin extends the range of extrusion widths which follow to [Minimum wall width - margin, 2 * Minimum wall width + margin]. Increasing this margin reduces the number of transitions, which reduces the number of extrusion starts/stops and travel time. However, large extrusion width variation can lead to under- or overextrusion problems. It's expressed as a percentage over nozzle diameter." +msgstr "ป้องกันการเปลี่ยนไปมาระหว่างผนังพิเศษหนึ่งอันและน้อยกว่าหนึ่งอัน ขอบนี้จะขยายช่วงความกว้างของการอัดขึ้นรูปซึ่งเป็นไปตาม [ความกว้างของผนังขั้นต่ำ - ขอบ 2 * ความกว้างของผนังขั้นต่ำ + ขอบ] การเพิ่มระยะขอบนี้จะลดจำนวนการเปลี่ยน ซึ่งจะลดจำนวนการเริ่ม/หยุดการอัดขึ้นรูปและเวลาการเดินทาง อย่างไรก็ตาม ความกว้างของการอัดขึ้นรูปที่หลากหลายอาจทำให้เกิดปัญหาการอัดขึ้นรูปน้อยเกินไปหรือมากเกินไปได้ โดยแสดงเป็นเปอร์เซ็นต์ของเส้นผ่านศูนย์กลางของหัวฉีด" msgid "Wall transitioning threshold angle" msgstr "มุมการเปลี่ยนผ่านของผนัง" -msgid "" -"When to create transitions between even and odd numbers of walls. A wedge " -"shape with an angle greater than this setting will not have transitions and " -"no walls will be printed in the center to fill the remaining space. Reducing " -"this setting reduces the number and length of these center walls, but may " -"leave gaps or overextrude." -msgstr "" -"เมื่อใดควรสร้างการเปลี่ยนระหว่างผนังเลขคู่และเลขคี่ รูปร่างลิ่มที่มีมุมมากกว่าการตั้งค่านี้ " -"จะไม่มีการเปลี่ยน และจะไม่มีการพิมพ์ผนังตรงกลางเพื่อไส้ในเต็มพื้นที่ที่เหลือ " -"การลดการตั้งค่านี้จะช่วยลดจำนวนและความยาวของผนังตรงกลาง " -"แต่อาจทำให้เกิดช่องว่างหรืออัดแน่นเกินไป" +msgid "When to create transitions between even and odd numbers of walls. A wedge shape with an angle greater than this setting will not have transitions and no walls will be printed in the center to fill the remaining space. Reducing this setting reduces the number and length of these center walls, but may leave gaps or overextrude." +msgstr "เมื่อใดควรสร้างการเปลี่ยนระหว่างผนังเลขคู่และเลขคี่ รูปร่างลิ่มที่มีมุมมากกว่าการตั้งค่านี้ จะไม่มีการเปลี่ยน และจะไม่มีการพิมพ์ผนังตรงกลางเพื่อไส้ในเต็มพื้นที่ที่เหลือ การลดการตั้งค่านี้จะช่วยลดจำนวนและความยาวของผนังตรงกลาง แต่อาจทำให้เกิดช่องว่างหรืออัดแน่นเกินไป" msgid "Wall distribution count" msgstr "จำนวนการกระจายผนัง" -msgid "" -"The number of walls, counted from the center, over which the variation needs " -"to be spread. Lower values mean that the outer walls don't change in width." -msgstr "" -"จำนวนผนัง นับจากจุดศูนย์กลาง ซึ่งต้องกระจายรูปแบบออกไป " -"ค่าที่ต่ำกว่าหมายความว่าผนังด้านนอกไม่เปลี่ยนความกว้าง" +msgid "The number of walls, counted from the center, over which the variation needs to be spread. Lower values mean that the outer walls don't change in width." +msgstr "จำนวนผนัง นับจากจุดศูนย์กลาง ซึ่งต้องกระจายรูปแบบออกไป ค่าที่ต่ำกว่าหมายความว่าผนังด้านนอกไม่เปลี่ยนความกว้าง" msgid "Minimum feature size" msgstr "ขนาดคุณสมบัติขั้นต่ำ" -msgid "" -"Minimum thickness of thin features. Model features that are thinner than " -"this value will not be printed, while features thicker than than this value " -"will be widened to the minimum wall width. It's expressed as a percentage " -"over nozzle diameter." -msgstr "" -"ความหนาขั้นต่ำของคุณสมบัติบาง คุณลักษณะของโมเดลที่บางกว่าค่านี้จะไม่ถูกพิมพ์ " -"ในขณะที่คุณลักษณะที่หนากว่าค่านี้จะถูกขยายให้เป็นความกว้างของผนังขั้นต่ำ " -"โดยแสดงเป็นเปอร์เซ็นต์ของเส้นผ่านศูนย์กลางของหัวฉีด" +msgid "Minimum thickness of thin features. Model features that are thinner than this value will not be printed, while features thicker than than this value will be widened to the minimum wall width. It's expressed as a percentage over nozzle diameter." +msgstr "ความหนาขั้นต่ำของคุณสมบัติบาง คุณลักษณะของโมเดลที่บางกว่าค่านี้จะไม่ถูกพิมพ์ ในขณะที่คุณลักษณะที่หนากว่าค่านี้จะถูกขยายให้เป็นความกว้างของผนังขั้นต่ำ โดยแสดงเป็นเปอร์เซ็นต์ของเส้นผ่านศูนย์กลางของหัวฉีด" msgid "Minimum wall length" msgstr "ความยาวผนังขั้นต่ำ" msgid "" -"Adjust this value to prevent short, unclosed walls from being printed, which " -"could increase print time. Higher values remove more and longer walls.\n" +"Adjust this value to prevent short, unclosed walls from being printed, which could increase print time. Higher values remove more and longer walls.\n" "\n" -"NOTE: Bottom and top surfaces will not be affected by this value to prevent " -"visual gaps on the outside of the model. Adjust 'One wall threshold' in the " -"Advanced settings below to adjust the sensitivity of what is considered a " -"top-surface. 'One wall threshold' is only visible if this setting is set " -"above the default value of 0.5, or if single-wall top surfaces is enabled." +"NOTE: Bottom and top surfaces will not be affected by this value to prevent visual gaps on the outside of the model. Adjust 'One wall threshold' in the Advanced settings below to adjust the sensitivity of what is considered a top-surface. 'One wall threshold' is only visible if this setting is set above the default value of 0.5, or if single-wall top surfaces is enabled." msgstr "" -"ปรับค่านี้เพื่อป้องกันการพิมพ์ผนังที่สั้นและไม่ปิด ซึ่งอาจเพิ่มเวลาในการพิมพ์ " -"ค่าที่สูงกว่าจะลบผนังที่ยาวขึ้นเรื่อยๆ\n" +"ปรับค่านี้เพื่อป้องกันการพิมพ์ผนังที่สั้นและไม่ปิด ซึ่งอาจเพิ่มเวลาในการพิมพ์ ค่าที่สูงกว่าจะลบผนังที่ยาวขึ้นเรื่อยๆ\n" "\n" -"หมายเหตุ: พื้นผิวด้านล่างและด้านบนจะไม่ได้รับผลกระทบจากค่านี้ " -"เพื่อป้องกันช่องว่างการมองเห็นด้านนอกของแบบจำลอง ปรับ 'เกณฑ์ผนังด้านเดียว' " -"ในการตั้งค่าขั้นสูงด้านล่างเพื่อปรับความไวของสิ่งที่ถือเป็นพื้นผิวด้านบน 'เกณฑ์ผนังด้านเดียว' " -"จะมองเห็นได้ก็ต่อเมื่อการตั้งค่านี้ตั้งไว้สูงกว่าค่าเริ่มต้นที่ 0.5 " -"หรือหากเปิดใช้งานพื้นผิวด้านบนแบบผนังเดียว" +"หมายเหตุ: พื้นผิวด้านล่างและด้านบนจะไม่ได้รับผลกระทบจากค่านี้ เพื่อป้องกันช่องว่างการมองเห็นด้านนอกของแบบจำลอง ปรับ 'เกณฑ์ผนังด้านเดียว' ในการตั้งค่าขั้นสูงด้านล่างเพื่อปรับความไวของสิ่งที่ถือเป็นพื้นผิวด้านบน 'เกณฑ์ผนังด้านเดียว' จะมองเห็นได้ก็ต่อเมื่อการตั้งค่านี้ตั้งไว้สูงกว่าค่าเริ่มต้นที่ 0.5 หรือหากเปิดใช้งานพื้นผิวด้านบนแบบผนังเดียว" msgid "Maximum wall resolution" msgstr "ความละเอียดผนังสูงสุด" -msgid "" -"This value determines the smallest wall line segment length in mm. The " -"smaller you set this value, the more accurate and precise the walls will be." -msgstr "" -"ค่านี้กำหนดความยาวส่วนของเส้นผนังที่เล็กที่สุดเป็นหน่วยมม. ยิ่งคุณตั้งค่านี้น้อย " -"ผนังก็จะยิ่งแม่นยำและแม่นยำมากขึ้นเท่านั้น" +msgid "This value determines the smallest wall line segment length in mm. The smaller you set this value, the more accurate and precise the walls will be." +msgstr "ค่านี้กำหนดความยาวส่วนของเส้นผนังที่เล็กที่สุดเป็นหน่วยมม. ยิ่งคุณตั้งค่านี้น้อย ผนังก็จะยิ่งแม่นยำและแม่นยำมากขึ้นเท่านั้น" msgid "Maximum wall deviation" msgstr "ส่วนเบี่ยงเบนของผนังสูงสุด" -msgid "" -"The maximum deviation allowed when reducing the resolution for the 'Maximum " -"wall resolution' setting. If you increase this, the print will be less " -"accurate, but the G-Code will be smaller. 'Maximum wall deviation' limits " -"'Maximum wall resolution', so if the two conflict, 'Maximum wall deviation' " -"takes precedence." -msgstr "" -"ค่าเบี่ยงเบนสูงสุดที่อนุญาตเมื่อลดความละเอียดสำหรับการตั้งค่า 'ความละเอียดสูงสุดของผนัง' " -"หากเพิ่มค่านี้ การพิมพ์จะแม่นยำน้อยลง แต่ G-Code จะน้อยลง 'ค่าเบี่ยงเบนของผนังสูงสุด' จะจำกัด " -"'ความละเอียดของผนังสูงสุด' ดังนั้นหากทั้งสองข้อขัดแย้งกัน 'ค่าเบี่ยงเบนของผนังสูงสุด' " -"จะมีความสำคัญเหนือกว่า" +msgid "The maximum deviation allowed when reducing the resolution for the 'Maximum wall resolution' setting. If you increase this, the print will be less accurate, but the G-Code will be smaller. 'Maximum wall deviation' limits 'Maximum wall resolution', so if the two conflict, 'Maximum wall deviation' takes precedence." +msgstr "ค่าเบี่ยงเบนสูงสุดที่อนุญาตเมื่อลดความละเอียดสำหรับการตั้งค่า 'ความละเอียดสูงสุดของผนัง' หากเพิ่มค่านี้ การพิมพ์จะแม่นยำน้อยลง แต่ G-Code จะน้อยลง 'ค่าเบี่ยงเบนของผนังสูงสุด' จะจำกัด 'ความละเอียดของผนังสูงสุด' ดังนั้นหากทั้งสองข้อขัดแย้งกัน 'ค่าเบี่ยงเบนของผนังสูงสุด' จะมีความสำคัญเหนือกว่า" msgid "First layer minimum wall width" msgstr "ความกว้างขั้นต่ำของผนังชั้นแรก" -msgid "" -"The minimum wall width that should be used for the first layer is " -"recommended to be set to the same size as the nozzle. This adjustment is " -"expected to enhance adhesion." -msgstr "" -"แนะนำให้กำหนดความกว้างของผนังขั้นต่ำที่ควรใช้สำหรับชั้นแรกให้เป็นขนาดเดียวกับหัวฉีด " -"การปรับนี้คาดว่าจะช่วยเพิ่มการยึดเกาะ" +msgid "The minimum wall width that should be used for the first layer is recommended to be set to the same size as the nozzle. This adjustment is expected to enhance adhesion." +msgstr "แนะนำให้กำหนดความกว้างของผนังขั้นต่ำที่ควรใช้สำหรับชั้นแรกให้เป็นขนาดเดียวกับหัวฉีด การปรับนี้คาดว่าจะช่วยเพิ่มการยึดเกาะ" msgid "Minimum wall width" msgstr "ความกว้างของผนังขั้นต่ำ" -msgid "" -"Width of the wall that will replace thin features (according to the Minimum " -"feature size) of the model. If the Minimum wall width is thinner than the " -"thickness of the feature, the wall will become as thick as the feature " -"itself. It's expressed as a percentage over nozzle diameter." -msgstr "" -"ความกว้างของผนังที่จะมาแทนที่คุณสมบัติบาง (ตามขนาดคุณสมบัติขั้นต่ำ) ของแบบจำลอง " -"หากความกว้างของผนังขั้นต่ำบางกว่าความหนาของคุณสมบัติ ผนังจะหนาเท่ากับคุณสมบัตินั้นเอง " -"โดยแสดงเป็นเปอร์เซ็นต์ของเส้นผ่านศูนย์กลางของหัวฉีด" +msgid "Width of the wall that will replace thin features (according to the Minimum feature size) of the model. If the Minimum wall width is thinner than the thickness of the feature, the wall will become as thick as the feature itself. It's expressed as a percentage over nozzle diameter." +msgstr "ความกว้างของผนังที่จะมาแทนที่คุณสมบัติบาง (ตามขนาดคุณสมบัติขั้นต่ำ) ของแบบจำลอง หากความกว้างของผนังขั้นต่ำบางกว่าความหนาของคุณสมบัติ ผนังจะหนาเท่ากับคุณสมบัตินั้นเอง โดยแสดงเป็นเปอร์เซ็นต์ของเส้นผ่านศูนย์กลางของหัวฉีด" msgid "Detect narrow internal solid infills" msgstr "ตรวจจับไส้ในของแข็งภายในที่แคบ" -msgid "" -"This option will auto-detect narrow internal solid infill areas. If enabled, " -"the concentric pattern will be used for the area to speed up printing. " -"Otherwise, the rectilinear pattern will be used by default." -msgstr "" -"ตัวเลือกนี้จะตรวจจับพื้นที่ไส้ในของแข็งภายในที่แคบโดยอัตโนมัติ หากเปิดใช้งาน " -"ระบบจะใช้รูปแบบศูนย์กลางสำหรับพื้นที่เพื่อเพิ่มความเร็วในการพิมพ์ มิฉะนั้น " -"ระบบจะใช้รูปแบบเส้นตรงเป็นค่าเริ่มต้น" +msgid "This option will auto-detect narrow internal solid infill areas. If enabled, the concentric pattern will be used for the area to speed up printing. Otherwise, the rectilinear pattern will be used by default." +msgstr "ตัวเลือกนี้จะตรวจจับพื้นที่ไส้ในของแข็งภายในที่แคบโดยอัตโนมัติ หากเปิดใช้งาน ระบบจะใช้รูปแบบศูนย์กลางสำหรับพื้นที่เพื่อเพิ่มความเร็วในการพิมพ์ มิฉะนั้น ระบบจะใช้รูปแบบเส้นตรงเป็นค่าเริ่มต้น" msgid "invalid value " msgstr "ค่าไม่ถูกต้อง" @@ -18894,14 +15684,10 @@ msgstr "การนับซ้ำของทั้งโมเดล" msgid "Ensure on bed" msgstr "มั่นใจบนฐานพิมพ์" -msgid "" -"Lift the object above the bed when it is partially below. Disabled by " -"default." +msgid "Lift the object above the bed when it is partially below. Disabled by default." msgstr "ยกสิ่งของไว้เหนือฐานพิมพ์เมื่ออยู่ด้านล่างบางส่วน ปิดใช้งานตามค่าเริ่มต้น" -msgid "" -"Arrange the supplied models in a plate and merge them in a single model in " -"order to perform actions once." +msgid "Arrange the supplied models in a plate and merge them in a single model in order to perform actions once." msgstr "จัดเรียงโมเดลที่ให้มาในจานและรวมเข้าด้วยกันเป็นโมเดลเดียวเพื่อดำเนินการเพียงครั้งเดียว" msgid "Convert Unit" @@ -18961,24 +15747,19 @@ msgstr "โคลนวัตถุในรายการโหลด" msgid "Load uptodate process/machine settings when using uptodate" msgstr "โหลดการตั้งค่ากระบวนการ/เครื่อง uptodate เมื่อใช้ uptodate" -msgid "" -"load up-to-date process/machine settings from the specified file when using " -"up-to-date" +msgid "load up-to-date process/machine settings from the specified file when using up-to-date" msgstr "โหลดการตั้งค่ากระบวนการ/เครื่อง uptodate จากไฟล์ที่ระบุเมื่อใช้ uptodate" msgid "Load uptodate filament settings when using uptodate" msgstr "โหลดการตั้งค่าเส้นพลาสติก uptodate เมื่อใช้ uptodate" -msgid "" -"Load uptodate filament settings from the specified file when using uptodate." +msgid "Load uptodate filament settings from the specified file when using uptodate." msgstr "โหลดการตั้งค่าเส้นพลาสติก uptodate จากไฟล์ที่ระบุเมื่อใช้ uptodate" msgid "Downward machines check" msgstr "เช็คเครื่องลง" -msgid "" -"If enabled, check whether current machine downward compatible with the " -"machines in the list." +msgid "If enabled, check whether current machine downward compatible with the machines in the list." msgstr "หากเปิดใช้งาน ให้ตรวจสอบว่าเครื่องปัจจุบันรุ่นล่างเข้ากันได้กับเครื่องในรายการหรือไม่" msgid "Downward machines settings" @@ -18996,13 +15777,8 @@ msgstr "โหลดรายการวัตถุประกอบจาก msgid "Data directory" msgstr "ไดเร็กทอรีข้อมูล" -msgid "" -"Load and store settings at the given directory. This is useful for " -"maintaining different profiles or including configurations from a network " -"storage." -msgstr "" -"โหลดและจัดเก็บการตั้งค่าไว้ที่ไดเร็กทอรีที่กำหนด " -"สิ่งนี้มีประโยชน์สำหรับการรักษาโปรไฟล์ที่แตกต่างกันหรือรวมถึงการกำหนดค่าจากที่จัดเก็บข้อมูลเครือข่าย" +msgid "Load and store settings at the given directory. This is useful for maintaining different profiles or including configurations from a network storage." +msgstr "โหลดและจัดเก็บการตั้งค่าไว้ที่ไดเร็กทอรีที่กำหนด สิ่งนี้มีประโยชน์สำหรับการรักษาโปรไฟล์ที่แตกต่างกันหรือรวมถึงการกำหนดค่าจากที่จัดเก็บข้อมูลเครือข่าย" msgid "Output directory" msgstr "ไดเร็กทอรีเอาต์พุต" @@ -19013,12 +15789,8 @@ msgstr "ไดเร็กทอรีเอาต์พุตสำหรับ msgid "Debug level" msgstr "ระดับการแก้ไขข้อบกพร่อง" -msgid "" -"Sets debug logging level. 0:fatal, 1:error, 2:warning, 3:info, 4:debug, 5:" -"trace\n" -msgstr "" -"ตั้งค่าระดับการบันทึกการดีบัก 0: ร้ายแรง, 1: ข้อผิดพลาด, 2: คำเตือน, 3: ข้อมูล, 4: ดีบัก, 5: " -"ติดตาม\n" +msgid "Sets debug logging level. 0:fatal, 1:error, 2:warning, 3:info, 4:debug, 5:trace\n" +msgstr "ตั้งค่าระดับการบันทึกการดีบัก 0: ร้ายแรง, 1: ข้อผิดพลาด, 2: คำเตือน, 3: ข้อมูล, 4: ดีบัก, 5: ติดตาม\n" msgid "Log file" msgstr "ไฟล์บันทึก" @@ -19059,9 +15831,7 @@ msgstr "หากเปิดใช้งาน การจัดเรีย msgid "Avoid extrusion calibrate region when arranging" msgstr "หลีกเลี่ยงการปรับเทียบขอบเขตการอัดขึ้นรูปเมื่อทำการจัดเรียง" -msgid "" -"If enabled, Arrange will avoid extrusion calibrate region when placing " -"objects." +msgid "If enabled, Arrange will avoid extrusion calibrate region when placing objects." msgstr "หากเปิดใช้งาน Arrange จะหลีกเลี่ยงขอบเขตการปรับเทียบการอัดขึ้นรูปเมื่อวางวัตถุ" msgid "Skip modified G-code in 3MF" @@ -19106,23 +15876,11 @@ msgstr "Z-hop ปัจจุบัน" msgid "Contains Z-hop present at the beginning of the custom G-code block." msgstr "มี Z-hop อยู่ที่จุดเริ่มต้นของบล็อก G-code ที่กำหนดเอง" -msgid "" -"Position of the extruder at the beginning of the custom G-code block. If the " -"custom G-code travels somewhere else, it should write to this variable so " -"OrcaSlicer knows where it travels from when it gets control back." -msgstr "" -"ตำแหน่งของชุดดันเส้นที่จุดเริ่มต้นของบล็อก G-code แบบกำหนดเอง หาก G-code " -"แบบกำหนดเองเดินทางไปที่อื่น ควรเขียนลงในตัวแปรนี้เพื่อให้ OrcaSlicer " -"รู้ว่าโค้ดเดินทางจากที่ใดเมื่อได้รับการควบคุมกลับ" +msgid "Position of the extruder at the beginning of the custom G-code block. If the custom G-code travels somewhere else, it should write to this variable so OrcaSlicer knows where it travels from when it gets control back." +msgstr "ตำแหน่งของชุดดันเส้นที่จุดเริ่มต้นของบล็อก G-code แบบกำหนดเอง หาก G-code แบบกำหนดเองเดินทางไปที่อื่น ควรเขียนลงในตัวแปรนี้เพื่อให้ OrcaSlicer รู้ว่าโค้ดเดินทางจากที่ใดเมื่อได้รับการควบคุมกลับ" -msgid "" -"Retraction state at the beginning of the custom G-code block. If the custom " -"G-code moves the extruder axis, it should write to this variable so " -"OrcaSlicer de-retracts correctly when it gets control back." -msgstr "" -"สถานะการเพิกถอนที่จุดเริ่มต้นของบล็อก G-code ที่กำหนดเอง หาก G-code " -"แบบกำหนดเองย้ายแกนชุดดันเส้น ควรเขียนลงในตัวแปรนี้เพื่อให้ OrcaSlicer " -"ยกเลิกการดึงกลับอย่างถูกต้องเมื่อได้รับการควบคุมกลับ" +msgid "Retraction state at the beginning of the custom G-code block. If the custom G-code moves the extruder axis, it should write to this variable so OrcaSlicer de-retracts correctly when it gets control back." +msgstr "สถานะการเพิกถอนที่จุดเริ่มต้นของบล็อก G-code ที่กำหนดเอง หาก G-code แบบกำหนดเองย้ายแกนชุดดันเส้น ควรเขียนลงในตัวแปรนี้เพื่อให้ OrcaSlicer ยกเลิกการดึงกลับอย่างถูกต้องเมื่อได้รับการควบคุมกลับ" msgid "Extra de-retraction" msgstr "การยกเลิกการดึงกลับเพิ่มเติม" @@ -19133,9 +15891,7 @@ msgstr "การดันเส้นเตรียมชุดดันเส msgid "Absolute E position" msgstr "ตำแหน่งสัมบูรณ์ E" -msgid "" -"Current position of the extruder axis. Only used with absolute extruder " -"addressing." +msgid "Current position of the extruder axis. Only used with absolute extruder addressing." msgstr "ตำแหน่งปัจจุบันของแกนชุดดันเส้น ใช้กับการกำหนดที่อยู่ของชุดดันเส้นแบบสัมบูรณ์เท่านั้น" msgid "Current extruder" @@ -19147,9 +15903,7 @@ msgstr "ดัชนีค่าศูนย์ของชุดดันเส msgid "Current object index" msgstr "ดัชนีวัตถุปัจจุบัน" -msgid "" -"Specific for sequential printing. Zero-based index of currently printed " -"object." +msgid "Specific for sequential printing. Zero-based index of currently printed object." msgstr "เฉพาะสำหรับการพิมพ์ตามลำดับ ดัชนีแบบศูนย์ของวัตถุที่พิมพ์ในปัจจุบัน" msgid "Has wipe tower" @@ -19161,32 +15915,25 @@ msgstr "มีการสร้างเช็ดทาวเวอร์ใน msgid "Initial extruder" msgstr "ชุดดันเส้นเริ่มต้น" -msgid "" -"Zero-based index of the first extruder used in the print. Same as " -"initial_tool." +msgid "Zero-based index of the first extruder used in the print. Same as initial_tool." msgstr "ดัชนีแบบศูนย์ของชุดดันเส้นตัวแรกที่ใช้ในการพิมพ์ เช่นเดียวกับ Initial_tool" msgid "Initial tool" msgstr "เครื่องมือเริ่มต้น" -msgid "" -"Zero-based index of the first extruder used in the print. Same as " -"initial_extruder." +msgid "Zero-based index of the first extruder used in the print. Same as initial_extruder." msgstr "ดัชนีแบบศูนย์ของชุดดันเส้นตัวแรกที่ใช้ในการพิมพ์ เช่นเดียวกับ Initial_extruder" msgid "Is extruder used?" msgstr "ใช้ชุดดันเส้นหรือไม่?" -msgid "" -"Vector of booleans stating whether a given extruder is used in the print." +msgid "Vector of booleans stating whether a given extruder is used in the print." msgstr "เวกเตอร์ของบูลีนที่ระบุว่ามีการใช้ชุดดันเส้นที่กำหนดในการพิมพ์หรือไม่" msgid "Number of extruders" msgstr "จำนวนชุดดันเส้น" -msgid "" -"Total number of extruders, regardless of whether they are used in the " -"current print." +msgid "Total number of extruders, regardless of whether they are used in the current print." msgstr "จำนวนชุดดันเส้นทั้งหมด ไม่ว่าจะใช้ในการพิมพ์ปัจจุบันหรือไม่ก็ตาม" msgid "Has single extruder MM priming" @@ -19216,19 +15963,13 @@ msgstr "ปริมาตรรวมของเส้นพลาสติก msgid "Weight per extruder" msgstr "น้ำหนักต่อชุดดันเส้น" -msgid "" -"Weight per extruder extruded during the entire print. Calculated from " -"filament_density value in Filament Settings." -msgstr "" -"น้ำหนักต่อชุดดันเส้นที่อัดออกมาระหว่างการพิมพ์ทั้งหมด คำนวณจากค่า fil_density " -"ในการตั้งค่าเส้นพลาสติก" +msgid "Weight per extruder extruded during the entire print. Calculated from filament_density value in Filament Settings." +msgstr "น้ำหนักต่อชุดดันเส้นที่อัดออกมาระหว่างการพิมพ์ทั้งหมด คำนวณจากค่า fil_density ในการตั้งค่าเส้นพลาสติก" msgid "Total weight" msgstr "น้ำหนักรวม" -msgid "" -"Total weight of the print. Calculated from filament_density value in " -"Filament Settings." +msgid "Total weight of the print. Calculated from filament_density value in Filament Settings." msgstr "น้ำหนักรวมของการพิมพ์ คำนวณจากค่า fil_density ในการตั้งค่าเส้นพลาสติก" msgid "Total layer count" @@ -19240,18 +15981,11 @@ msgstr "จำนวนชั้นในการพิมพ์ทั้งห msgid "Print time (normal mode)" msgstr "เวลาในการพิมพ์ (โหมดปกติ)" -msgid "" -"Estimated print time when printed in normal mode (i.e. not in silent mode). " -"Same as print_time." -msgstr "" -"เวลาพิมพ์โดยประมาณเมื่อพิมพ์ในโหมดปกติ (เช่น ไม่อยู่ในโหมดเงียบ) เช่นเดียวกับ print_time" +msgid "Estimated print time when printed in normal mode (i.e. not in silent mode). Same as print_time." +msgstr "เวลาพิมพ์โดยประมาณเมื่อพิมพ์ในโหมดปกติ (เช่น ไม่อยู่ในโหมดเงียบ) เช่นเดียวกับ print_time" -msgid "" -"Estimated print time when printed in normal mode (i.e. not in silent mode). " -"Same as normal_print_time." -msgstr "" -"เวลาพิมพ์โดยประมาณเมื่อพิมพ์ในโหมดปกติ (เช่น ไม่อยู่ในโหมดเงียบ) " -"เช่นเดียวกับเวลาปกติ_print_time" +msgid "Estimated print time when printed in normal mode (i.e. not in silent mode). Same as normal_print_time." +msgstr "เวลาพิมพ์โดยประมาณเมื่อพิมพ์ในโหมดปกติ (เช่น ไม่อยู่ในโหมดเงียบ) เช่นเดียวกับเวลาปกติ_print_time" msgid "Print time (silent mode)" msgstr "เวลาในการพิมพ์ (โหมดเงียบ)" @@ -19259,19 +15993,14 @@ msgstr "เวลาในการพิมพ์ (โหมดเงียบ) msgid "Estimated print time when printed in silent mode." msgstr "เวลาการพิมพ์โดยประมาณเมื่อพิมพ์ในโหมดเงียบ" -msgid "" -"Total cost of all material used in the print. Calculated from filament_cost " -"value in Filament Settings." +msgid "Total cost of all material used in the print. Calculated from filament_cost value in Filament Settings." msgstr "ต้นทุนรวมของวัสดุทั้งหมดที่ใช้ในการพิมพ์ คำนวณจากค่า fil_cost ในการตั้งค่า เส้นพลาสติก" msgid "Total wipe tower cost" msgstr "ต้นทุนเช็ดทาวเวอร์ทั้งหมด" -msgid "" -"Total cost of the material wasted on the wipe tower. Calculated from " -"filament_cost value in Filament Settings." -msgstr "" -"ต้นทุนรวมของวัสดุที่เสียไปบนไวด์ทาวเวอร์ คำนวณจากค่า fil_cost ในการตั้งค่า เส้นพลาสติก" +msgid "Total cost of the material wasted on the wipe tower. Calculated from filament_cost value in Filament Settings." +msgstr "ต้นทุนรวมของวัสดุที่เสียไปบนไวด์ทาวเวอร์ คำนวณจากค่า fil_cost ในการตั้งค่า เส้นพลาสติก" msgid "Wipe tower volume" msgstr "เช็ดปริมาตรทาวเวอร์" @@ -19288,17 +16017,13 @@ msgstr "ความยาวรวมของเส้นพลาสติก msgid "Print time (seconds)" msgstr "เวลาในการพิมพ์ (วินาที)" -msgid "" -"Total estimated print time in seconds. Replaced with actual value during " -"post-processing." +msgid "Total estimated print time in seconds. Replaced with actual value during post-processing." msgstr "เวลาพิมพ์ทั้งหมดโดยประมาณเป็นวินาที แทนที่ด้วยค่าจริงระหว่างการประมวลผลภายหลัง" msgid "Filament length (meters)" msgstr "ความยาวเส้นพลาสติก (เมตร)" -msgid "" -"Total filament length used in meters. Replaced with actual value during post-" -"processing." +msgid "Total filament length used in meters. Replaced with actual value during post-processing." msgstr "ความยาวเส้นพลาสติกทั้งหมดที่ใช้เป็นเมตร แทนที่ด้วยค่าจริงระหว่างการประมวลผลภายหลัง" msgid "Number of objects" @@ -19317,13 +16042,10 @@ msgid "Scale per object" msgstr "มาตราส่วนต่อวัตถุ" msgid "" -"Contains a string with the information about what scaling was applied to the " -"individual objects. Indexing of the objects is zero-based (first object has " -"index 0).\n" +"Contains a string with the information about what scaling was applied to the individual objects. Indexing of the objects is zero-based (first object has index 0).\n" "Example: 'x:100% y:50% z:100%'." msgstr "" -"ประกอบด้วยสตริงที่มีข้อมูลเกี่ยวกับการปรับสเกลที่ใช้กับออบเจ็กต์แต่ละรายการ " -"การทำดัชนีของวัตถุเป็นแบบศูนย์ (วัตถุแรกมีดัชนี 0)\n" +"ประกอบด้วยสตริงที่มีข้อมูลเกี่ยวกับการปรับสเกลที่ใช้กับออบเจ็กต์แต่ละรายการ การทำดัชนีของวัตถุเป็นแบบศูนย์ (วัตถุแรกมีดัชนี 0)\n" "ตัวอย่าง: 'x:100% y:50% z:100%'" msgid "Input filename without extension" @@ -19332,24 +16054,17 @@ msgstr "ป้อนชื่อไฟล์โดยไม่มีนามส msgid "Source filename of the first object, without extension." msgstr "ชื่อไฟล์ต้นฉบับของออบเจ็กต์แรก โดยไม่มีนามสกุล" -msgid "" -"The vector has two elements: X and Y coordinate of the point. Values in mm." +msgid "The vector has two elements: X and Y coordinate of the point. Values in mm." msgstr "เวกเตอร์มีสององค์ประกอบ: พิกัด X และ Y ของจุด ค่าเป็น มม." -msgid "" -"The vector has two elements: X and Y dimension of the bounding box. Values " -"in mm." +msgid "The vector has two elements: X and Y dimension of the bounding box. Values in mm." msgstr "เวกเตอร์มีสององค์ประกอบ: มิติ X และ Y ของกรอบขอบเขต ค่าเป็น มม." msgid "First layer convex hull" msgstr "ชั้นแรกเป็นตัวถังนูน" -msgid "" -"Vector of points of the first layer convex hull. Each element has the " -"following format:'[x, y]' (x and y are floating-point numbers in mm)." -msgstr "" -"เวกเตอร์ของจุดของตัวเรือนูนชั้นแรก แต่ละองค์ประกอบมีรูปแบบดังนี้:'[x, y]' (x และ y " -"เป็นตัวเลขทศนิยมในหน่วย มม.)" +msgid "Vector of points of the first layer convex hull. Each element has the following format:'[x, y]' (x and y are floating-point numbers in mm)." +msgstr "เวกเตอร์ของจุดของตัวเรือนูนชั้นแรก แต่ละองค์ประกอบมีรูปแบบดังนี้:'[x, y]' (x และ y เป็นตัวเลขทศนิยมในหน่วย มม.)" msgid "Bottom-left corner of the first layer bounding box" msgstr "มุมล่างซ้ายของกล่องขอบชั้นแรก" @@ -19396,12 +16111,8 @@ msgstr "ชื่อของค่าที่ตั้งล่วงหน้ msgid "Filament preset name" msgstr "ชื่อฟิลาเมนต์ที่ตั้งไว้ล่วงหน้า" -msgid "" -"Names of the filament presets used for slicing. The variable is a vector " -"containing one name for each extruder." -msgstr "" -"ชื่อของค่าที่ตั้งไว้ล่วงหน้าของฟิลาเมนท์ที่ใช้สำหรับการสไลซ์ " -"ตัวแปรคือเวกเตอร์ที่มีชื่อเดียวสำหรับชุดดันเส้นแต่ละเครื่อง" +msgid "Names of the filament presets used for slicing. The variable is a vector containing one name for each extruder." +msgstr "ชื่อของค่าที่ตั้งไว้ล่วงหน้าของฟิลาเมนท์ที่ใช้สำหรับการสไลซ์ ตัวแปรคือเวกเตอร์ที่มีชื่อเดียวสำหรับชุดดันเส้นแต่ละเครื่อง" msgid "Printer preset name" msgstr "ชื่อที่ตั้งไว้ล่วงหน้าของเครื่องพิมพ์" @@ -19424,9 +16135,7 @@ msgstr "ดัชนีของเลเยอร์ปัจจุบัน msgid "Layer Z" msgstr "เลเยอร์ Z" -msgid "" -"Height of the current layer above the print bed, measured to the top of the " -"layer." +msgid "Height of the current layer above the print bed, measured to the top of the layer." msgstr "ความสูงของเลเยอร์ปัจจุบันเหนือฐานพิมพ์พิมพ์ วัดจนถึงด้านบนของเลเยอร์" msgid "Maximal layer Z" @@ -19469,9 +16178,7 @@ msgid "large overhangs" msgstr "ส่วนยื่นขนาดใหญ่" #, c-format, boost-format -msgid "" -"It seems object %s has %s. Please re-orient the object or enable support " -"generation." +msgid "It seems object %s has %s. Please re-orient the object or enable support generation." msgstr "ดูเหมือนว่าวัตถุ %s มี %s โปรดปรับทิศทางวัตถุใหม่หรือเปิดใช้งานการสร้างส่วนรองรับ" msgid "Generating support" @@ -19483,24 +16190,18 @@ msgstr "กำลังปรับเส้นทางพิมพ์ให้ msgid "Slicing mesh" msgstr "กำลังสไลซ์เมช" -msgid "" -"No layers were detected. You might want to repair your STL file(s) or check " -"their size or thickness and retry.\n" -msgstr "" -"ไม่พบเลเยอร์ คุณอาจต้องการซ่อมแซมไฟล์ STL ของคุณ " -"หรือตรวจสอบขนาดหรือความหนาแล้วลองอีกครั้ง\n" +msgid "No layers were detected. You might want to repair your STL file(s) or check their size or thickness and retry.\n" +msgstr "ไม่พบเลเยอร์ คุณอาจต้องการซ่อมแซมไฟล์ STL ของคุณ หรือตรวจสอบขนาดหรือความหนาแล้วลองอีกครั้ง\n" msgid "" -"An object's XY size compensation will not be used because it is also color-" -"painted.\n" +"An object's XY size compensation will not be used because it is also color-painted.\n" "XY Size compensation cannot be combined with color-painting." msgstr "" "การชดเชยขนาด XY ของวัตถุจะไม่ถูกนำมาใช้เนื่องจากวัตถุนั้นถูกลงสีด้วย\n" "การชดเชยขนาด XY ไม่สามารถใช้ร่วมกับการวาดภาพสีได้" msgid "" -"An object has enabled XY Size compensation which will not be used because it " -"is also fuzzy skin painted.\n" +"An object has enabled XY Size compensation which will not be used because it is also fuzzy skin painted.\n" "XY Size compensation cannot be combined with fuzzy skin painting." msgstr "" "วัตถุได้เปิดใช้งานการชดเชยขนาด XY ซึ่งจะไม่ถูกใช้เนื่องจากเป็นสีที่ไม่ชัดเจนเช่นกัน\n" @@ -19521,9 +16222,7 @@ msgstr "การประสานไฟล์โมเดลล้มเหล msgid "The supplied file couldn't be read because it's empty." msgstr "ไม่สามารถอ่านไฟล์ที่ให้มาได้เนื่องจากไฟล์ว่างเปล่า" -msgid "" -"Unknown file format: input file must have .stl, .obj, or .amf(.xml) " -"extension." +msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "รูปแบบไฟล์ที่ไม่รู้จัก ไฟล์อินพุตต้องมีนามสกุล .stl, .obj, .amf(.xml)" msgid "Unknown file format: input file must have .3mf or .zip.amf extension." @@ -19580,8 +16279,7 @@ msgstr "เสร็จ" msgid "How can I use calibration results?" msgstr "จะใช้ผลการสอบเทียบได้อย่างไร?" -msgid "" -"You could change the Flow Dynamics Calibration Factor in material editing" +msgid "You could change the Flow Dynamics Calibration Factor in material editing" msgstr "คุณสามารถเปลี่ยน Flow Dynamics Calibration Factor ในการแก้ไขวัสดุได้" msgid "" @@ -19640,8 +16338,7 @@ msgstr "สร้างค่าที่ตั้งล่วงหน้าใ msgid "Could not find parameter: %s." msgstr "ไม่พบพารามิเตอร์: %s" -msgid "" -"Are you sure to cancel the current calibration and return to the home page?" +msgid "Are you sure to cancel the current calibration and return to the home page?" msgstr "คุณแน่ใจหรือไม่ว่าจะยกเลิกการสอบเทียบปัจจุบันและกลับสู่หน้าแรก" msgid "No Printer Connected!" @@ -19657,47 +16354,30 @@ msgid "The input value size must be 3." msgstr "ขนาดค่าอินพุตต้องเป็น 3" msgid "" -"This machine type can only hold 16 historical results per nozzle. You can " -"delete the existing historical results and then start calibration. Or you " -"can continue the calibration, but you cannot create new calibration " -"historical results.\n" +"This machine type can only hold 16 historical results per nozzle. You can delete the existing historical results and then start calibration. Or you can continue the calibration, but you cannot create new calibration historical results.\n" "Do you still want to continue the calibration?" msgstr "" -"เครื่องประเภทนี้สามารถเก็บผลลัพธ์ประวัติได้ 16 รายการต่อหัวฉีดเท่านั้น " -"คุณสามารถลบผลลัพธ์ในอดีตที่มีอยู่แล้วเริ่มการสอบเทียบได้ หรือคุณสามารถดำเนินการสอบเทียบต่อไปได้ " -"แต่คุณไม่สามารถสร้างผลลัพธ์ในอดีตของการสอบเทียบใหม่ได้\n" +"เครื่องประเภทนี้สามารถเก็บผลลัพธ์ประวัติได้ 16 รายการต่อหัวฉีดเท่านั้น คุณสามารถลบผลลัพธ์ในอดีตที่มีอยู่แล้วเริ่มการสอบเทียบได้ หรือคุณสามารถดำเนินการสอบเทียบต่อไปได้ แต่คุณไม่สามารถสร้างผลลัพธ์ในอดีตของการสอบเทียบใหม่ได้\n" "คุณยังต้องการทำการสอบเทียบต่อไปหรือไม่" #, c-format, boost-format -msgid "" -"Only one of the results with the same name: %s will be saved. Are you sure " -"you want to override the other results?" -msgstr "" -"มีเพียงผลลัพธ์เดียวเท่านั้นที่มีชื่อเดียวกัน: %s จะถูกบันทึก คุณแน่ใจหรือไม่ว่าต้องการแทนที่ผลลัพธ์อื่นๆ" +msgid "Only one of the results with the same name: %s will be saved. Are you sure you want to override the other results?" +msgstr "มีเพียงผลลัพธ์เดียวเท่านั้นที่มีชื่อเดียวกัน: %s จะถูกบันทึก คุณแน่ใจหรือไม่ว่าต้องการแทนที่ผลลัพธ์อื่นๆ" + +#, c-format, boost-format +msgid "There is already a previous calibration result with the same name: %s. Only one result with a name is saved. Are you sure you want to overwrite the previous result?" +msgstr "มีผลการสอบเทียบในอดีตที่มีชื่อเดียวกันอยู่แล้ว: %s บันทึกผลลัพธ์ที่มีชื่อเดียวกันเพียงรายการเดียวเท่านั้น คุณแน่ใจหรือไม่ว่าต้องการแทนที่ผลลัพธ์ในอดีต" #, c-format, boost-format msgid "" -"There is already a previous calibration result with the same name: %s. Only " -"one result with a name is saved. Are you sure you want to overwrite the " -"previous result?" -msgstr "" -"มีผลการสอบเทียบในอดีตที่มีชื่อเดียวกันอยู่แล้ว: %s บันทึกผลลัพธ์ที่มีชื่อเดียวกันเพียงรายการเดียวเท่านั้น " -"คุณแน่ใจหรือไม่ว่าต้องการแทนที่ผลลัพธ์ในอดีต" - -#, c-format, boost-format -msgid "" -"Within the same extruder, the name(%s) must be unique when the filament " -"type, nozzle diameter, and nozzle flow are the same.\n" +"Within the same extruder, the name(%s) must be unique when the filament type, nozzle diameter, and nozzle flow are the same.\n" "Are you sure you want to override the historical result?" msgstr "" -"ภายในชุดดันเส้นเดียวกัน ชื่อ(%s) จะต้องไม่ซ้ำกันเมื่อประเภทเส้นพลาสติก เส้นผ่านศูนย์กลางของหัวฉีด " -"และการไหลของหัวฉีดเหมือนกัน\n" +"ภายในชุดดันเส้นเดียวกัน ชื่อ(%s) จะต้องไม่ซ้ำกันเมื่อประเภทเส้นพลาสติก เส้นผ่านศูนย์กลางของหัวฉีด และการไหลของหัวฉีดเหมือนกัน\n" "คุณแน่ใจหรือไม่ว่าต้องการแทนที่ผลลัพธ์ในอดีต" #, c-format, boost-format -msgid "" -"This machine type can only hold %d historical results per nozzle. This " -"result will not be saved." +msgid "This machine type can only hold %d historical results per nozzle. This result will not be saved." msgstr "ประเภทเครื่องนี้สามารถเก็บผลลัพธ์ประวัติได้เพียง %d รายการต่อหัวฉีด ผลลัพธ์นี้จะไม่ถูกบันทึก" msgid "Connecting to printer..." @@ -19725,18 +16405,12 @@ msgid "When do you need Flow Dynamics Calibration" msgstr "เมื่อใดที่คุณต้องการ Flow Dynamics Calibration" msgid "" -"We now have added the auto-calibration for different filaments, which is " -"fully automated and the result will be saved into the printer for future " -"use. You only need to do the calibration in the following limited cases:\n" -"1. If you introduce a new filament of different brands/models or the " -"filament is damp;\n" +"We now have added the auto-calibration for different filaments, which is fully automated and the result will be saved into the printer for future use. You only need to do the calibration in the following limited cases:\n" +"1. If you introduce a new filament of different brands/models or the filament is damp;\n" "2. If the nozzle is worn out or replaced with a new one;\n" -"3. If the max volumetric speed or print temperature is changed in the " -"filament setting." +"3. If the max volumetric speed or print temperature is changed in the filament setting." msgstr "" -"ตอนนี้เราได้เพิ่มการปรับเทียบอัตโนมัติสำหรับเส้นพลาสติกต่างๆ ซึ่งเป็นระบบอัตโนมัติเต็มรูปแบบ " -"และผลลัพธ์จะถูกบันทึกไว้ในเครื่องพิมพ์เพื่อใช้ในอนาคต " -"คุณจะต้องทำการสอบเทียบเฉพาะในกรณีที่จำกัดต่อไปนี้เท่านั้น:\n" +"ตอนนี้เราได้เพิ่มการปรับเทียบอัตโนมัติสำหรับเส้นพลาสติกต่างๆ ซึ่งเป็นระบบอัตโนมัติเต็มรูปแบบ และผลลัพธ์จะถูกบันทึกไว้ในเครื่องพิมพ์เพื่อใช้ในอนาคต คุณจะต้องทำการสอบเทียบเฉพาะในกรณีที่จำกัดต่อไปนี้เท่านั้น:\n" "1. หากคุณแนะนำเส้นพลาสติกใหม่ของยี่ห้อ/รุ่นที่แตกต่างกัน หรือเส้นพลาสติกชื้น\n" "2. หากหัวฉีดชำรุดหรือเปลี่ยนหัวฉีดใหม่\n" "3. หากความเร็วปริมาตรสูงสุดหรืออุณหภูมิการพิมพ์เปลี่ยนแปลงในการตั้งค่าเส้นพลาสติก" @@ -19747,115 +16421,54 @@ msgstr "เกี่ยวกับการสอบเทียบนี้" msgid "" "Please find the details of Flow Dynamics Calibration from our wiki.\n" "\n" -"Usually the calibration is unnecessary. When you start a single color/" -"material print, with the \"flow dynamics calibration\" option checked in the " -"print start menu, the printer will follow the old way, calibrate the " -"filament before the print; When you start a multi color/material print, the " -"printer will use the default compensation parameter for the filament during " -"every filament switch which will have a good result in most cases.\n" +"Usually the calibration is unnecessary. When you start a single color/material print, with the \"flow dynamics calibration\" option checked in the print start menu, the printer will follow the old way, calibrate the filament before the print; When you start a multi color/material print, the printer will use the default compensation parameter for the filament during every filament switch which will have a good result in most cases.\n" "\n" -"Please note that there are a few cases that can make the calibration results " -"unreliable, such as insufficient adhesion on the build plate. Improving " -"adhesion can be achieved by washing the build plate or applying glue. For " -"more information on this topic, please refer to our Wiki.\n" +"Please note that there are a few cases that can make the calibration results unreliable, such as insufficient adhesion on the build plate. Improving adhesion can be achieved by washing the build plate or applying glue. For more information on this topic, please refer to our Wiki.\n" "\n" -"The calibration results have about 10 percent jitter in our test, which may " -"cause the result not exactly the same in each calibration. We are still " -"investigating the root cause to do improvements with new updates." +"The calibration results have about 10 percent jitter in our test, which may cause the result not exactly the same in each calibration. We are still investigating the root cause to do improvements with new updates." msgstr "" "โปรดดูรายละเอียดของ Flow Dynamics Calibration จากวิกิของเรา\n" "\n" -"โดยปกติแล้วการปรับเทียบจะไม่จำเป็น เมื่อคุณเริ่มพิมพ์สี/วัสดุเดียว โดยเลือกตัวเลือก \"flow " -"dynamics calibration\" ในเมนูเริ่มการพิมพ์ เครื่องพิมพ์จะปฏิบัติตามวิธีเก่า " -"ปรับเทียบเส้นพลาสติกก่อนพิมพ์ เมื่อคุณเริ่มการพิมพ์หลายสี/วัสดุ " -"เครื่องพิมพ์จะใช้พารามิเตอร์การชดเชยเริ่มต้นสำหรับเส้นพลาสติกในระหว่างการเปลี่ยนเส้นพลาสติกทุกครั้ง " -"ซึ่งจะให้ผลลัพธ์ที่ดีในกรณีส่วนใหญ่\n" +"โดยปกติแล้วการปรับเทียบจะไม่จำเป็น เมื่อคุณเริ่มพิมพ์สี/วัสดุเดียว โดยเลือกตัวเลือก \"flow dynamics calibration\" ในเมนูเริ่มการพิมพ์ เครื่องพิมพ์จะปฏิบัติตามวิธีเก่า ปรับเทียบเส้นพลาสติกก่อนพิมพ์ เมื่อคุณเริ่มการพิมพ์หลายสี/วัสดุ เครื่องพิมพ์จะใช้พารามิเตอร์การชดเชยเริ่มต้นสำหรับเส้นพลาสติกในระหว่างการเปลี่ยนเส้นพลาสติกทุกครั้ง ซึ่งจะให้ผลลัพธ์ที่ดีในกรณีส่วนใหญ่\n" "\n" -"โปรดทราบว่ามีบางกรณีที่อาจทำให้ผลการสอบเทียบไม่น่าเชื่อถือ เช่น " -"การยึดเกาะบนฐานรองพิมพ์ไม่เพียงพอ " -"การปรับปรุงการยึดเกาะสามารถทำได้โดยการล้างแผ่นฐานรองพิมพ์หรือใช้กาว " -"สำหรับข้อมูลเพิ่มเติมเกี่ยวกับหัวข้อนี้ โปรดดูที่ Wiki ของเรา\n" +"โปรดทราบว่ามีบางกรณีที่อาจทำให้ผลการสอบเทียบไม่น่าเชื่อถือ เช่น การยึดเกาะบนฐานรองพิมพ์ไม่เพียงพอ การปรับปรุงการยึดเกาะสามารถทำได้โดยการล้างแผ่นฐานรองพิมพ์หรือใช้กาว สำหรับข้อมูลเพิ่มเติมเกี่ยวกับหัวข้อนี้ โปรดดูที่ Wiki ของเรา\n" "\n" -"ผลการสอบเทียบมีความแปรปรวนประมาณ 10 เปอร์เซ็นต์ในการทดสอบของเรา " -"ซึ่งอาจทำให้ผลลัพธ์ไม่เหมือนกันทุกครั้งที่สอบเทียบ " -"เรายังคงตรวจสอบสาเหตุที่แท้จริงเพื่อปรับปรุงในอัปเดตใหม่" +"ผลการสอบเทียบมีความแปรปรวนประมาณ 10 เปอร์เซ็นต์ในการทดสอบของเรา ซึ่งอาจทำให้ผลลัพธ์ไม่เหมือนกันทุกครั้งที่สอบเทียบ เรายังคงตรวจสอบสาเหตุที่แท้จริงเพื่อปรับปรุงในอัปเดตใหม่" msgid "When to use Flow Rate Calibration" msgstr "เมื่อใดจึงจะใช้การปรับเทียบอัตราการไหล" msgid "" -"After using Flow Dynamics Calibration, there might still be some extrusion " -"issues, such as:\n" -"1. Over-Extrusion: Excess material on your printed object, forming blobs or " -"zits, or the layers seem thicker than expected and not uniform\n" -"2. Under-Extrusion: Very thin layers, weak infill strength, or gaps in the " -"top layer of the model, even when printing slowly\n" +"After using Flow Dynamics Calibration, there might still be some extrusion issues, such as:\n" +"1. Over-Extrusion: Excess material on your printed object, forming blobs or zits, or the layers seem thicker than expected and not uniform\n" +"2. Under-Extrusion: Very thin layers, weak infill strength, or gaps in the top layer of the model, even when printing slowly\n" "3. Poor Surface Quality: The surface of your prints seems rough or uneven\n" -"4. Weak Structural Integrity: Prints break easily or don't seem as sturdy as " -"they should be" +"4. Weak Structural Integrity: Prints break easily or don't seem as sturdy as they should be" msgstr "" "หลังจากใช้ Flow Dynamics Calibration แล้ว อาจยังมีปัญหาการอัดขึ้นรูปอยู่บ้าง เช่น:\n" -"1. การอัดขึ้นรูปมากเกินไป: วัสดุส่วนเกินบนวัตถุที่พิมพ์ของคุณ ก่อตัวเป็นหยดหรือสิว หรือชั้นต่างๆ " -"ดูหนากว่าที่คาดไว้และไม่สม่ำเสมอ\n" -"2. Under-Extrusion: ชั้นที่บางมาก ความแข็งแรงในไส้ในที่อ่อนแอ " -"หรือมีช่องว่างในชั้นบนสุดของแบบจำลอง แม้ว่าจะพิมพ์ช้าก็ตาม\n" +"1. การอัดขึ้นรูปมากเกินไป: วัสดุส่วนเกินบนวัตถุที่พิมพ์ของคุณ ก่อตัวเป็นหยดหรือสิว หรือชั้นต่างๆ ดูหนากว่าที่คาดไว้และไม่สม่ำเสมอ\n" +"2. Under-Extrusion: ชั้นที่บางมาก ความแข็งแรงในไส้ในที่อ่อนแอ หรือมีช่องว่างในชั้นบนสุดของแบบจำลอง แม้ว่าจะพิมพ์ช้าก็ตาม\n" "3. คุณภาพพื้นผิวไม่ดี: พื้นผิวงานพิมพ์ของคุณดูหยาบหรือไม่สม่ำเสมอ\n" "4. ความสมบูรณ์ของโครงสร้างที่อ่อนแอ: งานพิมพ์แตกหักง่ายหรือดูไม่แข็งแรงเท่าที่ควร" -msgid "" -"In addition, Flow Rate Calibration is crucial for foaming materials like LW-" -"PLA used in RC planes. These materials expand greatly when heated, and " -"calibration provides a useful reference flow rate." -msgstr "" -"นอกจากนี้ การสอบเทียบอัตราการไหลยังเป็นสิ่งสำคัญสำหรับวัสดุที่เกิดฟอง เช่น LW-PLA " -"ที่ใช้ในเครื่องบิน RC วัสดุเหล่านี้จะขยายตัวอย่างมากเมื่อถูกความร้อน " -"และการสอบเทียบจะให้อัตราการไหลอ้างอิงที่เป็นประโยชน์" +msgid "In addition, Flow Rate Calibration is crucial for foaming materials like LW-PLA used in RC planes. These materials expand greatly when heated, and calibration provides a useful reference flow rate." +msgstr "นอกจากนี้ การสอบเทียบอัตราการไหลยังเป็นสิ่งสำคัญสำหรับวัสดุที่เกิดฟอง เช่น LW-PLA ที่ใช้ในเครื่องบิน RC วัสดุเหล่านี้จะขยายตัวอย่างมากเมื่อถูกความร้อน และการสอบเทียบจะให้อัตราการไหลอ้างอิงที่เป็นประโยชน์" + +msgid "Flow Rate Calibration measures the ratio of expected to actual extrusion volumes. The default setting works well in Bambu Lab printers and official filaments as they were pre-calibrated and fine-tuned. For a regular filament, you usually won't need to perform a Flow Rate Calibration unless you still see the listed defects after you have done other calibrations. For more details, please check out the wiki article." +msgstr "การสอบเทียบอัตราการไหลจะวัดอัตราส่วนของปริมาณที่คาดหวังต่อปริมาณการอัดขึ้นรูปจริง การตั้งค่าเริ่มต้นทำงานได้ดีในเครื่องพิมพ์ Bambu Lab และเส้นพลาสติกอย่างเป็นทางการ เนื่องจากมีการสอบเทียบล่วงหน้าและปรับแต่งอย่างละเอียด สำหรับเส้นพลาสติกทั่วไป โดยปกติแล้วคุณไม่จำเป็นต้องทำการสอบเทียบอัตราการไหล เว้นแต่คุณยังคงเห็นข้อบกพร่องที่ระบุไว้หลังจากคุณทำการสอบเทียบอื่นๆ แล้ว สำหรับรายละเอียดเพิ่มเติม โปรดดูบทความวิกิ" msgid "" -"Flow Rate Calibration measures the ratio of expected to actual extrusion " -"volumes. The default setting works well in Bambu Lab printers and official " -"filaments as they were pre-calibrated and fine-tuned. For a regular " -"filament, you usually won't need to perform a Flow Rate Calibration unless " -"you still see the listed defects after you have done other calibrations. For " -"more details, please check out the wiki article." +"Auto Flow Rate Calibration utilizes Bambu Lab's Micro-Lidar technology, directly measuring the calibration patterns. However, please be advised that the efficacy and accuracy of this method may be compromised with specific types of materials. Particularly, filaments that are transparent or semi-transparent, have sparkles, or have a highly-reflective finish may not be suitable for this calibration and can produce less-than-desirable results.\n" +"\n" +"The calibration results may vary between each calibration or filament. We are still improving the accuracy and compatibility of this calibration through firmware updates over time.\n" +"\n" +"Caution: Flow Rate Calibration is an advanced process, to be attempted only by those who fully understand its purpose and implications. Incorrect usage can lead to sub-par prints or printer damage. Please make sure to carefully read and understand the process before doing it." msgstr "" -"การสอบเทียบอัตราการไหลจะวัดอัตราส่วนของปริมาณที่คาดหวังต่อปริมาณการอัดขึ้นรูปจริง " -"การตั้งค่าเริ่มต้นทำงานได้ดีในเครื่องพิมพ์ Bambu Lab และเส้นพลาสติกอย่างเป็นทางการ " -"เนื่องจากมีการสอบเทียบล่วงหน้าและปรับแต่งอย่างละเอียด สำหรับเส้นพลาสติกทั่วไป " -"โดยปกติแล้วคุณไม่จำเป็นต้องทำการสอบเทียบอัตราการไหล " -"เว้นแต่คุณยังคงเห็นข้อบกพร่องที่ระบุไว้หลังจากคุณทำการสอบเทียบอื่นๆ แล้ว สำหรับรายละเอียดเพิ่มเติม " -"โปรดดูบทความวิกิ" - -msgid "" -"Auto Flow Rate Calibration utilizes Bambu Lab's Micro-Lidar technology, " -"directly measuring the calibration patterns. However, please be advised that " -"the efficacy and accuracy of this method may be compromised with specific " -"types of materials. Particularly, filaments that are transparent or semi-" -"transparent, have sparkles, or have a highly-reflective finish may not be " -"suitable for this calibration and can produce less-than-desirable results.\n" +"การสอบเทียบอัตราการไหลอัตโนมัติใช้เทคโนโลยี Micro-Lidar ของ Bambu Lab ซึ่งวัดรูปแบบการสอบเทียบโดยตรง อย่างไรก็ตาม โปรดทราบว่าประสิทธิภาพและความแม่นยำของวิธีการนี้อาจลดลงกับวัสดุบางประเภท โดยเฉพาะอย่างยิ่ง เส้นพลาสติกที่โปร่งใสหรือกึ่งโปร่งใส มีอนุภาคเป็นประกาย หรือมีพื้นผิวที่มีการสะท้อนแสงสูง อาจไม่เหมาะสำหรับการสอบเทียบนี้ และให้ผลลัพธ์ที่ได้น้อยกว่าที่ต้องการ\n" "\n" -"The calibration results may vary between each calibration or filament. We " -"are still improving the accuracy and compatibility of this calibration " -"through firmware updates over time.\n" +"ผลการสอบเทียบอาจแตกต่างกันไปในแต่ละการสอบเทียบหรือเส้นพลาสติก เรายังคงปรับปรุงความแม่นยำและความเข้ากันได้ของการสอบเทียบนี้ผ่านการอัพเดตเฟิร์มแวร์เมื่อเวลาผ่านไป\n" "\n" -"Caution: Flow Rate Calibration is an advanced process, to be attempted only " -"by those who fully understand its purpose and implications. Incorrect usage " -"can lead to sub-par prints or printer damage. Please make sure to carefully " -"read and understand the process before doing it." -msgstr "" -"การสอบเทียบอัตราการไหลอัตโนมัติใช้เทคโนโลยี Micro-Lidar ของ Bambu Lab " -"ซึ่งวัดรูปแบบการสอบเทียบโดยตรง อย่างไรก็ตาม " -"โปรดทราบว่าประสิทธิภาพและความแม่นยำของวิธีการนี้อาจลดลงกับวัสดุบางประเภท โดยเฉพาะอย่างยิ่ง " -"เส้นพลาสติกที่โปร่งใสหรือกึ่งโปร่งใส มีอนุภาคเป็นประกาย หรือมีพื้นผิวที่มีการสะท้อนแสงสูง " -"อาจไม่เหมาะสำหรับการสอบเทียบนี้ และให้ผลลัพธ์ที่ได้น้อยกว่าที่ต้องการ\n" -"\n" -"ผลการสอบเทียบอาจแตกต่างกันไปในแต่ละการสอบเทียบหรือเส้นพลาสติก " -"เรายังคงปรับปรุงความแม่นยำและความเข้ากันได้ของการสอบเทียบนี้ผ่านการอัพเดตเฟิร์มแวร์เมื่อเวลาผ่านไป\n" -"\n" -"ข้อควรระวัง: " -"การสอบเทียบอัตราการไหลเป็นกระบวนการขั้นสูงที่จะดำเนินการได้เฉพาะผู้ที่เข้าใจวัตถุประสงค์และความหมายของกระบวนการเท่านั้น " -"การใช้งานที่ไม่ถูกต้องอาจทำให้งานพิมพ์ด้อยคุณภาพหรือเครื่องพิมพ์เสียหายได้ " -"โปรดอ่านและทำความเข้าใจกระบวนการอย่างละเอียดก่อนดำเนินการ" +"ข้อควรระวัง: การสอบเทียบอัตราการไหลเป็นกระบวนการขั้นสูงที่จะดำเนินการได้เฉพาะผู้ที่เข้าใจวัตถุประสงค์และความหมายของกระบวนการเท่านั้น การใช้งานที่ไม่ถูกต้องอาจทำให้งานพิมพ์ด้อยคุณภาพหรือเครื่องพิมพ์เสียหายได้ โปรดอ่านและทำความเข้าใจกระบวนการอย่างละเอียดก่อนดำเนินการ" msgid "When you need Max Volumetric Speed Calibration" msgstr "เมื่อคุณต้องการการสอบเทียบความเร็วตามปริมาตรสูงสุด" @@ -19875,16 +16488,10 @@ msgstr "วัสดุที่มีเส้นผ่านศูนย์ก msgid "We found the best Flow Dynamics Calibration Factor" msgstr "เราพบปัจจัยการปรับเทียบ Flow Dynamics ที่ดีที่สุด" -msgid "" -"Part of the calibration failed! You may clean the plate and retry. The " -"failed test result would be dropped." -msgstr "" -"การสอบเทียบบางส่วนล้มเหลว! คุณสามารถทำความสะอาดจานแล้วลองอีกครั้ง " -"ผลการทดสอบที่ล้มเหลวจะถูกยกเลิก" +msgid "Part of the calibration failed! You may clean the plate and retry. The failed test result would be dropped." +msgstr "การสอบเทียบบางส่วนล้มเหลว! คุณสามารถทำความสะอาดจานแล้วลองอีกครั้ง ผลการทดสอบที่ล้มเหลวจะถูกยกเลิก" -msgid "" -"*We recommend you to add brand, materia, type, and even humidity level in " -"the Name" +msgid "*We recommend you to add brand, materia, type, and even humidity level in the Name" msgstr "*เราขอแนะนำให้คุณเพิ่มแบรนด์ วัสดุ ประเภท และแม้แต่ระดับความชื้นในชื่อ" msgid "Please enter the name you want to save to printer." @@ -19957,11 +16564,8 @@ msgstr "การสอบเทียบแบบละเอียดตาม msgid "Title" msgstr "ชื่อเรื่อง" -msgid "" -"A test model will be printed. Please clear the build plate and place it back " -"to the hot bed before calibration." -msgstr "" -"แบบจำลองทดสอบจะถูกพิมพ์ โปรดเคลียร์เพลตฐานพิมพ์และวางกลับไว้บนฐานพิมพ์ร้อนก่อนทำการสอบเทียบ" +msgid "A test model will be printed. Please clear the build plate and place it back to the hot bed before calibration." +msgstr "แบบจำลองทดสอบจะถูกพิมพ์ โปรดเคลียร์เพลตฐานพิมพ์และวางกลับไว้บนฐานพิมพ์ร้อนก่อนทำการสอบเทียบ" msgid "Printing Parameters" msgstr "พารามิเตอร์การพิมพ์" @@ -19976,9 +16580,7 @@ msgid "Please connect the printer first before synchronizing." msgstr "โปรดเชื่อมต่อเครื่องพิมพ์ก่อนที่จะซิงโครไนซ์" #, c-format, boost-format -msgid "" -"Printer %s nozzle information has not been set. Please configure it before " -"proceeding with the calibration." +msgid "Printer %s nozzle information has not been set. Please configure it before proceeding with the calibration." msgstr "ข้อมูลหัวฉีดของเครื่องพิมพ์ %s ไม่ได้ถูกตั้งค่า โปรดกำหนดค่าก่อนดำเนินการสอบเทียบต่อ" msgid "AMS and nozzle information are synced" @@ -20018,22 +16620,13 @@ msgstr "%s เข้ากันไม่ได้กับ %s" msgid "TPU is not supported for Flow Dynamics Auto-Calibration." msgstr "ไม่รองรับ TPU สำหรับการปรับเทียบอัตโนมัติของ Flow Dynamics" -msgid "" -"Selected nozzle temperatures are incompatible. For multi-material printing, " -"each filament's nozzle temperature must be within the recommended nozzle " -"temperature range of the other filaments. Otherwise, nozzle clogging or " -"printer damage may occur." -msgstr "" -"อุณหภูมิหัวฉีดที่เลือกเข้ากันไม่ได้ สำหรับการพิมพ์หลายวัสดุ " -"อุณหภูมิหัวฉีดของเส้นพลาสติกแต่ละเส้นจะต้องอยู่ในช่วงอุณหภูมิหัวฉีดที่แนะนำของเส้นพลาสติกอื่นๆ " -"มิฉะนั้นอาจเกิดการอุดตันของหัวฉีดหรือเครื่องพิมพ์เสียหายได้" +msgid "Selected nozzle temperatures are incompatible. For multi-material printing, each filament's nozzle temperature must be within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." +msgstr "อุณหภูมิหัวฉีดที่เลือกเข้ากันไม่ได้ สำหรับการพิมพ์หลายวัสดุ อุณหภูมิหัวฉีดของเส้นพลาสติกแต่ละเส้นจะต้องอยู่ในช่วงอุณหภูมิหัวฉีดที่แนะนำของเส้นพลาสติกอื่นๆ มิฉะนั้นอาจเกิดการอุดตันของหัวฉีดหรือเครื่องพิมพ์เสียหายได้" msgid "Sync AMS and nozzle information" msgstr "ซิงค์ข้อมูล AMS และหัวฉีด" -msgid "" -"Calibration only supports cases where the left and right nozzle diameters " -"are identical." +msgid "Calibration only supports cases where the left and right nozzle diameters are identical." msgstr "การสอบเทียบรองรับเฉพาะกรณีที่เส้นผ่านศูนย์กลางหัวฉีดซ้ายและขวาเท่ากัน" msgid "From k Value" @@ -20083,13 +16676,8 @@ msgid "Edit Flow Dynamics Calibration" msgstr "แก้ไขปรับเทียบไดนามิกการไหล" #, c-format, boost-format -msgid "" -"Within the same extruder, the name '%s' must be unique when the filament " -"type, nozzle diameter, and nozzle flow are identical. Please choose a " -"different name." -msgstr "" -"ภายในชุดดันเส้นเดียวกัน ชื่อ '%s' จะต้องไม่ซ้ำกันเมื่อประเภทเส้นพลาสติก " -"เส้นผ่านศูนย์กลางของหัวฉีด และการไหลของหัวฉีดเหมือนกัน โปรดเลือกชื่ออื่น" +msgid "Within the same extruder, the name '%s' must be unique when the filament type, nozzle diameter, and nozzle flow are identical. Please choose a different name." +msgstr "ภายในชุดดันเส้นเดียวกัน ชื่อ '%s' จะต้องไม่ซ้ำกันเมื่อประเภทเส้นพลาสติก เส้นผ่านศูนย์กลางของหัวฉีด และการไหลของหัวฉีดเหมือนกัน โปรดเลือกชื่ออื่น" msgid "New Flow Dynamic Calibration" msgstr "การสอบเทียบแบบไดนามิกของ Flow ใหม่" @@ -20300,8 +16888,7 @@ msgstr "ริงกิ้งทาวเวอร์" msgid "Fast Tower" msgstr "ฟาสท์ ทาวเวอร์" -msgid "" -"Please ensure the selected type is compatible with your firmware version." +msgid "Please ensure the selected type is compatible with your firmware version." msgstr "โปรดตรวจสอบให้แน่ใจว่าประเภทที่เลือกเข้ากันได้กับเวอร์ชันเฟิร์มแวร์ของคุณ" msgid "" @@ -20381,8 +16968,7 @@ msgstr "" "กรุณากรอกค่าที่ถูกต้อง:\n" "(0 < ความถี่ < 500)" -msgid "" -"Please input a valid damping factor (0 <= DampingStart < DampingEnd <= 1)" +msgid "Please input a valid damping factor (0 <= DampingStart < DampingEnd <= 1)" msgstr "โปรดป้อนปัจจัยการหน่วงที่ถูกต้อง (0 <= DampingStart < DampingEnd <= 1)" msgid "Cornering test" @@ -20405,21 +16991,17 @@ msgstr "หมายเหตุ: ค่าที่ต่ำกว่า = ม msgid "" "Marlin 2 Junction Deviation detected:\n" -"To test Classic Jerk, set 'Maximum Junction Deviation' in Motion ability to " -"0." +"To test Classic Jerk, set 'Maximum Junction Deviation' in Motion ability to 0." msgstr "" "ตรวจพบส่วนเบี่ยงเบนทางแยก Marlin 2:\n" -"หากต้องการทดสอบ Classic Jerk ให้ตั้งค่า 'Maximum Junction Deviation' " -"ในความสามารถในการเคลื่อนไหวเป็น 0" +"หากต้องการทดสอบ Classic Jerk ให้ตั้งค่า 'Maximum Junction Deviation' ในความสามารถในการเคลื่อนไหวเป็น 0" msgid "" "Marlin 2 Classic Jerk detected:\n" -"To test Junction Deviation, set 'Maximum Junction Deviation' in Motion " -"ability to a value > 0." +"To test Junction Deviation, set 'Maximum Junction Deviation' in Motion ability to a value > 0." msgstr "" "ตรวจพบ Marlin 2 Classic Jerk:\n" -"ในการทดสอบค่าเบี่ยงเบนของทางแยก ให้ตั้งค่า 'ค่าเบี่ยงเบนทางแยกสูงสุด' " -"ในความสามารถในการเคลื่อนที่เป็นค่า > 0" +"ในการทดสอบค่าเบี่ยงเบนของทางแยก ให้ตั้งค่า 'ค่าเบี่ยงเบนทางแยกสูงสุด' ในความสามารถในการเคลื่อนที่เป็นค่า > 0" msgid "" "RepRap detected: Jerk in mm/s.\n" @@ -20517,21 +17099,16 @@ msgstr "กำลังโหลดช่อง IFS จากเครื่อ msgid "Slice the plate first to get project material information." msgstr "สไลซ์เพลตก่อนเพื่อให้ได้ข้อมูลวัสดุของโปรเจ็กต์" -msgid "" -"This plate uses multiple materials. Enable IFS and assign each tool to a " -"printer slot." +msgid "This plate uses multiple materials. Enable IFS and assign each tool to a printer slot." msgstr "เพลตนี้ใช้หลายวัสดุ เปิดใช้ IFS และกำหนดเครื่องมือแต่ละตัวให้ช่องบนเครื่องพิมพ์" msgid "Each project material must be assigned to an IFS slot before printing." msgstr "วัสดุของแต่ละโปรเจ็กต์ต้องถูกกำหนดให้กับช่อง IFS ก่อนพิมพ์" -msgid "" -"Each project material must be assigned to a loaded IFS slot before printing." +msgid "Each project material must be assigned to a loaded IFS slot before printing." msgstr "วัสดุของแต่ละโปรเจ็กต์ต้องถูกกำหนดให้กับช่อง IFS ที่โหลดแล้วก่อนพิมพ์" -msgid "" -"Each project material must match the material loaded in the selected IFS " -"slot." +msgid "Each project material must match the material loaded in the selected IFS slot." msgstr "วัสดุของแต่ละโปรเจ็กต์ต้องตรงกับวัสดุที่โหลดในช่อง IFS ที่เลือก" msgid "Print host upload queue" @@ -20571,9 +17148,7 @@ msgstr "กำลังยกเลิก" msgid "Error uploading to print host" msgstr "เกิดข้อผิดพลาดในการอัปโหลดไปยังโฮสต์การพิมพ์" -msgid "" -"The selected bed type does not match the file. Please confirm before " -"starting the print." +msgid "The selected bed type does not match the file. Please confirm before starting the print." msgstr "ประเภทฐานพิมพ์ที่เลือกไม่ตรงกับไฟล์ โปรดยืนยันก่อนเริ่มพิมพ์" msgid "Heated Bed Leveling" @@ -20724,8 +17299,7 @@ msgstr "ไม่ได้เลือกผู้ขาย โปรดเล msgid "Custom vendor missing; please input custom vendor." msgstr "ผู้ขายที่กำหนดเองหายไป โปรดป้อนผู้ขายที่กำหนดเอง" -msgid "" -"\"Bambu\" or \"Generic\" cannot be used as a Vendor for custom filaments." +msgid "\"Bambu\" or \"Generic\" cannot be used as a Vendor for custom filaments." msgstr "ไม่สามารถใช้ \"Bambu\" หรือ \"Generic\" เป็นผู้จำหน่ายเส้นพลาสติกสั่งทำพิเศษได้" msgid "Filament type is not selected, please reselect type." @@ -20734,9 +17308,7 @@ msgstr "ไม่ได้เลือกประเภทเส้นพลา msgid "Filament serial missing; please input serial." msgstr "ไม่ได้ป้อนซีเรียลของฟิลาเมนต์ โปรดป้อนซีเรียล" -msgid "" -"There may be disallowed characters in the vendor or serial input of the " -"filament. Please delete and re-enter." +msgid "There may be disallowed characters in the vendor or serial input of the filament. Please delete and re-enter." msgstr "อาจมีอักขระที่ไม่อนุญาตในช่องผู้ผลิตหรือซีเรียลของเส้นพลาสติก โปรดลบแล้วป้อนใหม่" msgid "All inputs in the custom vendor or serial are spaces. Please re-enter." @@ -20745,15 +17317,13 @@ msgstr "อินพุตทั้งหมดในผู้จำหน่า msgid "The vendor cannot be a number; please re-enter." msgstr "ผู้ผลิตต้องไม่เป็นตัวเลข โปรดป้อนใหม่" -msgid "" -"You have not selected a printer or preset yet. Please select at least one." +msgid "You have not selected a printer or preset yet. Please select at least one." msgstr "คุณยังไม่ได้เลือกเครื่องพิมพ์หรือค่าที่ตั้งไว้ล่วงหน้า โปรดเลือกอย่างน้อยหนึ่งรายการ" #, c-format, boost-format msgid "" "The Filament name %s you created already exists.\n" -"If you continue, the preset created will be displayed with its full name. Do " -"you want to continue?" +"If you continue, the preset created will be displayed with its full name. Do you want to continue?" msgstr "" "ชื่อเส้นพลาสติก %s ที่คุณสร้างมีอยู่แล้ว\n" "หากคุณดำเนินการต่อ ค่าที่ตั้งไว้ล่วงหน้าที่สร้างขึ้นจะแสดงพร้อมชื่อเต็ม คุณต้องการดำเนินการต่อหรือไม่?" @@ -20769,8 +17339,7 @@ msgstr "" "คุณต้องการเขียนใหม่หรือไม่?" msgid "" -"We would rename the presets as \"Vendor Type Serial @printer you selected" -"\".\n" +"We would rename the presets as \"Vendor Type Serial @printer you selected\".\n" "To add preset for more printers, please go to printer selection" msgstr "" "เราจะเปลี่ยนชื่อค่าที่ตั้งล่วงหน้าเป็น \"Vendor Type Serial @printer ที่คุณเลือก\"\n" @@ -20858,30 +17427,20 @@ msgstr "เทมเพลตที่ตั้งไว้ล่วงหน้ msgid "Process Preset Template" msgstr "ประมวลผลเทมเพลตที่กำหนดไว้ล่วงหน้า" -msgid "" -"You have not yet chosen which printer preset to create based on. Please " -"choose the vendor and model of the printer" +msgid "You have not yet chosen which printer preset to create based on. Please choose the vendor and model of the printer" msgstr "คุณยังไม่ได้เลือกพรีเซ็ตเครื่องพิมพ์ที่จะใช้สร้าง โปรดเลือกผู้จำหน่ายและรุ่นของเครื่องพิมพ์" -msgid "" -"You have entered a disallowed character in the printable area section on the " -"first page. Please use only numbers." -msgstr "" -"คุณได้ป้อนข้อมูลที่ไม่ถูกต้องในส่วนพื้นที่ที่สามารถพิมพ์ได้ในหน้าแรก โปรดตรวจสอบก่อนที่จะสร้างมัน" +msgid "You have entered a disallowed character in the printable area section on the first page. Please use only numbers." +msgstr "คุณได้ป้อนข้อมูลที่ไม่ถูกต้องในส่วนพื้นที่ที่สามารถพิมพ์ได้ในหน้าแรก โปรดตรวจสอบก่อนที่จะสร้างมัน" msgid "" -"The printer preset you created already has a preset with the same name. Do " -"you want to overwrite it?\n" -"\tYes: Overwrite the printer preset with the same name, and filament and " -"process presets with the same preset name will be recreated \n" -"and filament and process presets without the same preset name will be " -"reserved.\n" +"The printer preset you created already has a preset with the same name. Do you want to overwrite it?\n" +"\tYes: Overwrite the printer preset with the same name, and filament and process presets with the same preset name will be recreated \n" +"and filament and process presets without the same preset name will be reserved.\n" "\tCancel: Do not create a preset; return to the creation interface." msgstr "" -"ค่าที่ตั้งไว้ล่วงหน้าของเครื่องพิมพ์ที่คุณสร้างมีการตั้งค่าล่วงหน้าที่มีชื่อเดียวกันอยู่แล้ว " -"คุณต้องการเขียนทับมันหรือไม่?\n" -"\tใช่: เขียนทับพรีเซ็ตเครื่องพิมพ์ด้วยชื่อเดียวกัน " -"จากนั้นฟิลาเมนต์และพรีเซ็ตกระบวนการที่มีชื่อพรีเซ็ตเดียวกันจะถูกสร้างขึ้นใหม่ \n" +"ค่าที่ตั้งไว้ล่วงหน้าของเครื่องพิมพ์ที่คุณสร้างมีการตั้งค่าล่วงหน้าที่มีชื่อเดียวกันอยู่แล้ว คุณต้องการเขียนทับมันหรือไม่?\n" +"\tใช่: เขียนทับพรีเซ็ตเครื่องพิมพ์ด้วยชื่อเดียวกัน จากนั้นฟิลาเมนต์และพรีเซ็ตกระบวนการที่มีชื่อพรีเซ็ตเดียวกันจะถูกสร้างขึ้นใหม่ \n" "และค่าที่ตั้งล่วงหน้าของเส้นพลาสติกและกระบวนการที่ไม่มีชื่อที่ตั้งไว้ล่วงหน้าเหมือนกันจะถูกสงวนไว้\n" "\tยกเลิก: อย่าสร้างค่าที่ตั้งล่วงหน้า กลับไปที่อินเทอร์เฟซการสร้าง" @@ -20903,26 +17462,19 @@ msgstr "ไม่พบผู้ขาย โปรดเลือกใหม msgid "Current vendor has no models, please reselect." msgstr "ผู้จำหน่ายปัจจุบันไม่มีรุ่น โปรดเลือกใหม่" -msgid "" -"You have not selected the vendor and model or input the custom vendor and " -"model." +msgid "You have not selected the vendor and model or input the custom vendor and model." msgstr "คุณยังไม่ได้เลือกผู้จำหน่ายและรุ่น หรือป้อนผู้จำหน่ายและรุ่นที่กำหนดเอง" -msgid "" -"There may be escape characters in the custom printer vendor or model. Please " -"delete and re-enter." +msgid "There may be escape characters in the custom printer vendor or model. Please delete and re-enter." msgstr "อาจมีอักขระหลีกในผู้จำหน่ายเครื่องพิมพ์แบบกำหนดเองหรือรุ่น กรุณาลบและป้อนใหม่" -msgid "" -"All inputs in the custom printer vendor or model are spaces. Please re-enter." +msgid "All inputs in the custom printer vendor or model are spaces. Please re-enter." msgstr "อินพุตทั้งหมดในผู้จำหน่ายเครื่องพิมพ์แบบกำหนดเองหรือรุ่นคือช่องว่าง กรุณาเข้าใหม่อีกครั้ง" msgid "Please check bed printable shape and origin input." msgstr "โปรดตรวจสอบรูปร่างที่สามารถพิมพ์ฐานพิมพ์ได้และอินพุตจุดเริ่มต้น" -msgid "" -"You have not yet selected the printer to replace the nozzle for; please " -"choose a printer." +msgid "You have not yet selected the printer to replace the nozzle for; please choose a printer." msgstr "คุณยังไม่ได้เลือกเครื่องพิมพ์ที่จะเปลี่ยนหัวฉีด โปรดเลือก" msgid "The entered nozzle diameter is invalid, please re-enter:\n" @@ -20968,25 +17520,19 @@ msgstr "สร้างเส้นพลาสติกแล้ว" msgid "" "Please go to filament settings to edit your presets if you need to.\n" -"Please note that nozzle temperature, hot bed temperature, and maximum " -"volumetric speed each have a significant impact on printing quality. Please " -"set them carefully." +"Please note that nozzle temperature, hot bed temperature, and maximum volumetric speed each have a significant impact on printing quality. Please set them carefully." msgstr "" "โปรดไปที่การตั้งค่าฟิลาเมนต์เพื่อแก้ไขค่าที่ตั้งล่วงหน้าของคุณหากคุณต้องการ\n" -"โปรดทราบว่าอุณหภูมิหัวฉีด อุณหภูมิฐานพิมพ์ร้อน " -"และความเร็วปริมาตรสูงสุดมีผลกระทบอย่างมากต่อคุณภาพการพิมพ์ โปรดตั้งค่าอย่างระมัดระวัง" +"โปรดทราบว่าอุณหภูมิหัวฉีด อุณหภูมิฐานพิมพ์ร้อน และความเร็วปริมาตรสูงสุดมีผลกระทบอย่างมากต่อคุณภาพการพิมพ์ โปรดตั้งค่าอย่างระมัดระวัง" msgid "" "\n" "\n" -"Orca has detected that your user presets synchronization function is not " -"enabled, which may result in unsuccessful Filament settings on the Device " -"page.\n" +"Orca has detected that your user presets synchronization function is not enabled, which may result in unsuccessful Filament settings on the Device page.\n" "Click \"Sync user presets\" to enable the synchronization function." msgstr "" "\n" -"Orca ตรวจพบว่าไม่ได้เปิดใช้งานฟังก์ชันการซิงโครไนซ์ที่ตั้งไว้ล่วงหน้าของผู้ใช้ของคุณ " -"ซึ่งอาจส่งผลให้การตั้งค่า เส้นพลาสติก บนหน้าอุปกรณ์ไม่สำเร็จ\n" +"Orca ตรวจพบว่าไม่ได้เปิดใช้งานฟังก์ชันการซิงโครไนซ์ที่ตั้งไว้ล่วงหน้าของผู้ใช้ของคุณ ซึ่งอาจส่งผลให้การตั้งค่า เส้นพลาสติก บนหน้าอุปกรณ์ไม่สำเร็จ\n" "คลิก \"ซิงค์การตั้งค่าล่วงหน้าของผู้ใช้\" เพื่อเปิดใช้งานฟังก์ชันการซิงโครไนซ์" msgid "Printer Setting" @@ -21027,10 +17573,8 @@ msgstr "ส่งออกสำเร็จ" #, c-format, boost-format msgid "" -"The '%s' folder already exists in the current directory. Do you want to " -"clear it and rebuild it?\n" -"If not, a time suffix will be added, and you can modify the name after " -"creation." +"The '%s' folder already exists in the current directory. Do you want to clear it and rebuild it?\n" +"If not, a time suffix will be added, and you can modify the name after creation." msgstr "" "โฟลเดอร์ '%s' มีอยู่แล้วในไดเร็กทอรีปัจจุบัน คุณต้องการล้างมันและสร้างใหม่หรือไม่\n" "หากไม่เป็นเช่นนั้น ระบบจะเพิ่มส่วนต่อท้ายเวลา และคุณสามารถแก้ไขชื่อได้หลังการสร้าง" @@ -21046,8 +17590,7 @@ msgstr "" "กรุณาปิดและลองอีกครั้ง" msgid "" -"Printer and all the filament and process presets that belongs to the " -"printer.\n" +"Printer and all the filament and process presets that belongs to the printer.\n" "Can be shared with others." msgstr "" "เครื่องพิมพ์และเส้นพลาสติกและค่าที่ตั้งล่วงหน้ากระบวนการทั้งหมดที่เป็นของเครื่องพิมพ์\n" @@ -21060,36 +17603,25 @@ msgstr "" "ชุดพรีเซ็ตเส้นพลาสติกของผู้ใช้\n" "สามารถแบ่งปันกับผู้อื่นได้" -msgid "" -"Only display printers with changes to printer, filament, and process presets " -"are displayed." -msgstr "" -"แสดงเฉพาะชื่อเครื่องพิมพ์ที่มีการเปลี่ยนแปลงเครื่องพิมพ์ เส้นพลาสติก " -"และการตั้งค่าล่วงหน้าของกระบวนการ" +msgid "Only display printers with changes to printer, filament, and process presets are displayed." +msgstr "แสดงเฉพาะชื่อเครื่องพิมพ์ที่มีการเปลี่ยนแปลงเครื่องพิมพ์ เส้นพลาสติก และการตั้งค่าล่วงหน้าของกระบวนการ" msgid "Only display the filament names with changes to filament presets." msgstr "แสดงเฉพาะชื่อเส้นพลาสติกที่มีการเปลี่ยนแปลงการตั้งค่าล่วงหน้าของเส้นพลาสติก" -msgid "" -"Only printer names with user printer presets will be displayed, and each " -"preset you choose will be exported as a zip." -msgstr "" -"เฉพาะชื่อเครื่องพิมพ์ที่มีการตั้งค่าเครื่องพิมพ์ล่วงหน้าของผู้ใช้เท่านั้นที่จะแสดง " -"และการตั้งค่าล่วงหน้าแต่ละรายการที่คุณเลือกจะถูกส่งออกเป็นไฟล์ ZIP" +msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a zip." +msgstr "เฉพาะชื่อเครื่องพิมพ์ที่มีการตั้งค่าเครื่องพิมพ์ล่วงหน้าของผู้ใช้เท่านั้นที่จะแสดง และการตั้งค่าล่วงหน้าแต่ละรายการที่คุณเลือกจะถูกส่งออกเป็นไฟล์ ZIP" msgid "" "Only the filament names with user filament presets will be displayed, \n" -"and all user filament presets in each filament name you select will be " -"exported as a zip." +"and all user filament presets in each filament name you select will be exported as a zip." msgstr "" "เฉพาะชื่อเส้นพลาสติกที่มีการตั้งค่าล่วงหน้าของเส้นพลาสติกผู้ใช้เท่านั้นที่จะถูกแสดง \n" -"และค่าฟิลาเมนต์ของผู้ใช้ทั้งหมดที่กำหนดไว้ล่วงหน้าในแต่ละชื่อฟิลาเมนต์ที่คุณเลือกจะถูกส่งออกเป็นไฟล์ " -"ZIP" +"และค่าฟิลาเมนต์ของผู้ใช้ทั้งหมดที่กำหนดไว้ล่วงหน้าในแต่ละชื่อฟิลาเมนต์ที่คุณเลือกจะถูกส่งออกเป็นไฟล์ ZIP" msgid "" "Only printer names with changed process presets will be displayed, \n" -"and all user process presets in each printer name you select will be " -"exported as a zip." +"and all user process presets in each printer name you select will be exported as a zip." msgstr "" "เฉพาะชื่อเครื่องพิมพ์ที่มีการตั้งค่าล่วงหน้าของกระบวนการที่เปลี่ยนแปลงเท่านั้นที่จะปรากฏขึ้น \n" "และกระบวนการของผู้ใช้ที่ตั้งไว้ล่วงหน้าในชื่อเครื่องพิมพ์แต่ละเครื่องที่คุณเลือกจะถูกส่งออกเป็นไฟล์ ZIP" @@ -21109,12 +17641,8 @@ msgstr "แก้ไขเส้นพลาสติก" msgid "Filament presets under this filament" msgstr "เส้นพลาสติกที่ตั้งไว้ล่วงหน้าภายใต้เส้นพลาสติกนี้" -msgid "" -"Note: If the only preset under this filament is deleted, the filament will " -"be deleted after exiting the dialog." -msgstr "" -"หมายเหตุ: หากลบค่าที่กำหนดไว้ล่วงหน้าภายใต้เส้นพลาสติกนี้ " -"เส้นพลาสติกจะถูกลบหลังจากออกจากกล่องโต้ตอบ" +msgid "Note: If the only preset under this filament is deleted, the filament will be deleted after exiting the dialog." +msgstr "หมายเหตุ: หากลบค่าที่กำหนดไว้ล่วงหน้าภายใต้เส้นพลาสติกนี้ เส้นพลาสติกจะถูกลบหลังจากออกจากกล่องโต้ตอบ" msgid "Presets inherited by other presets cannot be deleted" msgstr "ค่าที่ตั้งล่วงหน้าที่สืบทอดมาจากค่าที่ตั้งล่วงหน้าอื่นไม่สามารถลบได้" @@ -21137,8 +17665,7 @@ msgstr "+ เพิ่มพรีเซ็ต" msgid "" "All the filament presets belong to this filament would be deleted.\n" -"If you are using this filament on your printer, please reset the filament " -"information for that slot." +"If you are using this filament on your printer, please reset the filament information for that slot." msgstr "" "ค่าที่ตั้งไว้ล่วงหน้าของเส้นพลาสติกทั้งหมดที่เป็นของเส้นพลาสติกนี้จะถูกลบ\n" "หากคุณใช้เส้นพลาสติกนี้กับเครื่องพิมพ์ของคุณ โปรดรีเซ็ตข้อมูลเส้นพลาสติกสำหรับช่องนั้น" @@ -21200,23 +17727,16 @@ msgstr "ต้องการเครื่องพิมพ์ที่เล msgid "The start, end or step is not valid value." msgstr "จุดเริ่มต้น สิ้นสุด หรือขั้นตอนไม่ใช่ค่าที่ถูกต้อง" -msgid "" -"The number of printer extruders and the printer selected for calibration " -"does not match." +msgid "The number of printer extruders and the printer selected for calibration does not match." msgstr "จำนวนชุดดันเส้นของเครื่องพิมพ์และเครื่องพิมพ์ที่เลือกสำหรับการสอบเทียบไม่ตรงกัน" #, c-format, boost-format -msgid "" -"The nozzle diameter of %s extruder is 0.2mm which does not support automatic " -"Flow Dynamics calibration." -msgstr "" -"เส้นผ่านศูนย์กลางหัวฉีดของชุดดันเส้น %s คือ 0.2 มม. ซึ่งไม่รองรับการสอบเทียบ Flow Dynamics " -"อัตโนมัติ" +msgid "The nozzle diameter of %s extruder is 0.2mm which does not support automatic Flow Dynamics calibration." +msgstr "เส้นผ่านศูนย์กลางหัวฉีดของชุดดันเส้น %s คือ 0.2 มม. ซึ่งไม่รองรับการสอบเทียบ Flow Dynamics อัตโนมัติ" #, c-format, boost-format msgid "" -"The currently selected nozzle diameter of %s extruder does not match the " -"actual nozzle diameter.\n" +"The currently selected nozzle diameter of %s extruder does not match the actual nozzle diameter.\n" "Please click the Sync button above and restart the calibration." msgstr "" "เส้นผ่านศูนย์กลางหัวฉีดที่เลือกในปัจจุบันของชุดดันเส้น %s ไม่ตรงกับเส้นผ่านศูนย์กลางหัวฉีดจริง\n" @@ -21231,18 +17751,14 @@ msgstr "" #, c-format, boost-format msgid "" -"The currently selected nozzle type of %s extruder does not match the actual " -"printer nozzle type.\n" +"The currently selected nozzle type of %s extruder does not match the actual printer nozzle type.\n" "Please click the Sync button above and restart the calibration." msgstr "" "ประเภทหัวฉีดที่เลือกในปัจจุบันของชุดดันเส้น %s ไม่ตรงกับประเภทหัวฉีดของเครื่องพิมพ์จริง\n" "โปรดคลิกปุ่มซิงค์ด้านบนแล้วเริ่มการปรับเทียบใหม่" -msgid "" -"Unable to calibrate: maybe because the set calibration value range is too " -"large, or the step is too small" -msgstr "" -"ไม่สามารถสอบเทียบ: อาจเป็นเพราะช่วงค่าการสอบเทียบที่ตั้งไว้ใหญ่เกินไป หรือขั้นตอนเล็กเกินไป" +msgid "Unable to calibrate: maybe because the set calibration value range is too large, or the step is too small" +msgstr "ไม่สามารถสอบเทียบ: อาจเป็นเพราะช่วงค่าการสอบเทียบที่ตั้งไว้ใหญ่เกินไป หรือขั้นตอนเล็กเกินไป" msgid "Physical Printer" msgstr "เครื่องพิมพ์ทางกายภาพ" @@ -21250,12 +17766,8 @@ msgstr "เครื่องพิมพ์ทางกายภาพ" msgid "Print Host upload" msgstr "อัพโหลดโฮสต์การพิมพ์" -msgid "" -"Select the network agent implementation for printer communication. Available " -"agents are registered at startup." -msgstr "" -"เลือกการใช้งานตัวแทนเครือข่ายสำหรับการสื่อสารของเครื่องพิมพ์ " -"ตัวแทนที่มีอยู่จะได้รับการลงทะเบียนเมื่อเริ่มต้น" +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "เลือกการใช้งานตัวแทนเครือข่ายสำหรับการสื่อสารของเครื่องพิมพ์ ตัวแทนที่มีอยู่จะได้รับการลงทะเบียนเมื่อเริ่มต้น" msgid "Select a Flashforge printer" msgstr "เลือกเครื่องพิมพ์ Flashforge" @@ -21281,11 +17793,8 @@ msgstr "ดู webui โฮสต์การพิมพ์ในแท็บ msgid "Replace the BambuLab's device tab with print host webui" msgstr "แทนที่แท็บอุปกรณ์ของ BambuLab ด้วย webui โฮสต์การพิมพ์" -msgid "" -"HTTPS CA file is optional. It is only needed if you use HTTPS with a self-" -"signed certificate." -msgstr "" -"ไฟล์ HTTPS CA เป็นทางเลือก จำเป็นเฉพาะเมื่อคุณใช้ HTTPS กับใบรับรองที่ลงนามด้วยตนเอง" +msgid "HTTPS CA file is optional. It is only needed if you use HTTPS with a self-signed certificate." +msgstr "ไฟล์ HTTPS CA เป็นทางเลือก จำเป็นเฉพาะเมื่อคุณใช้ HTTPS กับใบรับรองที่ลงนามด้วยตนเอง" msgid "Certificate files (*.crt, *.pem)|*.crt;*.pem|All files|*.*" msgstr "ไฟล์ใบรับรอง (*.crt, *.pem)|*.crt;*.pem|ไฟล์ทั้งหมด|*.*" @@ -21294,17 +17803,11 @@ msgid "Open CA certificate file" msgstr "เปิดไฟล์ใบรับรอง CA" #, c-format, boost-format -msgid "" -"On this system, %s uses HTTPS certificates from the system Certificate Store " -"or Keychain." +msgid "On this system, %s uses HTTPS certificates from the system Certificate Store or Keychain." msgstr "บนระบบนี้ %s ใช้ใบรับรอง HTTPS จากระบบที่จัดเก็บใบรับรองหรือพวงกุญแจ" -msgid "" -"To use a custom CA file, please import your CA file into Certificate Store / " -"Keychain." -msgstr "" -"หากต้องการใช้ไฟล์ CA ที่กำหนดเอง โปรดนำเข้าไฟล์ CA ของคุณไปที่ Certificate Store / " -"Keychain" +msgid "To use a custom CA file, please import your CA file into Certificate Store / Keychain." +msgstr "หากต้องการใช้ไฟล์ CA ที่กำหนดเอง โปรดนำเข้าไฟล์ CA ของคุณไปที่ Certificate Store / Keychain" msgid "Login/Test" msgstr "เข้าสู่ระบบ/ทดสอบ" @@ -21385,12 +17888,8 @@ msgstr "การเชื่อมต่อกับ FlashAir ทำงาน msgid "Could not connect to FlashAir" msgstr "ไม่สามารถเชื่อมต่อกับ FlashAir ได้" -msgid "" -"Note: FlashAir with firmware 2.00.02 or newer and activated upload function " -"is required." -msgstr "" -"หมายเหตุ: จำเป็นต้องมี FlashAir ที่มีเฟิร์มแวร์ 2.00.02 หรือใหม่กว่า " -"และเปิดใช้งานฟังก์ชันอัพโหลดแล้ว" +msgid "Note: FlashAir with firmware 2.00.02 or newer and activated upload function is required." +msgstr "หมายเหตุ: จำเป็นต้องมี FlashAir ที่มีเฟิร์มแวร์ 2.00.02 หรือใหม่กว่า และเปิดใช้งานฟังก์ชันอัพโหลดแล้ว" msgid "Connection to MKS is working correctly." msgstr "การเชื่อมต่อกับ MKS ทำงานอย่างถูกต้อง" @@ -21480,372 +17979,146 @@ msgstr "" "เนื้อหาข้อความ: \"%1%\"\n" "ข้อผิดพลาด: \"%2%\"" -msgid "" -"It has a small layer height. This results in almost negligible layer lines " -"and high print quality. It is suitable for most printing cases." -msgstr "" -"มีความสูงของชั้นเล็ก ๆ ส่งผลให้เส้นชั้นแทบไม่มีความสำคัญและคุณภาพการพิมพ์สูง " -"เหมาะสำหรับกรณีการพิมพ์ส่วนใหญ่" +msgid "It has a small layer height. This results in almost negligible layer lines and high print quality. It is suitable for most printing cases." +msgstr "มีความสูงของชั้นเล็ก ๆ ส่งผลให้เส้นชั้นแทบไม่มีความสำคัญและคุณภาพการพิมพ์สูง เหมาะสำหรับกรณีการพิมพ์ส่วนใหญ่" -msgid "" -"Compared with the default profile of a 0.2 mm nozzle, it has lower speeds " -"and acceleration, and the sparse infill pattern is Gyroid. This results in " -"much higher print quality but a much longer print time." -msgstr "" -"เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.2 มม. จะมีความเร็วและความเร่งที่ต่ำกว่า " -"และรูปแบบไส้ในแบบโปร่งคือ Gyroid ส่งผลให้คุณภาพการพิมพ์สูงขึ้นมาก แต่ใช้เวลาพิมพ์นานกว่ามาก" +msgid "Compared with the default profile of a 0.2 mm nozzle, it has lower speeds and acceleration, and the sparse infill pattern is Gyroid. This results in much higher print quality but a much longer print time." +msgstr "เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.2 มม. จะมีความเร็วและความเร่งที่ต่ำกว่า และรูปแบบไส้ในแบบโปร่งคือ Gyroid ส่งผลให้คุณภาพการพิมพ์สูงขึ้นมาก แต่ใช้เวลาพิมพ์นานกว่ามาก" -msgid "" -"Compared with the default profile of a 0.2 mm nozzle, it has a slightly " -"bigger layer height. This results in almost negligible layer lines and " -"slightly shorter print time." -msgstr "" -"เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.2 มม. จะมีความสูงของชั้นที่ใหญ่กว่าเล็กน้อย " -"ส่งผลให้เส้นเลเยอร์แทบไม่มีนัยสำคัญและใช้เวลาพิมพ์สั้นลงเล็กน้อย" +msgid "Compared with the default profile of a 0.2 mm nozzle, it has a slightly bigger layer height. This results in almost negligible layer lines and slightly shorter print time." +msgstr "เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.2 มม. จะมีความสูงของชั้นที่ใหญ่กว่าเล็กน้อย ส่งผลให้เส้นเลเยอร์แทบไม่มีนัยสำคัญและใช้เวลาพิมพ์สั้นลงเล็กน้อย" -msgid "" -"Compared with the default profile of a 0.2 mm nozzle, it has a bigger layer " -"height. This results in slightly visible layer lines but shorter print time." -msgstr "" -"เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.2 มม. จะมีความสูงของชั้นที่ใหญ่กว่า " -"ส่งผลให้มองเห็นเส้นเลเยอร์ได้เล็กน้อยแต่ใช้เวลาพิมพ์สั้นลง" +msgid "Compared with the default profile of a 0.2 mm nozzle, it has a bigger layer height. This results in slightly visible layer lines but shorter print time." +msgstr "เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.2 มม. จะมีความสูงของชั้นที่ใหญ่กว่า ส่งผลให้มองเห็นเส้นเลเยอร์ได้เล็กน้อยแต่ใช้เวลาพิมพ์สั้นลง" -msgid "" -"Compared with the default profile of a 0.2 mm nozzle, it has a smaller layer " -"height. This results in almost invisible layer lines and higher print " -"quality but longer print time." -msgstr "" -"เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.2 มม. จะมีความสูงของชั้นที่เล็กกว่า " -"ส่งผลให้มีเส้นชั้นที่แทบจะมองไม่เห็นและคุณภาพการพิมพ์ที่สูงขึ้นแต่ใช้เวลาพิมพ์นานขึ้น" +msgid "Compared with the default profile of a 0.2 mm nozzle, it has a smaller layer height. This results in almost invisible layer lines and higher print quality but longer print time." +msgstr "เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.2 มม. จะมีความสูงของชั้นที่เล็กกว่า ส่งผลให้มีเส้นชั้นที่แทบจะมองไม่เห็นและคุณภาพการพิมพ์ที่สูงขึ้นแต่ใช้เวลาพิมพ์นานขึ้น" -msgid "" -"Compared with the default profile of a 0.2 mm nozzle, it has a smaller layer " -"lines, lower speeds and acceleration, and the sparse infill pattern is " -"Gyroid. This results in almost invisible layer lines and much higher print " -"quality but much longer print time." -msgstr "" -"เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.2 มม. จะมีเส้นชั้นที่เล็กกว่า " -"ความเร็วและความเร่งที่ต่ำกว่า และรูปแบบไส้ในแบบโปร่งคือ Gyroid " -"ส่งผลให้มีเส้นชั้นที่แทบจะมองไม่เห็นและคุณภาพการพิมพ์ที่สูงขึ้นมาก แต่ใช้เวลาพิมพ์นานกว่ามาก" +msgid "Compared with the default profile of a 0.2 mm nozzle, it has a smaller layer lines, lower speeds and acceleration, and the sparse infill pattern is Gyroid. This results in almost invisible layer lines and much higher print quality but much longer print time." +msgstr "เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.2 มม. จะมีเส้นชั้นที่เล็กกว่า ความเร็วและความเร่งที่ต่ำกว่า และรูปแบบไส้ในแบบโปร่งคือ Gyroid ส่งผลให้มีเส้นชั้นที่แทบจะมองไม่เห็นและคุณภาพการพิมพ์ที่สูงขึ้นมาก แต่ใช้เวลาพิมพ์นานกว่ามาก" -msgid "" -"Compared with the default profile of a 0.2 mm nozzle, it has a smaller layer " -"height. This results in minimal layer lines and higher print quality but " -"longer print time." -msgstr "" -"เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.2 มม. จะมีความสูงของชั้นที่เล็กกว่า " -"ส่งผลให้มีเส้นชั้นน้อยที่สุดและคุณภาพการพิมพ์สูงขึ้นแต่ใช้เวลาพิมพ์นานขึ้น" +msgid "Compared with the default profile of a 0.2 mm nozzle, it has a smaller layer height. This results in minimal layer lines and higher print quality but longer print time." +msgstr "เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.2 มม. จะมีความสูงของชั้นที่เล็กกว่า ส่งผลให้มีเส้นชั้นน้อยที่สุดและคุณภาพการพิมพ์สูงขึ้นแต่ใช้เวลาพิมพ์นานขึ้น" -msgid "" -"Compared with the default profile of a 0.2 mm nozzle, it has a smaller layer " -"lines, lower speeds and acceleration, and the sparse infill pattern is " -"Gyroid. This results in minimal layer lines and much higher print quality " -"but much longer print time." -msgstr "" -"เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.2 มม. จะมีเส้นชั้นที่เล็กกว่า " -"ความเร็วและความเร่งที่ต่ำกว่า และรูปแบบไส้ในแบบโปร่งคือ Gyroid " -"ส่งผลให้มีเส้นชั้นน้อยที่สุดและคุณภาพการพิมพ์สูงขึ้นมาก แต่ใช้เวลาพิมพ์นานกว่ามาก" +msgid "Compared with the default profile of a 0.2 mm nozzle, it has a smaller layer lines, lower speeds and acceleration, and the sparse infill pattern is Gyroid. This results in minimal layer lines and much higher print quality but much longer print time." +msgstr "เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.2 มม. จะมีเส้นชั้นที่เล็กกว่า ความเร็วและความเร่งที่ต่ำกว่า และรูปแบบไส้ในแบบโปร่งคือ Gyroid ส่งผลให้มีเส้นชั้นน้อยที่สุดและคุณภาพการพิมพ์สูงขึ้นมาก แต่ใช้เวลาพิมพ์นานกว่ามาก" -msgid "" -"It has a normal layer height. This results in average layer lines and print " -"quality. It is suitable for most printing cases." -msgstr "" -"มีความสูงของชั้นปกติ ซึ่งส่งผลให้ได้เส้นชั้นเฉลี่ยและคุณภาพการพิมพ์ เหมาะสำหรับกรณีการพิมพ์ส่วนใหญ่" +msgid "It has a normal layer height. This results in average layer lines and print quality. It is suitable for most printing cases." +msgstr "มีความสูงของชั้นปกติ ซึ่งส่งผลให้ได้เส้นชั้นเฉลี่ยและคุณภาพการพิมพ์ เหมาะสำหรับกรณีการพิมพ์ส่วนใหญ่" -msgid "" -"Compared with the default profile of a 0.4 mm nozzle, it has more wall loops " -"and a higher sparse infill density. This results in higher print strength " -"but more filament consumption and longer print time." -msgstr "" -"เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.4 มม. " -"จะมีห่วงติดผนังมากกว่าและมีความหนาแน่นของช่องไส้ในที่เบากว่า " -"ส่งผลให้มีความแข็งแรงในการพิมพ์สูงขึ้นแต่ใช้เส้นพลาสติกมากขึ้นและใช้เวลาพิมพ์นานขึ้น" +msgid "Compared with the default profile of a 0.4 mm nozzle, it has more wall loops and a higher sparse infill density. This results in higher print strength but more filament consumption and longer print time." +msgstr "เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.4 มม. จะมีห่วงติดผนังมากกว่าและมีความหนาแน่นของช่องไส้ในที่เบากว่า ส่งผลให้มีความแข็งแรงในการพิมพ์สูงขึ้นแต่ใช้เส้นพลาสติกมากขึ้นและใช้เวลาพิมพ์นานขึ้น" -msgid "" -"Compared with the default profile of a 0.4 mm nozzle, it has a bigger layer " -"height. This results in more apparent layer lines and lower print quality, " -"but slightly shorter print time." -msgstr "" -"เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.4 มม. จะมีความสูงของชั้นที่ใหญ่กว่า " -"ส่งผลให้มีเส้นชั้นที่ชัดเจนมากขึ้นและคุณภาพการพิมพ์ลดลง แต่ใช้เวลาพิมพ์สั้นลงเล็กน้อย" +msgid "Compared with the default profile of a 0.4 mm nozzle, it has a bigger layer height. This results in more apparent layer lines and lower print quality, but slightly shorter print time." +msgstr "เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.4 มม. จะมีความสูงของชั้นที่ใหญ่กว่า ส่งผลให้มีเส้นชั้นที่ชัดเจนมากขึ้นและคุณภาพการพิมพ์ลดลง แต่ใช้เวลาพิมพ์สั้นลงเล็กน้อย" -msgid "" -"Compared with the default profile of a 0.4 mm nozzle, it has a bigger layer " -"height. This results in more apparent layer lines and lower print quality, " -"but shorter print time." -msgstr "" -"เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.4 มม. จะมีความสูงของชั้นที่ใหญ่กว่า " -"ส่งผลให้มีเส้นชั้นที่ชัดเจนมากขึ้นและคุณภาพการพิมพ์ลดลง แต่ใช้เวลาพิมพ์สั้นลง" +msgid "Compared with the default profile of a 0.4 mm nozzle, it has a bigger layer height. This results in more apparent layer lines and lower print quality, but shorter print time." +msgstr "เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.4 มม. จะมีความสูงของชั้นที่ใหญ่กว่า ส่งผลให้มีเส้นชั้นที่ชัดเจนมากขึ้นและคุณภาพการพิมพ์ลดลง แต่ใช้เวลาพิมพ์สั้นลง" -msgid "" -"Compared with the default profile of a 0.4 mm nozzle, it has a smaller layer " -"height. This results in less apparent layer lines and higher print quality " -"but longer print time." -msgstr "" -"เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.4 มม. จะมีความสูงของชั้นที่เล็กกว่า " -"ส่งผลให้มีเส้นชั้นที่ชัดเจนน้อยลงและคุณภาพการพิมพ์สูงขึ้นแต่ใช้เวลาพิมพ์นานขึ้น" +msgid "Compared with the default profile of a 0.4 mm nozzle, it has a smaller layer height. This results in less apparent layer lines and higher print quality but longer print time." +msgstr "เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.4 มม. จะมีความสูงของชั้นที่เล็กกว่า ส่งผลให้มีเส้นชั้นที่ชัดเจนน้อยลงและคุณภาพการพิมพ์สูงขึ้นแต่ใช้เวลาพิมพ์นานขึ้น" -msgid "" -"Compared with the default profile of a 0.4 mm nozzle, it has a smaller layer " -"height, lower speeds and acceleration, and the sparse infill pattern is " -"Gyroid. This results in less apparent layer lines and much higher print " -"quality but much longer print time." -msgstr "" -"เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.4 มม. จะมีความสูงของชั้นที่เล็กกว่า " -"ความเร็วและความเร่งที่ต่ำกว่า และรูปแบบไส้ในแบบโปร่งคือ Gyroid " -"ส่งผลให้เส้นเลเยอร์ปรากฏน้อยลงและคุณภาพการพิมพ์สูงขึ้นมาก แต่ใช้เวลาพิมพ์นานกว่ามาก" +msgid "Compared with the default profile of a 0.4 mm nozzle, it has a smaller layer height, lower speeds and acceleration, and the sparse infill pattern is Gyroid. This results in less apparent layer lines and much higher print quality but much longer print time." +msgstr "เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.4 มม. จะมีความสูงของชั้นที่เล็กกว่า ความเร็วและความเร่งที่ต่ำกว่า และรูปแบบไส้ในแบบโปร่งคือ Gyroid ส่งผลให้เส้นเลเยอร์ปรากฏน้อยลงและคุณภาพการพิมพ์สูงขึ้นมาก แต่ใช้เวลาพิมพ์นานกว่ามาก" -msgid "" -"Compared with the default profile of a 0.4 mm nozzle, it has a smaller layer " -"height. This results in almost negligible layer lines and higher print " -"quality but longer print time." -msgstr "" -"เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.4 มม. จะมีความสูงของชั้นที่เล็กกว่า " -"ส่งผลให้เส้นเลเยอร์แทบไม่มีความสำคัญและคุณภาพการพิมพ์สูงขึ้นแต่ใช้เวลาพิมพ์นานขึ้น" +msgid "Compared with the default profile of a 0.4 mm nozzle, it has a smaller layer height. This results in almost negligible layer lines and higher print quality but longer print time." +msgstr "เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.4 มม. จะมีความสูงของชั้นที่เล็กกว่า ส่งผลให้เส้นเลเยอร์แทบไม่มีความสำคัญและคุณภาพการพิมพ์สูงขึ้นแต่ใช้เวลาพิมพ์นานขึ้น" -msgid "" -"Compared with the default profile of a 0.4 mm nozzle, it has a smaller layer " -"height, lower speeds and acceleration, and the sparse infill pattern is " -"Gyroid. This results in almost negligible layer lines and much higher print " -"quality but much longer print time." -msgstr "" -"เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.4 มม. จะมีความสูงของชั้นที่เล็กกว่า " -"ความเร็วและความเร่งที่ต่ำกว่า และรูปแบบไส้ในแบบโปร่งคือ Gyroid " -"ส่งผลให้เส้นเลเยอร์แทบไม่มีความสำคัญและคุณภาพการพิมพ์สูงขึ้นมาก แต่ใช้เวลาพิมพ์นานกว่ามาก" +msgid "Compared with the default profile of a 0.4 mm nozzle, it has a smaller layer height, lower speeds and acceleration, and the sparse infill pattern is Gyroid. This results in almost negligible layer lines and much higher print quality but much longer print time." +msgstr "เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.4 มม. จะมีความสูงของชั้นที่เล็กกว่า ความเร็วและความเร่งที่ต่ำกว่า และรูปแบบไส้ในแบบโปร่งคือ Gyroid ส่งผลให้เส้นเลเยอร์แทบไม่มีความสำคัญและคุณภาพการพิมพ์สูงขึ้นมาก แต่ใช้เวลาพิมพ์นานกว่ามาก" -msgid "" -"Compared with the default profile of a 0.4 mm nozzle, it has a smaller layer " -"height. This results in almost negligible layer lines and longer print time." -msgstr "" -"เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.4 มม. จะมีความสูงของชั้นที่เล็กกว่า " -"ส่งผลให้เส้นชั้นแทบไม่มีนัยสำคัญและใช้เวลาพิมพ์นานขึ้น" +msgid "Compared with the default profile of a 0.4 mm nozzle, it has a smaller layer height. This results in almost negligible layer lines and longer print time." +msgstr "เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.4 มม. จะมีความสูงของชั้นที่เล็กกว่า ส่งผลให้เส้นชั้นแทบไม่มีนัยสำคัญและใช้เวลาพิมพ์นานขึ้น" -msgid "" -"It has a big layer height. This results in apparent layer lines and ordinary " -"print quality and print time." +msgid "It has a big layer height. This results in apparent layer lines and ordinary print quality and print time." msgstr "มีความสูงของชั้นใหญ่ ส่งผลให้ได้เส้นชั้นที่ชัดเจนและคุณภาพการพิมพ์และเวลาในการพิมพ์ตามปกติ" -msgid "" -"Compared with the default profile of a 0.6 mm nozzle, it has more wall loops " -"and a higher sparse infill density. This results in higher print strength " -"but more filament consumption and longer print time." -msgstr "" -"เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.6 มม. " -"จะมีห่วงติดผนังมากกว่าและมีความหนาแน่นของช่องไส้ในที่เบากว่า " -"ส่งผลให้มีความแข็งแรงในการพิมพ์สูงขึ้นแต่ใช้เส้นพลาสติกมากขึ้นและใช้เวลาพิมพ์นานขึ้น" +msgid "Compared with the default profile of a 0.6 mm nozzle, it has more wall loops and a higher sparse infill density. This results in higher print strength but more filament consumption and longer print time." +msgstr "เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.6 มม. จะมีห่วงติดผนังมากกว่าและมีความหนาแน่นของช่องไส้ในที่เบากว่า ส่งผลให้มีความแข็งแรงในการพิมพ์สูงขึ้นแต่ใช้เส้นพลาสติกมากขึ้นและใช้เวลาพิมพ์นานขึ้น" -msgid "" -"Compared with the default profile of a 0.6 mm nozzle, it has a bigger layer " -"height. This results in more apparent layer lines and lower print quality, " -"but shorter print time in some cases." -msgstr "" -"เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.6 มม. จะมีความสูงของชั้นที่ใหญ่กว่า " -"ซึ่งส่งผลให้มีเส้นชั้นที่ชัดเจนมากขึ้นและคุณภาพการพิมพ์ลดลง แต่เวลาในการพิมพ์สั้นลงในบางกรณี" +msgid "Compared with the default profile of a 0.6 mm nozzle, it has a bigger layer height. This results in more apparent layer lines and lower print quality, but shorter print time in some cases." +msgstr "เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.6 มม. จะมีความสูงของชั้นที่ใหญ่กว่า ซึ่งส่งผลให้มีเส้นชั้นที่ชัดเจนมากขึ้นและคุณภาพการพิมพ์ลดลง แต่เวลาในการพิมพ์สั้นลงในบางกรณี" -msgid "" -"Compared with the default profile of a 0.6 mm nozzle, it has a bigger layer " -"height. This results in much more apparent layer lines and much lower print " -"quality, but shorter print time in some cases." -msgstr "" -"เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.6 มม. จะมีความสูงของชั้นที่ใหญ่กว่า " -"ซึ่งส่งผลให้มีเส้นชั้นที่ชัดเจนมากขึ้นและคุณภาพการพิมพ์ลดลงมาก แต่ในบางกรณีใช้เวลาพิมพ์สั้นลง" +msgid "Compared with the default profile of a 0.6 mm nozzle, it has a bigger layer height. This results in much more apparent layer lines and much lower print quality, but shorter print time in some cases." +msgstr "เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.6 มม. จะมีความสูงของชั้นที่ใหญ่กว่า ซึ่งส่งผลให้มีเส้นชั้นที่ชัดเจนมากขึ้นและคุณภาพการพิมพ์ลดลงมาก แต่ในบางกรณีใช้เวลาพิมพ์สั้นลง" -msgid "" -"Compared with the default profile of a 0.6 mm nozzle, it has a smaller layer " -"height. This results in less apparent layer lines and slight higher print " -"quality but longer print time." -msgstr "" -"เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.6 มม. จะมีความสูงของชั้นที่เล็กกว่า " -"ส่งผลให้มีเส้นชั้นที่ชัดเจนน้อยลงและคุณภาพการพิมพ์สูงขึ้นเล็กน้อยแต่ใช้เวลาพิมพ์นานขึ้น" +msgid "Compared with the default profile of a 0.6 mm nozzle, it has a smaller layer height. This results in less apparent layer lines and slight higher print quality but longer print time." +msgstr "เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.6 มม. จะมีความสูงของชั้นที่เล็กกว่า ส่งผลให้มีเส้นชั้นที่ชัดเจนน้อยลงและคุณภาพการพิมพ์สูงขึ้นเล็กน้อยแต่ใช้เวลาพิมพ์นานขึ้น" -msgid "" -"Compared with the default profile of a 0.6 mm nozzle, it has a smaller layer " -"height. This results in less apparent layer lines and higher print quality " -"but longer print time." -msgstr "" -"เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.6 มม. จะมีความสูงของชั้นที่เล็กกว่า " -"ส่งผลให้มีเส้นชั้นที่ชัดเจนน้อยลงและคุณภาพการพิมพ์สูงขึ้นแต่ใช้เวลาพิมพ์นานขึ้น" +msgid "Compared with the default profile of a 0.6 mm nozzle, it has a smaller layer height. This results in less apparent layer lines and higher print quality but longer print time." +msgstr "เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.6 มม. จะมีความสูงของชั้นที่เล็กกว่า ส่งผลให้มีเส้นชั้นที่ชัดเจนน้อยลงและคุณภาพการพิมพ์สูงขึ้นแต่ใช้เวลาพิมพ์นานขึ้น" -msgid "" -"It has a very big layer height. This results in very apparent layer lines, " -"low print quality and shorter print time." +msgid "It has a very big layer height. This results in very apparent layer lines, low print quality and shorter print time." msgstr "มีความสูงของชั้นที่ใหญ่มาก ส่งผลให้มีเส้นชั้นที่ชัดเจน คุณภาพการพิมพ์ต่ำ และใช้เวลาพิมพ์สั้นลง" -msgid "" -"Compared with the default profile of a 0.8 mm nozzle, it has a bigger layer " -"height. This results in very apparent layer lines and much lower print " -"quality, but shorter print time in some cases." -msgstr "" -"เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.8 มม. จะมีความสูงของชั้นที่ใหญ่กว่า " -"ซึ่งส่งผลให้มีเส้นชั้นที่ชัดเจนและคุณภาพการพิมพ์ลดลงมาก แต่ในบางกรณีใช้เวลาพิมพ์สั้นลง" +msgid "Compared with the default profile of a 0.8 mm nozzle, it has a bigger layer height. This results in very apparent layer lines and much lower print quality, but shorter print time in some cases." +msgstr "เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.8 มม. จะมีความสูงของชั้นที่ใหญ่กว่า ซึ่งส่งผลให้มีเส้นชั้นที่ชัดเจนและคุณภาพการพิมพ์ลดลงมาก แต่ในบางกรณีใช้เวลาพิมพ์สั้นลง" -msgid "" -"Compared with the default profile of a 0.8 mm nozzle, it has a much bigger " -"layer height. This results in extremely apparent layer lines and much lower " -"print quality, but much shorter print time in some cases." -msgstr "" -"เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.8 มม. จะมีความสูงของชั้นที่ใหญ่กว่ามาก " -"ซึ่งส่งผลให้มีเส้นชั้นที่ชัดเจนมากและคุณภาพการพิมพ์ต่ำลงมาก แต่ในบางกรณีใช้เวลาพิมพ์สั้นลงมาก" +msgid "Compared with the default profile of a 0.8 mm nozzle, it has a much bigger layer height. This results in extremely apparent layer lines and much lower print quality, but much shorter print time in some cases." +msgstr "เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.8 มม. จะมีความสูงของชั้นที่ใหญ่กว่ามาก ซึ่งส่งผลให้มีเส้นชั้นที่ชัดเจนมากและคุณภาพการพิมพ์ต่ำลงมาก แต่ในบางกรณีใช้เวลาพิมพ์สั้นลงมาก" -msgid "" -"Compared with the default profile of a 0.8 mm nozzle, it has a slightly " -"smaller layer height. This results in slightly less but still apparent layer " -"lines and slightly higher print quality but longer print time in some cases." -msgstr "" -"เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.8 มม. จะมีความสูงของชั้นที่เล็กกว่าเล็กน้อย " -"ซึ่งส่งผลให้มีเส้นเลเยอร์น้อยลงเล็กน้อยแต่ยังคงปรากฏชัดเจน และคุณภาพการพิมพ์สูงขึ้นเล็กน้อย " -"แต่ใช้เวลาพิมพ์นานขึ้นในบางกรณี" +msgid "Compared with the default profile of a 0.8 mm nozzle, it has a slightly smaller layer height. This results in slightly less but still apparent layer lines and slightly higher print quality but longer print time in some cases." +msgstr "เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.8 มม. จะมีความสูงของชั้นที่เล็กกว่าเล็กน้อย ซึ่งส่งผลให้มีเส้นเลเยอร์น้อยลงเล็กน้อยแต่ยังคงปรากฏชัดเจน และคุณภาพการพิมพ์สูงขึ้นเล็กน้อย แต่ใช้เวลาพิมพ์นานขึ้นในบางกรณี" -msgid "" -"Compared with the default profile of a 0.8 mm nozzle, it has a smaller layer " -"height. This results in less but still apparent layer lines and slightly " -"higher print quality but longer print time in some cases." -msgstr "" -"เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.8 มม. จะมีความสูงของชั้นที่เล็กกว่า " -"ซึ่งส่งผลให้มีเส้นเลเยอร์น้อยลงแต่ยังคงปรากฏชัดเจน และคุณภาพการพิมพ์สูงขึ้นเล็กน้อย " -"แต่ใช้เวลาพิมพ์นานขึ้นในบางกรณี" +msgid "Compared with the default profile of a 0.8 mm nozzle, it has a smaller layer height. This results in less but still apparent layer lines and slightly higher print quality but longer print time in some cases." +msgstr "เมื่อเปรียบเทียบกับโปรไฟล์เริ่มต้นของหัวฉีดขนาด 0.8 มม. จะมีความสูงของชั้นที่เล็กกว่า ซึ่งส่งผลให้มีเส้นเลเยอร์น้อยลงแต่ยังคงปรากฏชัดเจน และคุณภาพการพิมพ์สูงขึ้นเล็กน้อย แต่ใช้เวลาพิมพ์นานขึ้นในบางกรณี" -msgid "" -"This is neither a commonly used filament, nor one of Bambu filaments, and it " -"varies a lot from brand to brand. So, it's highly recommended to ask its " -"vendor for suitable profile before printing and adjust some parameters " -"according to its performances." -msgstr "" -"นี่ไม่ใช่เส้นพลาสติกที่ใช้กันทั่วไปหรือไม่ใช่เส้นพลาสติก Bambu เส้นใดเส้นหนึ่ง " -"และมีความแตกต่างกันมากในแต่ละยี่ห้อ " -"ดังนั้นจึงขอแนะนำอย่างยิ่งให้สอบถามผู้จำหน่ายเกี่ยวกับโปรไฟล์ที่เหมาะสมก่อนพิมพ์ " -"และปรับพารามิเตอร์บางอย่างตามประสิทธิภาพ" +msgid "This is neither a commonly used filament, nor one of Bambu filaments, and it varies a lot from brand to brand. So, it's highly recommended to ask its vendor for suitable profile before printing and adjust some parameters according to its performances." +msgstr "นี่ไม่ใช่เส้นพลาสติกที่ใช้กันทั่วไปหรือไม่ใช่เส้นพลาสติก Bambu เส้นใดเส้นหนึ่ง และมีความแตกต่างกันมากในแต่ละยี่ห้อ ดังนั้นจึงขอแนะนำอย่างยิ่งให้สอบถามผู้จำหน่ายเกี่ยวกับโปรไฟล์ที่เหมาะสมก่อนพิมพ์ และปรับพารามิเตอร์บางอย่างตามประสิทธิภาพ" -msgid "" -"When printing this filament, there's a risk of warping and low layer " -"adhesion strength. To get better results, please refer to this wiki: " -"Printing Tips for High Temp / Engineering materials." -msgstr "" -"เมื่อพิมพ์ฟิลาเมนต์นี้ มีความเสี่ยงที่จะเกิดการบิดงอและมีการยึดเกาะต่ำ เพื่อให้ได้ผลลัพธ์ที่ดีขึ้น " -"โปรดดูที่วิกินี้: เคล็ดลับการพิมพ์สำหรับวัสดุอุณหภูมิสูง / วิศวกรรม" +msgid "When printing this filament, there's a risk of warping and low layer adhesion strength. To get better results, please refer to this wiki: Printing Tips for High Temp / Engineering materials." +msgstr "เมื่อพิมพ์ฟิลาเมนต์นี้ มีความเสี่ยงที่จะเกิดการบิดงอและมีการยึดเกาะต่ำ เพื่อให้ได้ผลลัพธ์ที่ดีขึ้น โปรดดูที่วิกินี้: เคล็ดลับการพิมพ์สำหรับวัสดุอุณหภูมิสูง / วิศวกรรม" -msgid "" -"When printing this filament, there's a risk of nozzle clogging, oozing, " -"warping and low layer adhesion strength. To get better results, please refer " -"to this wiki: Printing Tips for High Temp / Engineering materials." -msgstr "" -"เมื่อพิมพ์ฟิลาเมนต์นี้ มีความเสี่ยงที่หัวฉีดจะอุดตัน ไหลซึม บิดเบี้ยว และมีความแข็งแรงในการยึดเกาะต่ำ " -"เพื่อให้ได้ผลลัพธ์ที่ดีขึ้น โปรดดูที่วิกินี้: เคล็ดลับการพิมพ์สำหรับวัสดุอุณหภูมิสูง / วิศวกรรม" +msgid "When printing this filament, there's a risk of nozzle clogging, oozing, warping and low layer adhesion strength. To get better results, please refer to this wiki: Printing Tips for High Temp / Engineering materials." +msgstr "เมื่อพิมพ์ฟิลาเมนต์นี้ มีความเสี่ยงที่หัวฉีดจะอุดตัน ไหลซึม บิดเบี้ยว และมีความแข็งแรงในการยึดเกาะต่ำ เพื่อให้ได้ผลลัพธ์ที่ดีขึ้น โปรดดูที่วิกินี้: เคล็ดลับการพิมพ์สำหรับวัสดุอุณหภูมิสูง / วิศวกรรม" -msgid "" -"To get better transparent or translucent results with the corresponding " -"filament, please refer to this wiki: Printing tips for transparent PETG." -msgstr "" -"หากต้องการผลลัพธ์ที่โปร่งใสหรือโปร่งแสงดีขึ้นด้วยเส้นพลาสติกที่เกี่ยวข้อง โปรดดูที่วิกินี้: " -"เคล็ดลับการพิมพ์สำหรับ PETG แบบโปร่งใส" +msgid "To get better transparent or translucent results with the corresponding filament, please refer to this wiki: Printing tips for transparent PETG." +msgstr "หากต้องการผลลัพธ์ที่โปร่งใสหรือโปร่งแสงดีขึ้นด้วยเส้นพลาสติกที่เกี่ยวข้อง โปรดดูที่วิกินี้: เคล็ดลับการพิมพ์สำหรับ PETG แบบโปร่งใส" -msgid "" -"To make the prints get higher gloss, please dry the filament before use, and " -"set the outer wall speed to be 40 to 60 mm/s when slicing." -msgstr "" -"เพื่อให้งานพิมพ์มีความมันวาวมากขึ้น โปรดทำให้เส้นพลาสติกแห้งก่อนใช้งาน " -"และตั้งค่าความเร็วผนังด้านนอกเป็น 40 ถึง 60 มม./วินาที เมื่อสไลซ์" +msgid "To make the prints get higher gloss, please dry the filament before use, and set the outer wall speed to be 40 to 60 mm/s when slicing." +msgstr "เพื่อให้งานพิมพ์มีความมันวาวมากขึ้น โปรดทำให้เส้นพลาสติกแห้งก่อนใช้งาน และตั้งค่าความเร็วผนังด้านนอกเป็น 40 ถึง 60 มม./วินาที เมื่อสไลซ์" -msgid "" -"This filament is only used to print models with a low density usually, and " -"some special parameters are required. To get better printing quality, please " -"refer to this wiki: Instructions for printing RC model with foaming PLA (PLA " -"Aero)." -msgstr "" -"เส้นพลาสติกนี้ใช้สำหรับพิมพ์แบบจำลองที่มีความหนาแน่นต่ำเท่านั้น " -"และจำเป็นต้องมีพารามิเตอร์พิเศษบางอย่าง หากต้องการคุณภาพการพิมพ์ที่ดีขึ้น โปรดดูที่วิกินี้: " -"คำแนะนำสำหรับการพิมพ์รุ่น RC ด้วยฟอง PLA (PLA Aero)" +msgid "This filament is only used to print models with a low density usually, and some special parameters are required. To get better printing quality, please refer to this wiki: Instructions for printing RC model with foaming PLA (PLA Aero)." +msgstr "เส้นพลาสติกนี้ใช้สำหรับพิมพ์แบบจำลองที่มีความหนาแน่นต่ำเท่านั้น และจำเป็นต้องมีพารามิเตอร์พิเศษบางอย่าง หากต้องการคุณภาพการพิมพ์ที่ดีขึ้น โปรดดูที่วิกินี้: คำแนะนำสำหรับการพิมพ์รุ่น RC ด้วยฟอง PLA (PLA Aero)" -msgid "" -"This filament is only used to print models with a low density usually, and " -"some special parameters are required. To get better printing quality, please " -"refer to this wiki: ASA Aero Printing Guide." -msgstr "" -"เส้นพลาสติกนี้ใช้สำหรับพิมพ์แบบจำลองที่มีความหนาแน่นต่ำเท่านั้น " -"และจำเป็นต้องมีพารามิเตอร์พิเศษบางอย่าง หากต้องการคุณภาพการพิมพ์ที่ดีขึ้น โปรดดูที่วิกินี้: ASA " -"Aero Printing Guide" +msgid "This filament is only used to print models with a low density usually, and some special parameters are required. To get better printing quality, please refer to this wiki: ASA Aero Printing Guide." +msgstr "เส้นพลาสติกนี้ใช้สำหรับพิมพ์แบบจำลองที่มีความหนาแน่นต่ำเท่านั้น และจำเป็นต้องมีพารามิเตอร์พิเศษบางอย่าง หากต้องการคุณภาพการพิมพ์ที่ดีขึ้น โปรดดูที่วิกินี้: ASA Aero Printing Guide" -msgid "" -"This filament is too soft and not compatible with the AMS. Printing it is of " -"many requirements, and to get better printing quality, please refer to this " -"wiki: TPU printing guide." -msgstr "" -"เส้นพลาสติกนี้อ่อนเกินไปและเข้ากันไม่ได้กับ AMS การพิมพ์นั้นมีข้อกำหนดหลายประการ " -"และเพื่อให้ได้คุณภาพการพิมพ์ที่ดีขึ้น โปรดดูที่วิกินี้: คู่มือการพิมพ์ TPU" +msgid "This filament is too soft and not compatible with the AMS. Printing it is of many requirements, and to get better printing quality, please refer to this wiki: TPU printing guide." +msgstr "เส้นพลาสติกนี้อ่อนเกินไปและเข้ากันไม่ได้กับ AMS การพิมพ์นั้นมีข้อกำหนดหลายประการ และเพื่อให้ได้คุณภาพการพิมพ์ที่ดีขึ้น โปรดดูที่วิกินี้: คู่มือการพิมพ์ TPU" -msgid "" -"This filament has high enough hardness (about 67D) and is compatible with " -"the AMS. Printing it is of many requirements, and to get better printing " -"quality, please refer to this wiki: TPU printing guide." -msgstr "" -"เส้นพลาสติกนี้มีความแข็งสูงเพียงพอ (ประมาณ 67D) และเข้ากันได้กับ AMS " -"การพิมพ์นั้นมีข้อกำหนดหลายประการ และเพื่อให้ได้คุณภาพการพิมพ์ที่ดีขึ้น โปรดดูที่วิกินี้: คู่มือการพิมพ์ TPU" +msgid "This filament has high enough hardness (about 67D) and is compatible with the AMS. Printing it is of many requirements, and to get better printing quality, please refer to this wiki: TPU printing guide." +msgstr "เส้นพลาสติกนี้มีความแข็งสูงเพียงพอ (ประมาณ 67D) และเข้ากันได้กับ AMS การพิมพ์นั้นมีข้อกำหนดหลายประการ และเพื่อให้ได้คุณภาพการพิมพ์ที่ดีขึ้น โปรดดูที่วิกินี้: คู่มือการพิมพ์ TPU" -msgid "" -"If you are to print a kind of soft TPU, please don't slice with this " -"profile, and it is only for TPU that has high enough hardness (not less than " -"55D) and is compatible with the AMS. To get better printing quality, please " -"refer to this wiki: TPU printing guide." -msgstr "" -"หากคุณต้องการพิมพ์ TPU แบบอ่อน โปรดอย่าเฉือนด้วยโปรไฟล์นี้ และสำหรับ TPU " -"เท่านั้นที่มีความแข็งสูงเพียงพอ (ไม่น้อยกว่า 55D) และเข้ากันได้กับ AMS " -"หากต้องการคุณภาพการพิมพ์ที่ดีขึ้น โปรดดูที่วิกินี้: คู่มือการพิมพ์ TPU" +msgid "If you are to print a kind of soft TPU, please don't slice with this profile, and it is only for TPU that has high enough hardness (not less than 55D) and is compatible with the AMS. To get better printing quality, please refer to this wiki: TPU printing guide." +msgstr "หากคุณต้องการพิมพ์ TPU แบบอ่อน โปรดอย่าเฉือนด้วยโปรไฟล์นี้ และสำหรับ TPU เท่านั้นที่มีความแข็งสูงเพียงพอ (ไม่น้อยกว่า 55D) และเข้ากันได้กับ AMS หากต้องการคุณภาพการพิมพ์ที่ดีขึ้น โปรดดูที่วิกินี้: คู่มือการพิมพ์ TPU" -msgid "" -"This is a water-soluble support filament, and usually it is only for the " -"support structure and not for the model body. Printing this filament is of " -"many requirements, and to get better printing quality, please refer to this " -"wiki: PVA Printing Guide." -msgstr "" -"นี่คือเส้นพลาสติกรองรับที่ละลายน้ำได้ และโดยปกติจะใช้สำหรับโครงสร้างรองรับเท่านั้น " -"ไม่ใช่สำหรับตัวโมเดล การพิมพ์เส้นพลาสติกนี้มีข้อกำหนดหลายประการ " -"และเพื่อให้ได้คุณภาพการพิมพ์ที่ดีขึ้น โปรดดูที่วิกินี้: คู่มือการพิมพ์ PVA" +msgid "This is a water-soluble support filament, and usually it is only for the support structure and not for the model body. Printing this filament is of many requirements, and to get better printing quality, please refer to this wiki: PVA Printing Guide." +msgstr "นี่คือเส้นพลาสติกรองรับที่ละลายน้ำได้ และโดยปกติจะใช้สำหรับโครงสร้างรองรับเท่านั้น ไม่ใช่สำหรับตัวโมเดล การพิมพ์เส้นพลาสติกนี้มีข้อกำหนดหลายประการ และเพื่อให้ได้คุณภาพการพิมพ์ที่ดีขึ้น โปรดดูที่วิกินี้: คู่มือการพิมพ์ PVA" -msgid "" -"This is a non-water-soluble support filament, and usually it is only for the " -"support structure and not for the model body. To get better printing " -"quality, please refer to this wiki: Printing Tips for Support Filament and " -"Support Function." -msgstr "" -"นี่คือเส้นพลาสติกรองรับที่ไม่ละลายน้ำ และโดยปกติจะใช้สำหรับโครงสร้างรองรับเท่านั้น " -"ไม่ใช่สำหรับตัวโมเดล หากต้องการคุณภาพการพิมพ์ที่ดีขึ้น โปรดดูที่วิกินี้: " -"เคล็ดลับการพิมพ์สำหรับเส้นพลาสติกรองรับและฟังก์ชันรองรับ" +msgid "This is a non-water-soluble support filament, and usually it is only for the support structure and not for the model body. To get better printing quality, please refer to this wiki: Printing Tips for Support Filament and Support Function." +msgstr "นี่คือเส้นพลาสติกรองรับที่ไม่ละลายน้ำ และโดยปกติจะใช้สำหรับโครงสร้างรองรับเท่านั้น ไม่ใช่สำหรับตัวโมเดล หากต้องการคุณภาพการพิมพ์ที่ดีขึ้น โปรดดูที่วิกินี้: เคล็ดลับการพิมพ์สำหรับเส้นพลาสติกรองรับและฟังก์ชันรองรับ" -msgid "" -"The generic presets are conservatively tuned for compatibility with a wider " -"range of filaments. For higher printing quality and speeds, please use Bambu " -"filaments with Bambu presets." -msgstr "" -"ค่าที่ตั้งไว้ล่วงหน้าทั่วไปได้รับการปรับแต่งอย่างระมัดระวังเพื่อให้เข้ากันได้กับเส้นพลาสติกที่หลากหลายมากขึ้น " -"เพื่อคุณภาพการพิมพ์และความเร็วที่สูงขึ้น โปรดใช้เส้นพลาสติก Bambu กับการตั้งค่าล่วงหน้าของ Bambu" +msgid "The generic presets are conservatively tuned for compatibility with a wider range of filaments. For higher printing quality and speeds, please use Bambu filaments with Bambu presets." +msgstr "ค่าที่ตั้งไว้ล่วงหน้าทั่วไปได้รับการปรับแต่งอย่างระมัดระวังเพื่อให้เข้ากันได้กับเส้นพลาสติกที่หลากหลายมากขึ้น เพื่อคุณภาพการพิมพ์และความเร็วที่สูงขึ้น โปรดใช้เส้นพลาสติก Bambu กับการตั้งค่าล่วงหน้าของ Bambu" msgid "High quality profile for 0.2mm nozzle, prioritizing print quality." msgstr "โปรไฟล์คุณภาพสูงสำหรับหัวฉีดขนาด 0.2 มม. โดยให้ความสำคัญกับคุณภาพการพิมพ์" -msgid "" -"High quality profile for 0.16mm layer height, prioritizing print quality and " -"strength." -msgstr "" -"โปรไฟล์คุณภาพสูงสำหรับความสูงของชั้น 0.16 มม. ให้ความสำคัญกับคุณภาพและความแข็งแรงในการพิมพ์" +msgid "High quality profile for 0.16mm layer height, prioritizing print quality and strength." +msgstr "โปรไฟล์คุณภาพสูงสำหรับความสูงของชั้น 0.16 มม. ให้ความสำคัญกับคุณภาพและความแข็งแรงในการพิมพ์" msgid "Standard profile for 0.16mm layer height, prioritizing speed." msgstr "โปรไฟล์มาตรฐานสำหรับความสูงของชั้น 0.16 มม. โดยให้ความสำคัญกับความเร็ว" -msgid "" -"High quality profile for 0.2mm layer height, prioritizing strength and print " -"quality." -msgstr "" -"โปรไฟล์คุณภาพสูงสำหรับความสูงของชั้น 0.2 มม. " -"โดยให้ความสำคัญกับความแข็งแกร่งและคุณภาพการพิมพ์" +msgid "High quality profile for 0.2mm layer height, prioritizing strength and print quality." +msgstr "โปรไฟล์คุณภาพสูงสำหรับความสูงของชั้น 0.2 มม. โดยให้ความสำคัญกับความแข็งแกร่งและคุณภาพการพิมพ์" msgid "Standard profile for 0.4mm nozzle, prioritizing speed." msgstr "โปรไฟล์มาตรฐานสำหรับหัวฉีดขนาด 0.4 มม. โดยให้ความสำคัญกับความเร็ว" -msgid "" -"High quality profile for 0.6mm nozzle, prioritizing print quality and " -"strength." -msgstr "" -"โปรไฟล์คุณภาพสูงสำหรับหัวฉีดขนาด 0.6 มม. ให้ความสำคัญกับคุณภาพและความแข็งแรงในการพิมพ์" +msgid "High quality profile for 0.6mm nozzle, prioritizing print quality and strength." +msgstr "โปรไฟล์คุณภาพสูงสำหรับหัวฉีดขนาด 0.6 มม. ให้ความสำคัญกับคุณภาพและความแข็งแรงในการพิมพ์" msgid "Strength profile for 0.6mm nozzle, prioritizing strength." msgstr "โปรไฟล์ความแข็งแกร่งสำหรับหัวฉีดขนาด 0.6 มม. โดยให้ความสำคัญกับความแข็งแรง" @@ -21886,8 +18159,7 @@ msgstr "สถานะอุปกรณ์" msgid "AMS Status" msgstr "สถานะ AMS" -msgid "" -"Please select the devices you would like to manage here (up to 6 devices)" +msgid "Please select the devices you would like to manage here (up to 6 devices)" msgstr "โปรดเลือกอุปกรณ์ที่คุณต้องการจัดการที่นี่ (สูงสุด 6 อุปกรณ์)" msgid "Printing Options" @@ -21902,16 +18174,13 @@ msgstr "ตัวเลือกส่ง" msgid "Send to" msgstr "ส่งไปที่" -msgid "" -"printers at the same time. (It depends on how many devices can undergo " -"heating at the same time.)" +msgid "printers at the same time. (It depends on how many devices can undergo heating at the same time.)" msgstr "เครื่องพิมพ์ในเวลาเดียวกัน (ขึ้นอยู่กับจำนวนอุปกรณ์ที่สามารถทำความร้อนได้ในเวลาเดียวกัน)" msgid "Wait" msgstr "รอ" -msgid "" -"minute each batch. (It depends on how long it takes to complete heating.)" +msgid "minute each batch. (It depends on how long it takes to complete heating.)" msgstr "นาทีในแต่ละชุด (ขึ้นอยู่กับระยะเวลาในการทำความร้อนให้เสร็จสิ้น)" msgid "Task Sending" @@ -22012,18 +18281,11 @@ msgstr "โหมดความสะดวกสบาย" msgid "Custom Mode" msgstr "โหมดกำหนดเอง" -msgid "" -"Generates filament grouping for the left and right nozzles based on the most " -"filament-saving principles to minimize waste." +msgid "Generates filament grouping for the left and right nozzles based on the most filament-saving principles to minimize waste." msgstr "สร้างการจัดกลุ่มเส้นพลาสติกสำหรับหัวฉีดด้านซ้ายและขวาตามหลักการประหยัดเส้นพลาสติกมากที่สุดเพื่อลดของเสีย" -msgid "" -"Generates filament grouping for the left and right nozzles based on the " -"printer's actual filament status, reducing the need for manual filament " -"adjustment." -msgstr "" -"สร้างการจัดกลุ่มเส้นพลาสติกสำหรับหัวฉีดด้านซ้ายและขวาตามสถานะเส้นพลาสติกที่แท้จริงของเครื่องพิมพ์ " -"ซึ่งช่วยลดความจำเป็นในการปรับเส้นพลาสติกด้วยตนเอง" +msgid "Generates filament grouping for the left and right nozzles based on the printer's actual filament status, reducing the need for manual filament adjustment." +msgstr "สร้างการจัดกลุ่มเส้นพลาสติกสำหรับหัวฉีดด้านซ้ายและขวาตามสถานะเส้นพลาสติกที่แท้จริงของเครื่องพิมพ์ ซึ่งช่วยลดความจำเป็นในการปรับเส้นพลาสติกด้วยตนเอง" msgid "Manually assign filament to the left or right nozzle" msgstr "กำหนดเส้นพลาสติกให้กับหัวฉีดด้านซ้ายหรือขวาด้วยตนเอง" @@ -22043,9 +18305,7 @@ msgstr "เราจะสไลซ์ตามวิธีการจัดก msgid "Tip: You can drag the filaments to reassign them to different nozzles." msgstr "เคล็ดลับ: คุณสามารถลากเส้นพลาสติกเพื่อกำหนดใหม่ให้กับหัวฉีดที่แตกต่างกันได้" -msgid "" -"The filament grouping method for current plate is determined by the dropdown " -"option at the slicing plate button." +msgid "The filament grouping method for current plate is determined by the dropdown option at the slicing plate button." msgstr "วิธีการจัดกลุ่มเส้นพลาสติกสำหรับเพลตปัจจุบันถูกกำหนดโดยตัวเลือกแบบเลื่อนลงที่ปุ่มแผ่นสไลซ์" msgid "Connected to Obico successfully!" @@ -22124,12 +18384,8 @@ msgstr "รหัสข้อผิดพลาด: %1%" msgid "Upload failed" msgstr "อัปโหลดล้มเหลว" -msgid "" -"The file has been transferred, but some unknown errors occurred. Please " -"check the device page for the file and try to start printing again." -msgstr "" -"ไฟล์ถูกส่งแล้ว แต่เกิดข้อผิดพลาดที่ไม่ทราบสาเหตุ " -"โปรดตรวจสอบไฟล์ในหน้าอุปกรณ์แล้วลองเริ่มพิมพ์อีกครั้ง" +msgid "The file has been transferred, but some unknown errors occurred. Please check the device page for the file and try to start printing again." +msgstr "ไฟล์ถูกส่งแล้ว แต่เกิดข้อผิดพลาดที่ไม่ทราบสาเหตุ โปรดตรวจสอบไฟล์ในหน้าอุปกรณ์แล้วลองเริ่มพิมพ์อีกครั้ง" msgid "Failed to open file for upload." msgstr "เปิดไฟล์สำหรับอัปโหลดไม่สำเร็จ" @@ -22149,9 +18405,7 @@ msgstr "คำนวณ checksum ของไฟล์ไม่สำเร็ msgid "Error code not found" msgstr "ไม่พบรหัสข้อผิดพลาด" -msgid "" -"The printer is busy, Please check the device page for the file and try to " -"start printing again." +msgid "The printer is busy, Please check the device page for the file and try to start printing again." msgstr "เครื่องพิมพ์ไม่ว่าง โปรดตรวจสอบไฟล์ในหน้าอุปกรณ์แล้วลองเริ่มพิมพ์อีกครั้ง" msgid "The file is lost, please check and try again." @@ -22178,19 +18432,13 @@ msgstr "เชื่อมต่อ CrealityPrint สำเร็จ!" msgid "Could not connect to CrealityPrint" msgstr "ไม่สามารถเชื่อมต่อ CrealityPrint ได้" -msgid "" -"Connection timed out. Please check if the printer and computer network are " -"functioning properly, and confirm that they are on the same network." -msgstr "" -"การเชื่อมต่อหมดเวลา กรุณาตรวจสอบว่าเครื่องพิมพ์และเครือข่ายของคอมพิวเตอร์ทำงานปกติ " -"และอยู่ในเครือข่ายเดียวกัน" +msgid "Connection timed out. Please check if the printer and computer network are functioning properly, and confirm that they are on the same network." +msgstr "การเชื่อมต่อหมดเวลา กรุณาตรวจสอบว่าเครื่องพิมพ์และเครือข่ายของคอมพิวเตอร์ทำงานปกติ และอยู่ในเครือข่ายเดียวกัน" msgid "The Hostname/IP/URL could not be parsed, please check it and try again." msgstr "ไม่สามารถแยกวิเคราะห์ Hostname/IP/URL ได้ กรุณาตรวจสอบและลองอีกครั้ง" -msgid "" -"File/data transfer interrupted. Please check the printer and network, then " -"try it again." +msgid "File/data transfer interrupted. Please check the printer and network, then try it again." msgstr "การถ่ายโอนไฟล์/ข้อมูลถูกขัดจังหวะ กรุณาตรวจสอบเครื่องพิมพ์และเครือข่าย แล้วลองอีกครั้ง" msgid "The provided state is not correct." @@ -22226,9 +18474,7 @@ msgstr "สร้างหูขอบยึดชิ้นงานนกโด msgid "Add or Select" msgstr "เพิ่มหรือเลือก" -msgid "" -"Warning: The brim type is not set to \"painted\", the brim ears will not " -"take effect!" +msgid "Warning: The brim type is not set to \"painted\", the brim ears will not take effect!" msgstr "คำเตือน: ไม่ได้ตั้งค่าประเภทขอบยึดชิ้นงานเป็น \"ทาสี\" หูขอบยึดชิ้นงานจะไม่เกิดผล!" msgid "Set the brim type of this object to \"painted\"" @@ -22311,15 +18557,11 @@ msgstr "มีการอัปเดตปลั๊กอินเครือ msgid "Bambu Network Plug-in Required" msgstr "ต้องใช้ปลั๊กอินเครือข่าย Bambu" -msgid "" -"The Bambu Network Plug-in is corrupted or incompatible. Please reinstall it." +msgid "The Bambu Network Plug-in is corrupted or incompatible. Please reinstall it." msgstr "ปลั๊กอินเครือข่าย Bambu เสียหายหรือเข้ากันไม่ได้ กรุณาติดตั้งใหม่" -msgid "" -"The Bambu Network Plug-in is required for cloud features, printer discovery, " -"and remote printing." -msgstr "" -"ปลั๊กอินเครือข่าย Bambu จำเป็นสำหรับคุณสมบัติคลาวด์ การค้นหาเครื่องพิมพ์ และการพิมพ์ระยะไกล" +msgid "The Bambu Network Plug-in is required for cloud features, printer discovery, and remote printing." +msgstr "ปลั๊กอินเครือข่าย Bambu จำเป็นสำหรับคุณสมบัติคลาวด์ การค้นหาเครื่องพิมพ์ และการพิมพ์ระยะไกล" #, c-format, boost-format msgid "Error: %s" @@ -22362,8 +18604,7 @@ msgstr "อย่าถามอีก" msgid "The Bambu Network Plug-in has been installed successfully." msgstr "ติดตั้งปลั๊กอินเครือข่าย Bambu สำเร็จแล้ว" -msgid "" -"A restart is required to load the new plug-in. Would you like to restart now?" +msgid "A restart is required to load the new plug-in. Would you like to restart now?" msgstr "จำเป็นต้องรีสตาร์ทเพื่อโหลดปลั๊กอินใหม่ คุณต้องการรีสตาร์ทตอนนี้หรือไม่?" msgid "Restart Now" @@ -22381,12 +18622,8 @@ msgstr "ความเร็วปริมาตร" msgid "Step file import parameters" msgstr "พารามิเตอร์การนำเข้าไฟล์ขั้นตอน" -msgid "" -"Smaller linear and angular deflections result in higher-quality " -"transformations but increase the processing time." -msgstr "" -"การโก่งตัวเชิงเส้นและเชิงมุมที่น้อยลงส่งผลให้มีการแปลงคุณภาพสูงขึ้น " -"แต่จะใช้เวลาในการประมวลผลเพิ่มขึ้น" +msgid "Smaller linear and angular deflections result in higher-quality transformations but increase the processing time." +msgstr "การโก่งตัวเชิงเส้นและเชิงมุมที่น้อยลงส่งผลให้มีการแปลงคุณภาพสูงขึ้น แต่จะใช้เวลาในการประมวลผลเพิ่มขึ้น" msgid "Linear Deflection" msgstr "การโก่งตัวเชิงเส้น" @@ -22448,42 +18685,25 @@ msgstr "ส่งออกชุดค่าที่ตั้งไว้ล่ msgid "Save preset bundle" msgstr "บันทึกชุดค่าที่ตั้งไว้ล่วงหน้า" -msgid "" -"Performing desktop integration failed - boost::filesystem::canonical did not " -"return appimage path." -msgstr "" -"การผสานรวมเดสก์ท็อปล้มเหลว - boost::filesystem::canonical ไม่ได้คืนค่าเส้นทาง " -"AppImage" +msgid "Performing desktop integration failed - boost::filesystem::canonical did not return appimage path." +msgstr "การผสานรวมเดสก์ท็อปล้มเหลว - boost::filesystem::canonical ไม่ได้คืนค่าเส้นทาง AppImage" msgid "Performing desktop integration failed - Could not find executable." msgstr "การผสานรวมเดสก์ท็อปล้มเหลว - ไม่พบไฟล์ที่รันได้" -msgid "" -"Performing desktop integration failed because the application directory was " -"not found." +msgid "Performing desktop integration failed because the application directory was not found." msgstr "การผสานรวมเดสก์ท็อปล้มเหลว เนื่องจากไม่พบไดเรกทอรีแอปพลิเคชัน" -msgid "" -"Performing desktop integration failed - could not create Gcodeviewer desktop " -"file. OrcaSlicer desktop file was probably created successfully." -msgstr "" -"การผสานรวมเดสก์ท็อปล้มเหลว - ไม่สามารถสร้างไฟล์เดสก์ท็อป Gcodeviewer ได้ ไฟล์เดสก์ท็อป " -"OrcaSlicer อาจถูกสร้างสำเร็จแล้ว" +msgid "Performing desktop integration failed - could not create Gcodeviewer desktop file. OrcaSlicer desktop file was probably created successfully." +msgstr "การผสานรวมเดสก์ท็อปล้มเหลว - ไม่สามารถสร้างไฟล์เดสก์ท็อป Gcodeviewer ได้ ไฟล์เดสก์ท็อป OrcaSlicer อาจถูกสร้างสำเร็จแล้ว" -msgid "" -"Performing downloader desktop integration failed - boost::filesystem::" -"canonical did not return appimage path." -msgstr "" -"การผสานรวมเดสก์ท็อปของตัวดาวน์โหลดล้มเหลว - boost::filesystem::canonical " -"ไม่ได้ส่งคืนเส้นทาง AppImage" +msgid "Performing downloader desktop integration failed - boost::filesystem::canonical did not return appimage path." +msgstr "การผสานรวมเดสก์ท็อปของตัวดาวน์โหลดล้มเหลว - boost::filesystem::canonical ไม่ได้ส่งคืนเส้นทาง AppImage" -msgid "" -"Performing downloader desktop integration failed - Could not find executable." +msgid "Performing downloader desktop integration failed - Could not find executable." msgstr "การผสานรวมเดสก์ท็อปของตัวดาวน์โหลดล้มเหลว - ไม่พบไฟล์ที่รันได้" -msgid "" -"Performing downloader desktop integration failed because the application " -"directory was not found." +msgid "Performing downloader desktop integration failed because the application directory was not found." msgstr "การผสานรวมเดสก์ท็อปของตัวดาวน์โหลดล้มเหลว เนื่องจากไม่พบไดเรกทอรีแอปพลิเคชัน" msgid "Desktop Integration" @@ -22512,21 +18732,16 @@ msgstr "ดูตัวอย่างไฟล์บีบอัด" msgid "Open File" msgstr "เปิดไฟล์" -msgid "" -"The filament may not be compatible with the current machine settings. " -"Generic filament presets will be used." +msgid "The filament may not be compatible with the current machine settings. Generic filament presets will be used." msgstr "เส้นพลาสติกอาจไม่เข้ากันกับการตั้งค่าเครื่องปัจจุบัน จะใช้การตั้งค่าฟิลาเมนต์ทั่วไป" -msgid "" -"The filament model is unknown. Still using the previous filament preset." +msgid "The filament model is unknown. Still using the previous filament preset." msgstr "ไม่ทราบแบบจำลองเส้นพลาสติก ยังคงใช้ค่าที่ตั้งไว้ล่วงหน้าของฟิลาเมนต์ก่อนหน้านี้" msgid "The filament model is unknown. Generic filament presets will be used." msgstr "ไม่ทราบแบบจำลองเส้นพลาสติก จะใช้การตั้งค่าฟิลาเมนต์ทั่วไป" -msgid "" -"The filament may not be compatible with the current machine settings. A " -"random filament preset will be used." +msgid "The filament may not be compatible with the current machine settings. A random filament preset will be used." msgstr "เส้นพลาสติกอาจไม่เข้ากันกับการตั้งค่าเครื่องปัจจุบัน ระบบจะใช้การตั้งค่าฟิลาเมนต์แบบสุ่ม" msgid "The filament model is unknown. A random filament preset will be used." @@ -22535,8 +18750,7 @@ msgstr "ไม่ทราบแบบจำลองเส้นพลาสต #: resources/data/hints.ini: [hint:Precise wall] msgid "" "Precise wall\n" -"Did you know that turning on precise wall can improve precision and layer " -"consistency?" +"Did you know that turning on precise wall can improve precision and layer consistency?" msgstr "" "ผนังที่แม่นยำ\n" "คุณรู้หรือไม่ว่าการเปิดผนังที่แม่นยำสามารถปรับปรุงความแม่นยำและความสม่ำเสมอของชั้นได้" @@ -22544,14 +18758,10 @@ msgstr "" #: resources/data/hints.ini: [hint:Sandwich mode] msgid "" "Sandwich mode\n" -"Did you know that you can use sandwich mode (inner-outer-inner) to improve " -"precision and layer consistency if your model doesn't have very steep " -"overhangs?" +"Did you know that you can use sandwich mode (inner-outer-inner) to improve precision and layer consistency if your model doesn't have very steep overhangs?" msgstr "" "โหมดแซนด์วิช\n" -"คุณรู้ไหมว่าคุณสามารถใช้โหมดแซนวิช (ด้านใน-ด้านนอก-ด้านใน) " -"เพื่อปรับปรุงความแม่นยำและความสม่ำเสมอของเลเยอร์ได้ " -"หากแบบจำลองของคุณไม่มีระยะยื่นที่สูงชันมากนัก" +"คุณรู้ไหมว่าคุณสามารถใช้โหมดแซนวิช (ด้านใน-ด้านนอก-ด้านใน) เพื่อปรับปรุงความแม่นยำและความสม่ำเสมอของเลเยอร์ได้ หากแบบจำลองของคุณไม่มีระยะยื่นที่สูงชันมากนัก" #: resources/data/hints.ini: [hint:Chamber temperature] msgid "" @@ -22564,12 +18774,10 @@ msgstr "" #: resources/data/hints.ini: [hint:Calibration] msgid "" "Calibration\n" -"Did you know that calibrating your printer can do wonders? Check out our " -"beloved calibration solution in OrcaSlicer." +"Did you know that calibrating your printer can do wonders? Check out our beloved calibration solution in OrcaSlicer." msgstr "" "การสอบเทียบ\n" -"คุณรู้หรือไม่ว่าการปรับเทียบเครื่องพิมพ์ของคุณสามารถสร้างสิ่งมหัศจรรย์ได้ " -"ตรวจสอบโซลูชันการสอบเทียบที่เราชื่นชอบใน OrcaSlicer" +"คุณรู้หรือไม่ว่าการปรับเทียบเครื่องพิมพ์ของคุณสามารถสร้างสิ่งมหัศจรรย์ได้ ตรวจสอบโซลูชันการสอบเทียบที่เราชื่นชอบใน OrcaSlicer" #: resources/data/hints.ini: [hint:Auxiliary fan] msgid "" @@ -22598,18 +18806,15 @@ msgstr "" #: resources/data/hints.ini: [hint:Switch workspaces] msgid "" "Switch workspaces\n" -"You can switch between Prepare and Preview workspaces by " -"pressing the Tab key." +"You can switch between Prepare and Preview workspaces by pressing the Tab key." msgstr "" "สลับพื้นที่ทำงาน\n" -"คุณสามารถสลับระหว่างพื้นที่ทำงาน จัดเตรียม และ ดูตัวอย่าง ได้โดยการกดปุ่ม " -"Tab" +"คุณสามารถสลับระหว่างพื้นที่ทำงาน จัดเตรียม และ ดูตัวอย่าง ได้โดยการกดปุ่ม Tab" #: resources/data/hints.ini: [hint:How to use keyboard shortcuts] msgid "" "How to use keyboard shortcuts\n" -"Did you know that Orca Slicer offers a wide range of keyboard shortcuts and " -"3D scene operations?" +"Did you know that Orca Slicer offers a wide range of keyboard shortcuts and 3D scene operations?" msgstr "" "วิธีใช้แป้นพิมพ์ลัด\n" "คุณรู้หรือไม่ว่า Orca Slicer มีแป้นพิมพ์ลัดและการทำงานของฉาก 3 มิติที่หลากหลาย" @@ -22617,20 +18822,15 @@ msgstr "" #: resources/data/hints.ini: [hint:Reverse on even] msgid "" "Reverse on even\n" -"Did you know that Reverse on even feature can significantly improve " -"the surface quality of your overhangs? However, it can cause wall " -"inconsistencies so use carefully!" +"Did you know that Reverse on even feature can significantly improve the surface quality of your overhangs? However, it can cause wall inconsistencies so use carefully!" msgstr "" "ย้อนกลับในคู่\n" -"คุณรู้หรือไม่ว่าคุณลักษณะ ย้อนกลับเมื่อสม่ำเสมอ " -"สามารถปรับปรุงคุณภาพพื้นผิวของระยะยื่นของคุณได้อย่างมาก อย่างไรก็ตาม " -"อาจทำให้เกิดความไม่สอดคล้องกันของผนังได้ ดังนั้นโปรดใช้อย่างระมัดระวัง!" +"คุณรู้หรือไม่ว่าคุณลักษณะ ย้อนกลับเมื่อสม่ำเสมอ สามารถปรับปรุงคุณภาพพื้นผิวของระยะยื่นของคุณได้อย่างมาก อย่างไรก็ตาม อาจทำให้เกิดความไม่สอดคล้องกันของผนังได้ ดังนั้นโปรดใช้อย่างระมัดระวัง!" #: resources/data/hints.ini: [hint:Cut Tool] msgid "" "Cut Tool\n" -"Did you know that you can cut a model at any angle and position with the " -"cutting tool?" +"Did you know that you can cut a model at any angle and position with the cutting tool?" msgstr "" "เครื่องมือตัด\n" "คุณรู้หรือไม่ว่าคุณสามารถตัดแบบจำลองได้ทุกมุมและทุกตำแหน่งด้วยเครื่องมือตัด" @@ -22638,12 +18838,10 @@ msgstr "" #: resources/data/hints.ini: [hint:Fix Model] msgid "" "Fix Model\n" -"Did you know that you can fix a corrupted 3D model to avoid a lot of slicing " -"problems on the Windows system?" +"Did you know that you can fix a corrupted 3D model to avoid a lot of slicing problems on the Windows system?" msgstr "" "แก้ไขโมเดล\n" -"คุณรู้ไหมว่าคุณสามารถแก้ไขโมเดล 3 มิติที่เสียหายได้เพื่อหลีกเลี่ยงปัญหาการแบ่งส่วนในระบบ Windows " -"มากมาย" +"คุณรู้ไหมว่าคุณสามารถแก้ไขโมเดล 3 มิติที่เสียหายได้เพื่อหลีกเลี่ยงปัญหาการแบ่งส่วนในระบบ Windows มากมาย" #: resources/data/hints.ini: [hint:Timelapse] msgid "" @@ -22664,8 +18862,7 @@ msgstr "" #: resources/data/hints.ini: [hint:Auto-Orient] msgid "" "Auto-Orient\n" -"Did you know that you can rotate objects to an optimal orientation for " -"printing with a simple click?" +"Did you know that you can rotate objects to an optimal orientation for printing with a simple click?" msgstr "" "ปรับทิศทางอัตโนมัติ\n" "คุณรู้หรือไม่ว่าคุณสามารถหมุนวัตถุให้อยู่ในแนวที่เหมาะสมที่สุดสำหรับการพิมพ์ด้วยการคลิกเพียงครั้งเดียว" @@ -22673,19 +18870,15 @@ msgstr "" #: resources/data/hints.ini: [hint:Lay on Face] msgid "" "Lay on Face\n" -"Did you know that you can quickly orient a model so that one of its faces " -"sits on the print bed? Select the \"Place on face\" function or press the " -"F key." +"Did you know that you can quickly orient a model so that one of its faces sits on the print bed? Select the \"Place on face\" function or press the F key." msgstr "" "วางบนหน้า\n" -"คุณรู้ไหมว่าคุณสามารถปรับทิศทางแบบจำลองได้อย่างรวดเร็วเพื่อให้ผิวหน้าด้านใดด้านหนึ่งวางอยู่บนฐานพิมพ์ " -"เลือกฟังก์ชัน \"วางบนหน้า\" หรือกดปุ่ม F" +"คุณรู้ไหมว่าคุณสามารถปรับทิศทางแบบจำลองได้อย่างรวดเร็วเพื่อให้ผิวหน้าด้านใดด้านหนึ่งวางอยู่บนฐานพิมพ์ เลือกฟังก์ชัน \"วางบนหน้า\" หรือกดปุ่ม F" #: resources/data/hints.ini: [hint:Object List] msgid "" "Object List\n" -"Did you know that you can view all objects/parts in a list and change " -"settings for each object/part?" +"Did you know that you can view all objects/parts in a list and change settings for each object/part?" msgstr "" "รายการวัตถุ\n" "คุณรู้ไหมว่าคุณสามารถดูวัตถุ/ชิ้นส่วนทั้งหมดในรายการและเปลี่ยนการตั้งค่าสำหรับแต่ละวัตถุ/ชิ้นส่วนได้" @@ -22693,8 +18886,7 @@ msgstr "" #: resources/data/hints.ini: [hint:Search Functionality] msgid "" "Search Functionality\n" -"Did you know that you use the Search tool to quickly find a specific Orca " -"Slicer setting?" +"Did you know that you use the Search tool to quickly find a specific Orca Slicer setting?" msgstr "" "ฟังก์ชั่นการค้นหา\n" "คุณรู้ไหมว่าคุณใช้เครื่องมือค้นหาเพื่อค้นหาการตั้งค่า Orca Slicer ที่ต้องการอย่างรวดเร็ว" @@ -22702,18 +18894,15 @@ msgstr "" #: resources/data/hints.ini: [hint:Simplify Model] msgid "" "Simplify Model\n" -"Did you know that you can reduce the number of triangles in a mesh using the " -"Simplify mesh feature? Right-click the model and select Simplify model." +"Did you know that you can reduce the number of triangles in a mesh using the Simplify mesh feature? Right-click the model and select Simplify model." msgstr "" "ลดความซับซ้อนของโมเดล\n" -"คุณรู้ไหมว่าคุณสามารถลดจำนวนสามเหลี่ยมใน mesh ได้โดยใช้ฟีเจอร์ Simplify mesh " -"คลิกขวาที่โมเดลแล้วเลือกลดความซับซ้อนของโมเดล" +"คุณรู้ไหมว่าคุณสามารถลดจำนวนสามเหลี่ยมใน mesh ได้โดยใช้ฟีเจอร์ Simplify mesh คลิกขวาที่โมเดลแล้วเลือกลดความซับซ้อนของโมเดล" #: resources/data/hints.ini: [hint:Slicing Parameter Table] msgid "" "Slicing Parameter Table\n" -"Did you know that you can view all objects/parts on a table and change " -"settings for each object/part?" +"Did you know that you can view all objects/parts on a table and change settings for each object/part?" msgstr "" "ตารางพารามิเตอร์การแบ่งส่วน\n" "คุณรู้ไหมว่าคุณสามารถดูวัตถุ/ชิ้นส่วนทั้งหมดบนโต๊ะและเปลี่ยนการตั้งค่าสำหรับแต่ละวัตถุ/ชิ้นส่วนได้" @@ -22721,8 +18910,7 @@ msgstr "" #: resources/data/hints.ini: [hint:Split to Objects/Parts] msgid "" "Split to Objects/Parts\n" -"Did you know that you can split a big object into small ones for easy " -"colorizing or printing?" +"Did you know that you can split a big object into small ones for easy colorizing or printing?" msgstr "" "แยกเป็นวัตถุ/ชิ้นส่วน\n" "คุณรู้ไหมว่าคุณสามารถแบ่งวัตถุขนาดใหญ่ออกเป็นชิ้นเล็กเพื่อให้ระบายสีหรือพิมพ์ได้ง่าย" @@ -22730,112 +18918,82 @@ msgstr "" #: resources/data/hints.ini: [hint:Subtract a Part] msgid "" "Subtract a Part\n" -"Did you know that you can subtract one mesh from another using the Negative " -"part modifier? That way you can, for example, create easily resizable holes " -"directly in Orca Slicer." +"Did you know that you can subtract one mesh from another using the Negative part modifier? That way you can, for example, create easily resizable holes directly in Orca Slicer." msgstr "" "ลบส่วนหนึ่ง\n" -"คุณรู้ไหมว่าคุณสามารถลบหนึ่ง mesh จากอีกอันหนึ่งได้โดยใช้ Negative part modifier? ด้วยวิธีนี้ " -"คุณสามารถสร้างรูที่ปรับขนาดได้อย่างง่ายดายโดยตรงใน Orca Slicer" +"คุณรู้ไหมว่าคุณสามารถลบหนึ่ง mesh จากอีกอันหนึ่งได้โดยใช้ Negative part modifier? ด้วยวิธีนี้ คุณสามารถสร้างรูที่ปรับขนาดได้อย่างง่ายดายโดยตรงใน Orca Slicer" #: resources/data/hints.ini: [hint:STEP] msgid "" "STEP\n" -"Did you know that you can improve your print quality by slicing a STEP file " -"instead of an STL?\n" -"Orca Slicer supports slicing STEP files, providing smoother results than a " -"lower resolution STL. Give it a try!" +"Did you know that you can improve your print quality by slicing a STEP file instead of an STL?\n" +"Orca Slicer supports slicing STEP files, providing smoother results than a lower resolution STL. Give it a try!" msgstr "" "ขั้นตอน\n" "คุณรู้ไหมว่าคุณสามารถปรับปรุงคุณภาพการพิมพ์ได้โดยการแบ่งไฟล์ STEP แทนที่จะเป็น STL\n" -"Orca Slicer รองรับการแบ่งไฟล์ STEP ซึ่งให้ผลลัพธ์ที่นุ่มนวลกว่า STL ที่มีความละเอียดต่ำกว่า " -"ลองดูสิ!" +"Orca Slicer รองรับการแบ่งไฟล์ STEP ซึ่งให้ผลลัพธ์ที่นุ่มนวลกว่า STL ที่มีความละเอียดต่ำกว่า ลองดูสิ!" #: resources/data/hints.ini: [hint:Z seam location] msgid "" "Z seam location\n" -"Did you know that you can customize the location of the Z seam, and even " -"paint it on your print, to have it in a less visible location? This improves " -"the overall look of your model. Check it out!" +"Did you know that you can customize the location of the Z seam, and even paint it on your print, to have it in a less visible location? This improves the overall look of your model. Check it out!" msgstr "" "ตำแหน่งรอยตะเข็บ Z\n" -"คุณรู้ไหมว่าคุณสามารถปรับแต่งตำแหน่งของรอยตะเข็บ Z " -"และแม้แต่ทาสีบนงานพิมพ์เพื่อให้อยู่ในตำแหน่งที่มองเห็นได้น้อยลง " -"สิ่งนี้จะช่วยปรับปรุงรูปลักษณ์โดยรวมของโมเดลของคุณ ตรวจสอบออก!" +"คุณรู้ไหมว่าคุณสามารถปรับแต่งตำแหน่งของรอยตะเข็บ Z และแม้แต่ทาสีบนงานพิมพ์เพื่อให้อยู่ในตำแหน่งที่มองเห็นได้น้อยลง สิ่งนี้จะช่วยปรับปรุงรูปลักษณ์โดยรวมของโมเดลของคุณ ตรวจสอบออก!" #: resources/data/hints.ini: [hint:Fine-tuning for flow rate] msgid "" "Fine-tuning for flow rate\n" -"Did you know that flow rate can be fine-tuned for even better-looking " -"prints? Depending on the material, you can improve the overall finish of the " -"printed model by doing some fine-tuning." +"Did you know that flow rate can be fine-tuned for even better-looking prints? Depending on the material, you can improve the overall finish of the printed model by doing some fine-tuning." msgstr "" "Fine-tuning for flow rate\n" -"คุณรู้หรือไม่ว่าอัตราการไหลสามารถปรับได้อย่างละเอียดเพื่อให้งานพิมพ์ดูดีขึ้น " -"คุณสามารถปรับปรุงการตกแต่งโดยรวมของโมเดลที่พิมพ์ออกมาได้โดยการปรับแต่งแบบละเอียด " -"ทั้งนี้ขึ้นอยู่กับวัสดุ" +"คุณรู้หรือไม่ว่าอัตราการไหลสามารถปรับได้อย่างละเอียดเพื่อให้งานพิมพ์ดูดีขึ้น คุณสามารถปรับปรุงการตกแต่งโดยรวมของโมเดลที่พิมพ์ออกมาได้โดยการปรับแต่งแบบละเอียด ทั้งนี้ขึ้นอยู่กับวัสดุ" #: resources/data/hints.ini: [hint:Split your prints into plates] msgid "" "Split your prints into plates\n" -"Did you know that you can split a model that has a lot of parts into " -"individual plates ready to print? This will simplify the process of keeping " -"track of all the parts." +"Did you know that you can split a model that has a lot of parts into individual plates ready to print? This will simplify the process of keeping track of all the parts." msgstr "" "แบ่งงานพิมพ์ของคุณออกเป็นจาน\n" -"คุณรู้ไหมว่าคุณสามารถแบ่งแบบจำลองที่มีชิ้นส่วนจำนวนมากออกเป็นเพลตแต่ละอันที่พร้อมพิมพ์ได้ " -"สิ่งนี้จะทำให้กระบวนการติดตามทุกส่วนง่ายขึ้น" +"คุณรู้ไหมว่าคุณสามารถแบ่งแบบจำลองที่มีชิ้นส่วนจำนวนมากออกเป็นเพลตแต่ละอันที่พร้อมพิมพ์ได้ สิ่งนี้จะทำให้กระบวนการติดตามทุกส่วนง่ายขึ้น" #: resources/data/hints.ini: [hint:Speed up your print with Adaptive Layer #: Height] msgid "" "Speed up your print with Adaptive Layer Height\n" -"Did you know that you can print a model even faster by using the Adaptive " -"Layer Height option? Check it out!" +"Did you know that you can print a model even faster by using the Adaptive Layer Height option? Check it out!" msgstr "" "เร่งความเร็วการพิมพ์ของคุณด้วย Adaptive Layer Height\n" -"คุณรู้ไหมว่าคุณสามารถพิมพ์แบบจำลองได้เร็วยิ่งขึ้นด้วยการใช้ตัวเลือก Adaptive Layer Height " -"ตรวจสอบออก!" +"คุณรู้ไหมว่าคุณสามารถพิมพ์แบบจำลองได้เร็วยิ่งขึ้นด้วยการใช้ตัวเลือก Adaptive Layer Height ตรวจสอบออก!" #: resources/data/hints.ini: [hint:Support painting] msgid "" "Support painting\n" -"Did you know that you can paint the location of your supports? This feature " -"makes it easy to place the support material only on the sections of the " -"model that actually need it." +"Did you know that you can paint the location of your supports? This feature makes it easy to place the support material only on the sections of the model that actually need it." msgstr "" "ส่วนรองรับการวาดภาพ\n" -"คุณรู้ไหมว่าคุณสามารถทาสีตำแหน่งของส่วนรองรับของคุณได้? " -"คุณสมบัตินี้ทำให้ง่ายต่อการวางวัสดุรองรับเฉพาะส่วนของรุ่นที่ต้องการจริงๆ" +"คุณรู้ไหมว่าคุณสามารถทาสีตำแหน่งของส่วนรองรับของคุณได้? คุณสมบัตินี้ทำให้ง่ายต่อการวางวัสดุรองรับเฉพาะส่วนของรุ่นที่ต้องการจริงๆ" #: resources/data/hints.ini: [hint:Different types of supports] msgid "" "Different types of supports\n" -"Did you know that you can choose from multiple types of supports? Tree " -"supports work great for organic models while saving filament and improving " -"print speed. Check them out!" +"Did you know that you can choose from multiple types of supports? Tree supports work great for organic models while saving filament and improving print speed. Check them out!" msgstr "" "ส่วนรองรับประเภทต่าง ๆ\n" -"คุณรู้ไหมว่าคุณสามารถเลือกส่วนรองรับได้หลายประเภท? " -"ส่วนรองรับแบบต้นไม้เหมาะกับโมเดลทรงออร์แกนิก " -"ช่วยประหยัดเส้นพลาสติกและเพิ่มความเร็วในการพิมพ์ ลองดูได้เลย!" +"คุณรู้ไหมว่าคุณสามารถเลือกส่วนรองรับได้หลายประเภท? ส่วนรองรับแบบต้นไม้เหมาะกับโมเดลทรงออร์แกนิก ช่วยประหยัดเส้นพลาสติกและเพิ่มความเร็วในการพิมพ์ ลองดูได้เลย!" #: resources/data/hints.ini: [hint:Printing Silk Filament] msgid "" "Printing Silk Filament\n" -"Did you know that Silk filament needs special consideration to print " -"successfully? A higher temperature and lower speed are always recommended " -"for the best results." +"Did you know that Silk filament needs special consideration to print successfully? A higher temperature and lower speed are always recommended for the best results." msgstr "" "การพิมพ์เส้นพลาสติก Silk\n" -"คุณรู้ไหมว่าเส้นพลาสติก Silk ต้องตั้งค่าด้วยความระมัดระวังเป็นพิเศษเพื่อให้พิมพ์สำเร็จ " -"โดยทั่วไปแนะนำให้ใช้อุณหภูมิสูงขึ้นและความเร็วต่ำลงเพื่อผลลัพธ์ที่ดีที่สุด" +"คุณรู้ไหมว่าเส้นพลาสติก Silk ต้องตั้งค่าด้วยความระมัดระวังเป็นพิเศษเพื่อให้พิมพ์สำเร็จ โดยทั่วไปแนะนำให้ใช้อุณหภูมิสูงขึ้นและความเร็วต่ำลงเพื่อผลลัพธ์ที่ดีที่สุด" #: resources/data/hints.ini: [hint:Brim for better adhesion] msgid "" "Brim for better adhesion\n" -"Did you know that when printed models have a small contact interface with " -"the printing surface, it's recommended to use a brim?" +"Did you know that when printed models have a small contact interface with the printing surface, it's recommended to use a brim?" msgstr "" "ขอบยึดชิ้นงานเพื่อการยึดเกาะที่ดีขึ้น\n" "คุณรู้หรือไม่ว่าเมื่อแบบจำลองที่พิมพ์มีส่วนติดต่อขนาดเล็กกับพื้นผิวการพิมพ์ ขอแนะนำให้ใช้ขอบยึดชิ้นงาน" @@ -22843,8 +19001,7 @@ msgstr "" #: resources/data/hints.ini: [hint:Set parameters for multiple objects] msgid "" "Set parameters for multiple objects\n" -"Did you know that you can set slicing parameters for all selected objects at " -"once?" +"Did you know that you can set slicing parameters for all selected objects at once?" msgstr "" "ตั้งค่าพารามิเตอร์สำหรับวัตถุหลายรายการ\n" "คุณรู้ไหมว่าคุณสามารถตั้งค่าพารามิเตอร์การแบ่งส่วนสำหรับวัตถุที่เลือกทั้งหมดได้ในคราวเดียว" @@ -22860,18 +19017,15 @@ msgstr "" #: resources/data/hints.ini: [hint:Flush into support/objects/infill] msgid "" "Flush into support/objects/infill\n" -"Did you know that you can reduce wasted filament by flushing it into support/" -"objects/infill during filament changes?" +"Did you know that you can reduce wasted filament by flushing it into support/objects/infill during filament changes?" msgstr "" "ไล่เส้นเข้าไปในส่วนรองรับ/วัตถุ/ไส้ใน\n" -"คุณรู้ไหมว่าคุณสามารถลดเส้นพลาสติกที่เสียไปได้ โดยไล่เส้นเข้าไปในส่วนรองรับ วัตถุ " -"หรือไส้ในระหว่างเปลี่ยนเส้นพลาสติก" +"คุณรู้ไหมว่าคุณสามารถลดเส้นพลาสติกที่เสียไปได้ โดยไล่เส้นเข้าไปในส่วนรองรับ วัตถุ หรือไส้ในระหว่างเปลี่ยนเส้นพลาสติก" #: resources/data/hints.ini: [hint:Improve strength] msgid "" "Improve strength\n" -"Did you know that you can use more wall loops and higher sparse infill " -"density to improve the strength of the model?" +"Did you know that you can use more wall loops and higher sparse infill density to improve the strength of the model?" msgstr "" "ปรับปรุงความแข็งแรง\n" "คุณรู้ไหมว่าคุณสามารถใช้ลูปติดผนังได้มากขึ้นและมีความหนาแน่นของไส้ในแบบโปร่งที่สูงขึ้นเพื่อปรับปรุงความแข็งแกร่งของแบบจำลอง" @@ -22880,43 +19034,27 @@ msgstr "" #: door opened] msgid "" "When do you need to print with the printer door opened?\n" -"Did you know that opening the printer door can reduce the probability of " -"extruder/hotend clogging when printing lower temperature filament with a " -"higher enclosure temperature? There is more info about this in the Wiki." +"Did you know that opening the printer door can reduce the probability of extruder/hotend clogging when printing lower temperature filament with a higher enclosure temperature? There is more info about this in the Wiki." msgstr "" "เมื่อใดที่คุณจำเป็นต้องพิมพ์โดยเปิดประตูเครื่องพิมพ์?\n" -"คุณรู้หรือไม่ว่าการเปิดประตูเครื่องพิมพ์สามารถลดโอกาสที่ชุดดันเส้น/" -"ฮอตเอนด์จะอุดตันเมื่อพิมพ์เส้นพลาสติกที่มีอุณหภูมิต่ำลงและมีอุณหภูมิของกรอบที่สูงขึ้น " -"ข้อมูลเพิ่มเติมเกี่ยวกับเรื่องนี้ใน Wiki" +"คุณรู้หรือไม่ว่าการเปิดประตูเครื่องพิมพ์สามารถลดโอกาสที่ชุดดันเส้น/ฮอตเอนด์จะอุดตันเมื่อพิมพ์เส้นพลาสติกที่มีอุณหภูมิต่ำลงและมีอุณหภูมิของกรอบที่สูงขึ้น ข้อมูลเพิ่มเติมเกี่ยวกับเรื่องนี้ใน Wiki" #: resources/data/hints.ini: [hint:Avoid warping] msgid "" "Avoid warping\n" -"Did you know that when printing materials that are prone to warping such as " -"ABS, appropriately increasing the heatbed temperature can reduce the " -"probability of warping?" +"Did you know that when printing materials that are prone to warping such as ABS, appropriately increasing the heatbed temperature can reduce the probability of warping?" msgstr "" "หลีกเลี่ยงการบิดเบี้ยว\n" -"คุณรู้หรือไม่ว่าเมื่อพิมพ์วัสดุที่มีแนวโน้มที่จะเกิดการบิดเบี้ยว เช่น ABS " -"การเพิ่มอุณหภูมิฐานพิมพ์อย่างเหมาะสมสามารถลดความน่าจะเป็นของการบิดเบี้ยวได้" +"คุณรู้หรือไม่ว่าเมื่อพิมพ์วัสดุที่มีแนวโน้มที่จะเกิดการบิดเบี้ยว เช่น ABS การเพิ่มอุณหภูมิฐานพิมพ์อย่างเหมาะสมสามารถลดความน่าจะเป็นของการบิดเบี้ยวได้" -#~ msgid "" -#~ "This is the height of the first layer. Making the first layer height " -#~ "thicker can improve build plate adhesion." +#~ msgid "This is the height of the first layer. Making the first layer height thicker can improve build plate adhesion." #~ msgstr "ความสูงของชั้นแรก การทำให้ชั้นแรกหนาขึ้นสามารถปรับปรุงการยึดเกาะของแผ่นรองพื้นได้" #~ msgid "Enable network plug-in" #~ msgstr "เปิดใช้งานปลั๊กอินเครือข่าย" -#~ msgid "" -#~ "Enable adaptive PA for overhangs as well as when flow changes within the " -#~ "same feature. This is an experimental option, as if the PA profile is not " -#~ "set accurately, it will cause uniformity issues on the external surfaces " -#~ "before and after overhangs.\n" -#~ msgstr "" -#~ "เปิดใช้งาน PA แบบปรับได้สำหรับส่วนยื่นและเมื่อโฟลว์เปลี่ยนแปลงภายในฟีเจอร์เดียวกัน " -#~ "นี่เป็นตัวเลือกทดลอง ราวกับว่าโปรไฟล์ PA ไม่ได้ตั้งค่าอย่างถูกต้อง " -#~ "ก็จะทำให้เกิดปัญหาความสม่ำเสมอบนพื้นผิวภายนอกก่อนและหลังระยะยื่น\n" +#~ msgid "Enable adaptive PA for overhangs as well as when flow changes within the same feature. This is an experimental option, as if the PA profile is not set accurately, it will cause uniformity issues on the external surfaces before and after overhangs.\n" +#~ msgstr "เปิดใช้งาน PA แบบปรับได้สำหรับส่วนยื่นและเมื่อโฟลว์เปลี่ยนแปลงภายในฟีเจอร์เดียวกัน นี่เป็นตัวเลือกทดลอง ราวกับว่าโปรไฟล์ PA ไม่ได้ตั้งค่าอย่างถูกต้อง ก็จะทำให้เกิดปัญหาความสม่ำเสมอบนพื้นผิวภายนอกก่อนและหลังระยะยื่น\n" #~ msgid "Perform" #~ msgstr "ดำเนินการ" @@ -23025,19 +19163,11 @@ msgstr "" #~ msgid "Cloud Error" #~ msgstr "ข้อผิดพลาดคลาวด์" -#~ msgid "" -#~ "This disables all cloud services e.g. Orca Cloud and Bambu Cloud. This " -#~ "stops the transmission of data to Bambu's cloud services too. Users who " -#~ "don't use BBL machines or use LAN mode only can safely turn on this " -#~ "function." -#~ msgstr "" -#~ "การดำเนินการนี้จะปิดใช้งานบริการคลาวด์ทั้งหมด เช่น ออร์ก้าคลาวด์และแบมบูคลาวด์ " -#~ "ซึ่งจะหยุดการส่งข้อมูลไปยังบริการคลาวด์ของ Bambu ด้วยเช่นกัน ผู้ใช้ที่ไม่ได้ใช้เครื่อง BBL " -#~ "หรือใช้โหมด LAN เท่านั้นสามารถเปิดฟังก์ชันนี้ได้อย่างปลอดภัย" +#~ msgid "This disables all cloud services e.g. Orca Cloud and Bambu Cloud. This stops the transmission of data to Bambu's cloud services too. Users who don't use BBL machines or use LAN mode only can safely turn on this function." +#~ msgstr "การดำเนินการนี้จะปิดใช้งานบริการคลาวด์ทั้งหมด เช่น ออร์ก้าคลาวด์และแบมบูคลาวด์ ซึ่งจะหยุดการส่งข้อมูลไปยังบริการคลาวด์ของ Bambu ด้วยเช่นกัน ผู้ใช้ที่ไม่ได้ใช้เครื่อง BBL หรือใช้โหมด LAN เท่านั้นสามารถเปิดฟังก์ชันนี้ได้อย่างปลอดภัย" #~ msgid "" -#~ "The Wipe option is not available when using the Firmware Retraction " -#~ "mode.\n" +#~ "The Wipe option is not available when using the Firmware Retraction mode.\n" #~ "\n" #~ "Shall I disable it in order to enable Firmware Retraction?" #~ msgstr "" @@ -23045,171 +19175,93 @@ msgstr "" #~ "\n" #~ "ฉันควรปิดการใช้งานเพื่อเปิดใช้งานการถอนเฟิร์มแวร์หรือไม่" -#~ msgid "" -#~ "\"G92 E0\" was found in before_layer_gcode, which is incompatible with " -#~ "absolute extruder addressing." -#~ msgstr "" -#~ "พบ \"G92 E0\" ใน before_layer_gcode " -#~ "ซึ่งเข้ากันไม่ได้กับการระบุที่อยู่ของชุดดันเส้นแบบสัมบูรณ์" +#~ msgid "\"G92 E0\" was found in before_layer_gcode, which is incompatible with absolute extruder addressing." +#~ msgstr "พบ \"G92 E0\" ใน before_layer_gcode ซึ่งเข้ากันไม่ได้กับการระบุที่อยู่ของชุดดันเส้นแบบสัมบูรณ์" -#~ msgid "" -#~ "\"G92 E0\" was found in layer_gcode, which is incompatible with absolute " -#~ "extruder addressing." -#~ msgstr "" -#~ "พบ \"G92 E0\" ใน layer_gcode ซึ่งเข้ากันไม่ได้กับการกำหนดที่อยู่ของชุดดันเส้นแบบสัมบูรณ์" +#~ msgid "\"G92 E0\" was found in layer_gcode, which is incompatible with absolute extruder addressing." +#~ msgstr "พบ \"G92 E0\" ใน layer_gcode ซึ่งเข้ากันไม่ได้กับการกำหนดที่อยู่ของชุดดันเส้นแบบสัมบูรณ์" #, no-c-format, no-boost-format -#~ msgid "" -#~ "Bridging angle override. If left to zero, the bridging angle will be " -#~ "calculated automatically. Otherwise the provided angle will be used for " -#~ "external bridges. Use 180° for zero angle." -#~ msgstr "" -#~ "การแทนที่มุมเชื่อมโยง หากปล่อยให้เหลือศูนย์ มุมเชื่อมโยงจะถูกคำนวณโดยอัตโนมัติ " -#~ "มิฉะนั้นจะใช้มุมที่ให้ไว้สำหรับสะพานภายนอก ใช้ 180° สำหรับมุมศูนย์" +#~ msgid "Bridging angle override. If left to zero, the bridging angle will be calculated automatically. Otherwise the provided angle will be used for external bridges. Use 180° for zero angle." +#~ msgstr "การแทนที่มุมเชื่อมโยง หากปล่อยให้เหลือศูนย์ มุมเชื่อมโยงจะถูกคำนวณโดยอัตโนมัติ มิฉะนั้นจะใช้มุมที่ให้ไว้สำหรับสะพานภายนอก ใช้ 180° สำหรับมุมศูนย์" #~ msgid "" -#~ "Internal bridging angle override. If left to zero, the bridging angle " -#~ "will be calculated automatically. Otherwise the provided angle will be " -#~ "used for internal bridges. Use 180° for zero angle.\n" +#~ "Internal bridging angle override. If left to zero, the bridging angle will be calculated automatically. Otherwise the provided angle will be used for internal bridges. Use 180° for zero angle.\n" #~ "\n" -#~ "It is recommended to leave it at 0 unless there is a specific model need " -#~ "not to." +#~ "It is recommended to leave it at 0 unless there is a specific model need not to." #~ msgstr "" -#~ "การแทนที่มุมเชื่อมต่อภายใน หากปล่อยให้เหลือศูนย์ มุมเชื่อมโยงจะถูกคำนวณโดยอัตโนมัติ " -#~ "มิฉะนั้นจะใช้มุมที่ให้ไว้สำหรับสะพานภายใน ใช้ 180° สำหรับมุมศูนย์\n" +#~ "การแทนที่มุมเชื่อมต่อภายใน หากปล่อยให้เหลือศูนย์ มุมเชื่อมโยงจะถูกคำนวณโดยอัตโนมัติ มิฉะนั้นจะใช้มุมที่ให้ไว้สำหรับสะพานภายใน ใช้ 180° สำหรับมุมศูนย์\n" #~ "\n" #~ "แนะนำให้ปล่อยไว้ที่ 0 เว้นแต่ว่ามีรุ่นเฉพาะที่ไม่จำเป็นต้องทำ" #~ msgid "" -#~ "Controls the density (spacing) of external bridge lines. Default is " -#~ "100%.\n" +#~ "Controls the density (spacing) of external bridge lines. Default is 100%.\n" #~ "\n" -#~ "Lower density external bridges can help improve reliability as there is " -#~ "more space for air to circulate around the extruded bridge, improving its " -#~ "cooling speed. Minimum is 10%.\n" +#~ "Lower density external bridges can help improve reliability as there is more space for air to circulate around the extruded bridge, improving its cooling speed. Minimum is 10%.\n" #~ "\n" -#~ "Higher densities can produce smoother bridge surfaces, as overlapping " -#~ "lines provide additional support during printing. Maximum is 120%.\n" +#~ "Higher densities can produce smoother bridge surfaces, as overlapping lines provide additional support during printing. Maximum is 120%.\n" #~ "Note: Bridge density that is too high can cause warping or overextrusion." #~ msgstr "" #~ "ควบคุมความหนาแน่น (ระยะห่าง) ของเส้นสะพานภายนอก ค่าเริ่มต้นคือ 100%\n" #~ "\n" -#~ "สะพานภายนอกที่มีความหนาแน่นต่ำสามารถช่วยปรับปรุงความน่าเชื่อถือได้ " -#~ "เนื่องจากมีพื้นที่ให้อากาศไหลเวียนรอบสะพานที่อัดขึ้นรูปมากขึ้น " -#~ "ซึ่งช่วยเพิ่มความเร็วในการทำความเย็น ขั้นต่ำคือ 10%\n" +#~ "สะพานภายนอกที่มีความหนาแน่นต่ำสามารถช่วยปรับปรุงความน่าเชื่อถือได้ เนื่องจากมีพื้นที่ให้อากาศไหลเวียนรอบสะพานที่อัดขึ้นรูปมากขึ้น ซึ่งช่วยเพิ่มความเร็วในการทำความเย็น ขั้นต่ำคือ 10%\n" #~ "\n" -#~ "ความหนาแน่นที่สูงขึ้นสามารถสร้างพื้นผิวสะพานที่นุ่มนวลขึ้น " -#~ "เนื่องจากเส้นที่ทับซ้อนกันจะให้ส่วนรองรับเพิ่มไส้ในในระหว่างการพิมพ์ สูงสุดคือ 120%\n" +#~ "ความหนาแน่นที่สูงขึ้นสามารถสร้างพื้นผิวสะพานที่นุ่มนวลขึ้น เนื่องจากเส้นที่ทับซ้อนกันจะให้ส่วนรองรับเพิ่มไส้ในในระหว่างการพิมพ์ สูงสุดคือ 120%\n" #~ "หมายเหตุ: ความหนาแน่นของสะพานที่สูงเกินไปอาจทำให้เกิดการบิดงอหรืออัดขึ้นรูปมากเกินไปได้" #~ msgid "" -#~ "Controls the density (spacing) of internal bridge lines. 100% means solid " -#~ "bridge. Default is 100%.\n" +#~ "Controls the density (spacing) of internal bridge lines. 100% means solid bridge. Default is 100%.\n" #~ "\n" -#~ "Lower density internal bridges can help reduce top surface pillowing and " -#~ "improve internal bridge reliability as there is more space for air to " -#~ "circulate around the extruded bridge, improving its cooling speed.\n" +#~ "Lower density internal bridges can help reduce top surface pillowing and improve internal bridge reliability as there is more space for air to circulate around the extruded bridge, improving its cooling speed.\n" #~ "\n" -#~ "This option works particularly well when combined with the second " -#~ "internal bridge over infill option, further improving internal bridging " -#~ "structure before solid infill is extruded." +#~ "This option works particularly well when combined with the second internal bridge over infill option, further improving internal bridging structure before solid infill is extruded." #~ msgstr "" -#~ "ควบคุมความหนาแน่น (ระยะห่าง) ของเส้นสะพานภายใน 100% หมายถึงสะพานที่มั่นคง " -#~ "ค่าเริ่มต้นคือ 100%\n" +#~ "ควบคุมความหนาแน่น (ระยะห่าง) ของเส้นสะพานภายใน 100% หมายถึงสะพานที่มั่นคง ค่าเริ่มต้นคือ 100%\n" #~ "\n" -#~ "สะพานภายในที่มีความหนาแน่นต่ำสามารถช่วยลดการบุผิวด้านบน " -#~ "และปรับปรุงความน่าเชื่อถือของสะพานภายใน เนื่องจากมีพื้นที่ให้อากาศไหลเวียนรอบๆ " -#~ "สะพานที่อัดขึ้นรูปมากขึ้น ซึ่งช่วยเพิ่มความเร็วในการทำความเย็น\n" +#~ "สะพานภายในที่มีความหนาแน่นต่ำสามารถช่วยลดการบุผิวด้านบน และปรับปรุงความน่าเชื่อถือของสะพานภายใน เนื่องจากมีพื้นที่ให้อากาศไหลเวียนรอบๆ สะพานที่อัดขึ้นรูปมากขึ้น ซึ่งช่วยเพิ่มความเร็วในการทำความเย็น\n" #~ "\n" -#~ "ตัวเลือกนี้ทำงานได้ดีเป็นพิเศษเมื่อรวมกับตัวเลือกบริดจ์ภายในตัวที่สองเหนือตัวเลือกไส้ใน " -#~ "ซึ่งจะช่วยปรับปรุงโครงสร้างการเชื่อมต่อภายในเพิ่มไส้ในก่อนที่จะอัดรีดไส้ในแบบทึบ" +#~ "ตัวเลือกนี้ทำงานได้ดีเป็นพิเศษเมื่อรวมกับตัวเลือกบริดจ์ภายในตัวที่สองเหนือตัวเลือกไส้ใน ซึ่งจะช่วยปรับปรุงโครงสร้างการเชื่อมต่อภายในเพิ่มไส้ในก่อนที่จะอัดรีดไส้ในแบบทึบ" #~ msgid "" -#~ "Decrease this value slightly (for example 0.9) to reduce the amount of " -#~ "material for bridge, to improve sag.\n" +#~ "Decrease this value slightly (for example 0.9) to reduce the amount of material for bridge, to improve sag.\n" #~ "\n" -#~ "The actual bridge flow used is calculated by multiplying this value with " -#~ "the filament flow ratio, and if set, the object's flow ratio." +#~ "The actual bridge flow used is calculated by multiplying this value with the filament flow ratio, and if set, the object's flow ratio." #~ msgstr "" #~ "ลดค่านี้ลงเล็กน้อย (เช่น 0.9) เพื่อลดปริมาณวัสดุสำหรับสะพานและปรับปรุงการย้อย\n" #~ "\n" -#~ "การไหลของบริดจ์จริงที่ใช้คำนวณโดยการคูณค่านี้ด้วยอัตราส่วนการไหลของเส้นพลาสติก " -#~ "และหากตั้งค่าไว้ อัตราการไหลของวัตถุ" +#~ "การไหลของบริดจ์จริงที่ใช้คำนวณโดยการคูณค่านี้ด้วยอัตราส่วนการไหลของเส้นพลาสติก และหากตั้งค่าไว้ อัตราการไหลของวัตถุ" #~ msgid "" -#~ "This value governs the thickness of the internal bridge layer. This is " -#~ "the first layer over sparse infill. Decrease this value slightly (for " -#~ "example 0.9) to improve surface quality over sparse infill.\n" +#~ "This value governs the thickness of the internal bridge layer. This is the first layer over sparse infill. Decrease this value slightly (for example 0.9) to improve surface quality over sparse infill.\n" #~ "\n" -#~ "The actual internal bridge flow used is calculated by multiplying this " -#~ "value with the bridge flow ratio, the filament flow ratio, and if set, " -#~ "the object's flow ratio." +#~ "The actual internal bridge flow used is calculated by multiplying this value with the bridge flow ratio, the filament flow ratio, and if set, the object's flow ratio." #~ msgstr "" -#~ "ค่านี้ควบคุมความหนาของชั้นบริดจ์ภายใน นี่เป็นชั้นแรกที่อยู่เหนือไส้ในแบบโปร่ง ลดค่านี้เล็กน้อย " -#~ "(เช่น 0.9) เพื่อปรับปรุงคุณภาพพื้นผิวมากกว่าไส้ในแบบโปร่ง\n" +#~ "ค่านี้ควบคุมความหนาของชั้นบริดจ์ภายใน นี่เป็นชั้นแรกที่อยู่เหนือไส้ในแบบโปร่ง ลดค่านี้เล็กน้อย (เช่น 0.9) เพื่อปรับปรุงคุณภาพพื้นผิวมากกว่าไส้ในแบบโปร่ง\n" #~ "\n" -#~ "การไหลของบริดจ์ภายในจริงที่ใช้คำนวณโดยการคูณค่านี้ด้วยอัตราส่วนการไหลของบริดจ์ " -#~ "อัตราการไหลของเส้นพลาสติก และอัตราการไหลของวัตถุหากตั้งค่าไว้" +#~ "การไหลของบริดจ์ภายในจริงที่ใช้คำนวณโดยการคูณค่านี้ด้วยอัตราส่วนการไหลของบริดจ์ อัตราการไหลของเส้นพลาสติก และอัตราการไหลของวัตถุหากตั้งค่าไว้" #, no-c-format, no-boost-format #~ msgid "" -#~ "Enable this option to slow down printing in areas where perimeters may " -#~ "have curled upwards. For example, additional slowdown will be applied " -#~ "when printing overhangs on sharp corners like the front of the Benchy " -#~ "hull, reducing curling which compounds over multiple layers.\n" +#~ "Enable this option to slow down printing in areas where perimeters may have curled upwards. For example, additional slowdown will be applied when printing overhangs on sharp corners like the front of the Benchy hull, reducing curling which compounds over multiple layers.\n" #~ "\n" -#~ "It is generally recommended to have this option switched on unless your " -#~ "printer cooling is powerful enough or the print speed slow enough that " -#~ "perimeter curling does not happen. If printing with a high external " -#~ "perimeter speed, this parameter may introduce slight artifacts when " -#~ "slowing down due to the large variance in print speeds. If you notice " -#~ "artifacts, ensure your pressure advance is tuned correctly.\n" +#~ "It is generally recommended to have this option switched on unless your printer cooling is powerful enough or the print speed slow enough that perimeter curling does not happen. If printing with a high external perimeter speed, this parameter may introduce slight artifacts when slowing down due to the large variance in print speeds. If you notice artifacts, ensure your pressure advance is tuned correctly.\n" #~ "\n" -#~ "Note: When this option is enabled, overhang perimeters are treated like " -#~ "overhangs, meaning the overhang speed is applied even if the overhanging " -#~ "perimeter is part of a bridge. For example, when the perimeters are 100% " -#~ "overhanging, with no wall supporting them from underneath, the 100% " -#~ "overhang speed will be applied." +#~ "Note: When this option is enabled, overhang perimeters are treated like overhangs, meaning the overhang speed is applied even if the overhanging perimeter is part of a bridge. For example, when the perimeters are 100% overhanging, with no wall supporting them from underneath, the 100% overhang speed will be applied." #~ msgstr "" -#~ "เปิดใช้งานตัวเลือกนี้เพื่อทำให้การพิมพ์ช้าลงในพื้นที่ที่ขอบด้านนอกอาจโค้งงอขึ้น ตัวอย่างเช่น " -#~ "การชะลอตัวเพิ่มไส้ในจะถูกนำไปใช้เมื่อพิมพ์ส่วนยื่นบนมุมที่แหลมคม เช่น ด้านหน้าของตัวถัง Benchy " -#~ "ซึ่งช่วยลดการม้วนงอซึ่งเกิดขึ้นบนหลายชั้น\n" +#~ "เปิดใช้งานตัวเลือกนี้เพื่อทำให้การพิมพ์ช้าลงในพื้นที่ที่ขอบด้านนอกอาจโค้งงอขึ้น ตัวอย่างเช่น การชะลอตัวเพิ่มไส้ในจะถูกนำไปใช้เมื่อพิมพ์ส่วนยื่นบนมุมที่แหลมคม เช่น ด้านหน้าของตัวถัง Benchy ซึ่งช่วยลดการม้วนงอซึ่งเกิดขึ้นบนหลายชั้น\n" #~ "\n" -#~ "โดยทั่วไป แนะนำให้เปิดตัวเลือกนี้ เว้นแต่ว่าการระบายความร้อนของเครื่องพิมพ์จะแรงเพียงพอ " -#~ "หรือความเร็วในการพิมพ์ช้าพอที่จะไม่เกิดการม้วนงอของเส้นรอบวง " -#~ "หากการพิมพ์ด้วยความเร็วรอบนอกสูง " -#~ "พารามิเตอร์นี้อาจเกิดข้อผิดพลาดเล็กน้อยเมื่อลดความเร็วลงเนื่องจากความเร็วการพิมพ์แตกต่างกันมาก " -#~ "หากคุณสังเกตเห็นสิ่งผิดปกติ ตรวจสอบให้แน่ใจว่าการปรับPressure Advanceของคุณถูกต้อง\n" +#~ "โดยทั่วไป แนะนำให้เปิดตัวเลือกนี้ เว้นแต่ว่าการระบายความร้อนของเครื่องพิมพ์จะแรงเพียงพอ หรือความเร็วในการพิมพ์ช้าพอที่จะไม่เกิดการม้วนงอของเส้นรอบวง หากการพิมพ์ด้วยความเร็วรอบนอกสูง พารามิเตอร์นี้อาจเกิดข้อผิดพลาดเล็กน้อยเมื่อลดความเร็วลงเนื่องจากความเร็วการพิมพ์แตกต่างกันมาก หากคุณสังเกตเห็นสิ่งผิดปกติ ตรวจสอบให้แน่ใจว่าการปรับPressure Advanceของคุณถูกต้อง\n" #~ "\n" -#~ "หมายเหตุ: เมื่อเปิดใช้งานตัวเลือกนี้ เส้นรอบวงส่วนยื่นจะถือว่าเหมือนกับส่วนยื่น " -#~ "ซึ่งหมายความว่าความเร็วส่วนยื่นจะถูกใช้ แม้ว่าเส้นรอบวงส่วนยื่นจะเป็นส่วนหนึ่งของสะพานก็ตาม " -#~ "ตัวอย่างเช่น เมื่อเส้นรอบวงยื่นออกมา 100% โดยไม่มีผนังรองรับจากด้านล่าง " -#~ "ระบบจะใช้ความเร็วยื่น 100%" +#~ "หมายเหตุ: เมื่อเปิดใช้งานตัวเลือกนี้ เส้นรอบวงส่วนยื่นจะถือว่าเหมือนกับส่วนยื่น ซึ่งหมายความว่าความเร็วส่วนยื่นจะถูกใช้ แม้ว่าเส้นรอบวงส่วนยื่นจะเป็นส่วนหนึ่งของสะพานก็ตาม ตัวอย่างเช่น เมื่อเส้นรอบวงยื่นออกมา 100% โดยไม่มีผนังรองรับจากด้านล่าง ระบบจะใช้ความเร็วยื่น 100%" -#~ msgid "" -#~ "If enabled, bridges are more reliable, can bridge longer distances, but " -#~ "may look worse. If disabled, bridges look better but are reliable just " -#~ "for shorter bridged distances." -#~ msgstr "" -#~ "หากเปิดใช้งาน สะพานจะมีความน่าเชื่อถือมากกว่า สามารถเชื่อมสะพานในระยะทางที่ไกลกว่าได้ " -#~ "แต่อาจดูแย่กว่านั้น หากปิดใช้งาน " -#~ "สะพานจะดูดีขึ้นแต่จะเชื่อถือได้สำหรับระยะทางสะพานที่สั้นกว่าเท่านั้น" +#~ msgid "If enabled, bridges are more reliable, can bridge longer distances, but may look worse. If disabled, bridges look better but are reliable just for shorter bridged distances." +#~ msgstr "หากเปิดใช้งาน สะพานจะมีความน่าเชื่อถือมากกว่า สามารถเชื่อมสะพานในระยะทางที่ไกลกว่าได้ แต่อาจดูแย่กว่านั้น หากปิดใช้งาน สะพานจะดูดีขึ้นแต่จะเชื่อถือได้สำหรับระยะทางสะพานที่สั้นกว่าเท่านั้น" -#~ msgid "" -#~ "If enabled, thick internal bridges will be used. It's usually recommended " -#~ "to have this feature turned on. However, consider turning it off if you " -#~ "are using large nozzles." -#~ msgstr "" -#~ "หากเปิดใช้งาน บริดจ์ภายในแบบหนาจะถูกนำมาใช้ โดยปกติจะแนะนำให้เปิดฟีเจอร์นี้ อย่างไรก็ตาม " -#~ "ให้พิจารณาปิดหากคุณใช้หัวฉีดขนาดใหญ่" +#~ msgid "If enabled, thick internal bridges will be used. It's usually recommended to have this feature turned on. However, consider turning it off if you are using large nozzles." +#~ msgstr "หากเปิดใช้งาน บริดจ์ภายในแบบหนาจะถูกนำมาใช้ โดยปกติจะแนะนำให้เปิดฟีเจอร์นี้ อย่างไรก็ตาม ให้พิจารณาปิดหากคุณใช้หัวฉีดขนาดใหญ่" -#~ msgid "" -#~ "Aligns infill and surface fill directions to follow the model's " -#~ "orientation on the build plate. When enabled, fill directions rotate with " -#~ "the model to maintain optimal strength characteristics." -#~ msgstr "" -#~ "จัดทิศทางไส้ในแบบไส้ในและแบบไส้ในพื้นผิวให้สอดคล้องกับการวางแนวของแบบจำลองบนฐานรองพิมพ์ " -#~ "เมื่อเปิดใช้งาน ทิศทางไส้ในจะหมุนตามโมเดลเพื่อรักษาลักษณะความแข็งแกร่งที่เหมาะสมที่สุด" +#~ msgid "Aligns infill and surface fill directions to follow the model's orientation on the build plate. When enabled, fill directions rotate with the model to maintain optimal strength characteristics." +#~ msgstr "จัดทิศทางไส้ในแบบไส้ในและแบบไส้ในพื้นผิวให้สอดคล้องกับการวางแนวของแบบจำลองบนฐานรองพิมพ์ เมื่อเปิดใช้งาน ทิศทางไส้ในจะหมุนตามโมเดลเพื่อรักษาลักษณะความแข็งแกร่งที่เหมาะสมที่สุด" #~ msgid "Filament to print internal sparse infill." #~ msgstr "เส้นพลาสติกสำหรับพิมพ์ ไส้ใน เบาบางภายใน" @@ -23223,16 +19275,8 @@ msgstr "" #~ msgid "Filament to print solid infill." #~ msgstr "ฟิลาเมนต์สำหรับพิมพ์ไส้แข็ง" -#~ msgid "" -#~ "Filament to print support base and raft. \"Default\" means no specific " -#~ "filament for support and current filament is used." -#~ msgstr "" -#~ "เส้นพลาสติกพิมพ์ฐานรองรับและฐานรองชิ้นงาน \"ค่าเริ่มต้น\" " -#~ "หมายถึงไม่มีการใช้เส้นพลาสติกเฉพาะเพื่อรองรับและใช้เส้นพลาสติกปัจจุบัน" +#~ msgid "Filament to print support base and raft. \"Default\" means no specific filament for support and current filament is used." +#~ msgstr "เส้นพลาสติกพิมพ์ฐานรองรับและฐานรองชิ้นงาน \"ค่าเริ่มต้น\" หมายถึงไม่มีการใช้เส้นพลาสติกเฉพาะเพื่อรองรับและใช้เส้นพลาสติกปัจจุบัน" -#~ msgid "" -#~ "Filament to print support interface. \"Default\" means no specific " -#~ "filament for support interface and current filament is used." -#~ msgstr "" -#~ "เส้นพลาสติกสำหรับพิมพ์ส่วนต่อประสานส่วนรองรับ \"ค่าเริ่มต้น\" " -#~ "หมายความว่าไม่มีการใช้ฟิลาเมนต์เฉพาะสำหรับอินเทอร์เฟซรองรับและมีการใช้ฟิลาเมนต์ปัจจุบัน" +#~ msgid "Filament to print support interface. \"Default\" means no specific filament for support interface and current filament is used." +#~ msgstr "เส้นพลาสติกสำหรับพิมพ์ส่วนต่อประสานส่วนรองรับ \"ค่าเริ่มต้น\" หมายความว่าไม่มีการใช้ฟิลาเมนต์เฉพาะสำหรับอินเทอร์เฟซรองรับและมีการใช้ฟิลาเมนต์ปัจจุบัน" diff --git a/localization/i18n/tr/OrcaSlicer_tr.po b/localization/i18n/tr/OrcaSlicer_tr.po index 13c5456a49..a20151e359 100644 --- a/localization/i18n/tr/OrcaSlicer_tr.po +++ b/localization/i18n/tr/OrcaSlicer_tr.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-19 10:33-0300\n" +"POT-Creation-Date: 2026-06-22 20:13-0300\n" "PO-Revision-Date: 2026-04-08 23:59+0300\n" "Last-Translator: GlauTech\n" "Language-Team: \n" @@ -4063,6 +4063,9 @@ msgstr "" msgid "The recommended nozzle temperature for this filament type is [%d, %d] degrees Celsius." msgstr "Bu filament tipinin tavsiye edilen Nozul sıcaklığı [%d, %d] derece santigrattır." +msgid "Adaptive Pressure Advance model validation failed:\n" +msgstr "" + # TODO: Review, changed by lang refactor. PR 14254 msgid "" "Too small max volumetric speed.\n" @@ -11160,6 +11163,9 @@ msgstr "" msgid "The precise wall option will be ignored for outer-inner or inner-outer-inner wall sequences." msgstr "Dış–iç veya iç–dış–iç duvar sıralamaları için 'Hassas duvar' seçeneği yok sayılacaktır." +msgid "The Adaptive Pressure Advance model for one or more extruders may contain invalid values." +msgstr "" + msgid "Filament shrinkage will not be used because filament shrinkage for the used filaments does not match." msgstr "Filament büzülmesi kullanılmayacaktır çünkü kullanılan filamentlerin filament büzülmesi önemli ölçüde farklılık göstermektedir." diff --git a/localization/i18n/uk/OrcaSlicer_uk.po b/localization/i18n/uk/OrcaSlicer_uk.po index 89856c9fee..7a758bc090 100644 --- a/localization/i18n/uk/OrcaSlicer_uk.po +++ b/localization/i18n/uk/OrcaSlicer_uk.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: orcaslicerua\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-19 10:33-0300\n" +"POT-Creation-Date: 2026-06-22 20:13-0300\n" "PO-Revision-Date: 2025-03-07 09:30+0200\n" "Last-Translator: \n" "Language-Team: Ukrainian\n" @@ -4071,6 +4071,9 @@ msgstr "" msgid "The recommended nozzle temperature for this filament type is [%d, %d] degrees Celsius." msgstr "Рекомендована температура сопла для цього типу нитки становить [%d, %d] градусів Цельсія" +msgid "Adaptive Pressure Advance model validation failed:\n" +msgstr "" + # TODO: Review, changed by lang refactor. PR 14254 msgid "" "Too small max volumetric speed.\n" @@ -11165,6 +11168,9 @@ msgstr "" msgid "The precise wall option will be ignored for outer-inner or inner-outer-inner wall sequences." msgstr "" +msgid "The Adaptive Pressure Advance model for one or more extruders may contain invalid values." +msgstr "" + msgid "Filament shrinkage will not be used because filament shrinkage for the used filaments does not match." msgstr "" diff --git a/localization/i18n/vi/OrcaSlicer_vi.po b/localization/i18n/vi/OrcaSlicer_vi.po index 5d66819cf1..f3a6cb6212 100644 --- a/localization/i18n/vi/OrcaSlicer_vi.po +++ b/localization/i18n/vi/OrcaSlicer_vi.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-19 10:33-0300\n" +"POT-Creation-Date: 2026-06-22 20:13-0300\n" "PO-Revision-Date: 2025-10-02 17:43+0700\n" "Last-Translator: \n" "Language-Team: hainguyen.ts13@gmail.com\n" @@ -4036,6 +4036,9 @@ msgstr "" msgid "The recommended nozzle temperature for this filament type is [%d, %d] degrees Celsius." msgstr "Nhiệt độ vòi phun khuyến nghị cho loại filament này là [%d, %d] độ C." +msgid "Adaptive Pressure Advance model validation failed:\n" +msgstr "" + # TODO: Review, changed by lang refactor. PR 14254 msgid "" "Too small max volumetric speed.\n" @@ -11083,6 +11086,9 @@ msgstr "" msgid "The precise wall option will be ignored for outer-inner or inner-outer-inner wall sequences." msgstr "Tùy chọn thành chính xác sẽ bị bỏ qua cho trình tự thành ngoài-trong hoặc trong-ngoài-trong." +msgid "The Adaptive Pressure Advance model for one or more extruders may contain invalid values." +msgstr "" + msgid "Filament shrinkage will not be used because filament shrinkage for the used filaments does not match." msgstr "Co ngót filament sẽ không được sử dụng vì co ngót filament của các filament đã dùng khác nhau đáng kể." diff --git a/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po b/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po index 4190ee1ae3..34e44eb505 100644 --- a/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po +++ b/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Slic3rPE\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-19 10:33-0300\n" +"POT-Creation-Date: 2026-06-22 20:13-0300\n" "PO-Revision-Date: 2026-06-11 12:37-0300\n" "Last-Translator: Handle \n" "Language-Team: \n" @@ -4059,6 +4059,9 @@ msgstr "" msgid "The recommended nozzle temperature for this filament type is [%d, %d] degrees Celsius." msgstr "该耗材的推荐喷嘴温度是[%d, %d]摄氏度" +msgid "Adaptive Pressure Advance model validation failed:\n" +msgstr "" + # TODO: Review, changed by lang refactor. PR 14254 msgid "" "Too small max volumetric speed.\n" @@ -11170,6 +11173,9 @@ msgstr "" msgid "The precise wall option will be ignored for outer-inner or inner-outer-inner wall sequences." msgstr "当壁序列为外内或内外内时,精确墙壁选项将被忽略。" +msgid "The Adaptive Pressure Advance model for one or more extruders may contain invalid values." +msgstr "" + msgid "Filament shrinkage will not be used because filament shrinkage for the used filaments does not match." msgstr "不会使用耗材收缩率,因为所用耗材的耗材收缩率不匹配。" diff --git a/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po b/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po index e6611da9e4..16b323b5f5 100644 --- a/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po +++ b/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-19 10:33-0300\n" +"POT-Creation-Date: 2026-06-22 20:13-0300\n" "PO-Revision-Date: 2025-11-28 13:48-0600\n" "Last-Translator: tntchn <15895303+tntchn@users.noreply.github.com>\n" "Language-Team: \n" @@ -4077,6 +4077,9 @@ msgstr "" msgid "The recommended nozzle temperature for this filament type is [%d, %d] degrees Celsius." msgstr "該線材的推薦噴嘴溫度是攝氏 [%d, %d] 度" +msgid "Adaptive Pressure Advance model validation failed:\n" +msgstr "" + # TODO: Review, changed by lang refactor. PR 14254 msgid "" "Too small max volumetric speed.\n" @@ -11231,6 +11234,9 @@ msgstr "" msgid "The precise wall option will be ignored for outer-inner or inner-outer-inner wall sequences." msgstr "當壁序列為外內或內外內時,精確牆壁選項將被忽略。" +msgid "The Adaptive Pressure Advance model for one or more extruders may contain invalid values." +msgstr "" + msgid "Filament shrinkage will not be used because filament shrinkage for the used filaments does not match." msgstr "線材收縮補償將被停用,因為所使用的線材之間的收縮率差異過大。" From ddbee84e683d94acf7e7e077c0f773973e647ceb Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Tue, 23 Jun 2026 00:14:17 -0500 Subject: [PATCH 060/224] render the G-code preview upright (designed view) + toggle UI --- src/slic3r/GUI/GCodeViewer.cpp | 78 +++++++++++++++++++++++++--------- src/slic3r/GUI/GCodeViewer.hpp | 2 - src/slic3r/GUI/GLCanvas3D.cpp | 19 +++++++++ src/slic3r/GUI/GUI_Preview.cpp | 11 +++++ src/slic3r/GUI/GUI_Preview.hpp | 2 + src/slic3r/GUI/Plater.cpp | 31 ++++---------- src/slic3r/GUI/Plater.hpp | 4 ++ 7 files changed, 104 insertions(+), 43 deletions(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 1263e3fed6..9d9090480f 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1140,6 +1140,14 @@ static Transform3d compute_belt_back_transform(const PrintConfig& cfg) const Transform3d mf_inv = mft.is_active() ? Transform3d(mft.transform().inverse()) : Transform3d::Identity(); + // Forward G-code axis remap as an affine matrix (out_i = sign * in[r_i % 3], plus a + // 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.) 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) { @@ -1172,6 +1180,14 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const { m_loaded_as_preview = false; + // Belt printers: drive the designed/raw view UI (legend checkbox, hotkey B, canvas-toolbar + // menu item) from the loaded print here. Plater::set_bed_shape also calls set_belt_printer(), + // but only on bed-shape changes — not reliably on every slice/preview load — so the UI was + // staying hidden even though the (config-driven) designed view rendered. The tilt magnitude + // comes from the G-code header (gcode_result.belt_tilt_angle, abs of the slicing rotation). + m_belt_view_enabled = print.config().belt_printer.value; + m_belt_angle_deg = gcode_result.belt_tilt_angle; + const bool current_top_layer_only = m_viewer.is_top_layer_only_view_range(); const bool required_top_layer_only = get_app_config()->get_bool("seq_top_layer_only"); if (current_top_layer_only != required_top_layer_only) @@ -1260,23 +1276,36 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const // inverse to well outside the model body; if one becomes the bbox minimum it // drags the min-corner anchor by ~20mm. Drop moves that land clearly outside the // (correct) model bbox — a geometric filter, not a role guess. + // Tolerance around the model bbox for outlier rejection, and the minimum fraction + // of MOVES (by count) the clip must retain before we trust it over the full bbox. + // Count, not bbox extent: a single far-flung outlier move (e.g. a belt-entry purge + // that maps to y~0) inflates the full bbox so much that a size-fraction test would + // wrongly reject the clip and re-include the outlier, dragging the min-corner anchor + // by ~the offset. By count the clip keeps ~100% of moves here and ~0% only when the + // object is genuinely offset from the belt entry (the real fall-back-to-full case). + constexpr double ANCHOR_CLIP_MARGIN_MM = 10.0; + constexpr double ANCHOR_CLIP_MIN_KEEP = 0.5; BoundingBoxf3 tp_bb_clip, tp_bb_full; - const double y_lo = model_bb.defined ? model_bb.min.y() - 10.0 : -1e30; - const double y_hi = model_bb.defined ? model_bb.max.y() + 10.0 : 1e30; + size_t n_filtered = 0, n_clip = 0; + const double y_lo = model_bb.defined ? model_bb.min.y() - ANCHOR_CLIP_MARGIN_MM : -1e30; + const double y_hi = model_bb.defined ? model_bb.max.y() + ANCHOR_CLIP_MARGIN_MM : 1e30; for (const GCodeProcessorResult::MoveVertex& mv : gcode_result.moves) if (mv.type == EMoveType::Extrude && mv.layer_id >= 1) { // skip layer-0 prime/skirt + ++n_filtered; const Vec3d p = belt_inv * mv.position.cast(); tp_bb_full.merge(p); - if (p.y() >= y_lo && p.y() <= y_hi) + if (p.y() >= y_lo && p.y() <= y_hi) { tp_bb_clip.merge(p); + ++n_clip; + } } - // Use the clipped bbox only when it still holds the bulk of the body (outliers - // removed). If the object sits far from the belt entry the toolpaths are grossly - // offset and the clip would drop most of them — fall back to the full bbox so the - // min-corner anchor still recovers that gross translation rather than breaking. + // Use the clipped bbox when it still holds the bulk of the moves (outliers removed). + // If the object sits far from the belt entry the toolpaths are grossly offset and the + // clip drops most of them — fall back to the full bbox so the min-corner anchor still + // recovers that gross translation rather than breaking. const BoundingBoxf3& tp_bb = - (tp_bb_clip.defined && tp_bb_full.defined && - tp_bb_clip.size().y() >= 0.5 * tp_bb_full.size().y()) ? tp_bb_clip : tp_bb_full; + (tp_bb_clip.defined && n_filtered > 0 && + double(n_clip) >= ANCHOR_CLIP_MIN_KEEP * double(n_filtered)) ? tp_bb_clip : tp_bb_full; if (model_bb.defined && tp_bb.defined) { // Anchor the back-transformed toolpath body onto the upright model bbox by // its MIN corner. (Center anchoring was tried and regressed when the toolpath @@ -1287,9 +1316,8 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const // translation, which this min-corner step recovers. const Vec3d d = model_bb.min - tp_bb.min; belt_inv = Transform3d(Eigen::Translation3d(d)) * belt_inv; - BOOST_LOG_TRIVIAL(debug) << "[BELT-PREVIEW] z_origin=" << gcode_result.belt_z_origin - << " model_bb.y=[" << model_bb.min.y() << "," << model_bb.max.y() - << "] tp_bb.y=[" << tp_bb.min.y() << "," << tp_bb.max.y() << "] d.y=" << d.y(); + BOOST_LOG_TRIVIAL(debug) << "[BELT-PREVIEW] anchor d=[" << d.x() << "," << d.y() << "," << d.z() + << "] (clip kept " << n_clip << "/" << n_filtered << " moves)"; } } libvgcode::GCodeInputData data = libvgcode::convert(gcode_result, str_tool_colors, str_color_print_colors, m_viewer, @@ -2423,12 +2451,12 @@ void GCodeViewer::render_toolpaths() { const Camera& camera = wxGetApp().plater()->get_camera(); Matrix4f view = camera.get_view_matrix().matrix().cast(); - // Belt "designed" (upright) view is now produced by back-transforming the - // toolpath GEOMETRY into model space at load time (see load_as_gcode -> - // libvgcode::convert with m_belt_inverse_transform). The camera is therefore - // left untouched here; transforming the view as well would double-apply the - // inverse. Keeping m_belt_inverse_transform on the geometry (not the camera) - // also keeps the bed and toolpaths in the same frame so they stay aligned. + // Belt "designed" (upright) view is produced by back-transforming the toolpath + // GEOMETRY into model space at load time (see load_as_gcode -> + // compute_belt_back_transform -> libvgcode::convert). The camera is left untouched + // here; transforming the view as well would double-apply the inverse. Baking the + // transform into the geometry (not the camera) also keeps the bed and toolpaths in + // the same frame so they stay aligned. const libvgcode::Mat4x4 converted_view_matrix = libvgcode::convert(view); const libvgcode::Mat4x4 converted_projetion_matrix = libvgcode::convert(static_cast(camera.get_projection_matrix().matrix().cast())); #if VGCODE_ENABLE_COG_AND_TOOL_MARKERS @@ -4822,7 +4850,19 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv ImGui::TextColored(ImVec4(0.f, 0.59f, 0.53f, 1.f), "%s", _u8L("Belt Printer").c_str()); ImGui::Dummy({ window_padding, 0 }); ImGui::SameLine(); - ImGui::Checkbox(_u8L("Show designed view (upright) [B]").c_str(), &m_belt_show_designed); + // 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; + if (ImGui::Checkbox(_u8L("Show raw G-code (belt only) [B]").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; diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index d5173b02e5..a0dc4ab3f7 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -247,7 +247,6 @@ mutable bool m_no_render_path { 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. - Transform3d m_belt_inverse_transform{Transform3d::Identity()}; libvgcode::Viewer m_viewer; bool m_loaded_as_preview{ false }; @@ -349,7 +348,6 @@ public: void export_toolpaths_to_obj(const char* filename) const; void set_belt_printer(bool enabled, float angle_deg) { m_belt_view_enabled = enabled; m_belt_angle_deg = angle_deg; } - void set_belt_inverse_transform(const Transform3d& t) { m_belt_inverse_transform = t; } 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; } diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 5df82877d1..e948860345 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -3512,6 +3512,10 @@ void GLCanvas3D::on_char(wxKeyEvent& evt) if (dynamic_cast(m_canvas->GetParent()) != nullptr && m_gcode_viewer.is_belt_view()) { m_gcode_viewer.toggle_belt_show_designed(); + // The designed-view back-transform is baked into the toolpath geometry at load + // time, so the toggle only takes visual effect once the preview is re-converted. + if (Plater* plater = wxGetApp().plater()) + plater->refresh_belt_view(); m_dirty = true; } break; @@ -9190,6 +9194,21 @@ 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 hotkey B 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 wxGetApp().show_3d_navigator(), diff --git a/src/slic3r/GUI/GUI_Preview.cpp b/src/slic3r/GUI/GUI_Preview.cpp index ea3ae5d21d..b56613e165 100644 --- a/src/slic3r/GUI/GUI_Preview.cpp +++ b/src/slic3r/GUI/GUI_Preview.cpp @@ -351,6 +351,17 @@ 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, + // and belt printers are exempt from the same-result-id load cache 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) { diff --git a/src/slic3r/GUI/GUI_Preview.hpp b/src/slic3r/GUI/GUI_Preview.hpp index 5469e32b71..d8c137574b 100644 --- a/src/slic3r/GUI/GUI_Preview.hpp +++ b/src/slic3r/GUI/GUI_Preview.hpp @@ -141,6 +141,8 @@ public: //BBS: add only gcode mode 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(); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 061e12791e..c3b38a5e0e 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -11381,31 +11381,13 @@ void Plater::priv::set_bed_shape(const Pointfs &shape, bed.set_belt_printer(true, static_cast(belt_angle), tilt_axis); if (preview) preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(true, static_cast(belt_angle)); - - // Compute the inverse of the mesh-side belt transform for the G-code - // viewer. The sole mesh transform is the slicing rotation; build its - // matrix from config and hand the viewer its inverse (rotation is - // orthogonal, so the inverse is the transpose). - Transform3d forward = Transform3d::Identity(); - if (rot_axis != BeltRotationAxis::None && std::abs(rot_angle) > EPSILON) { - Vec3d unit_axis = Vec3d::UnitX(); - switch (rot_axis) { - case BeltRotationAxis::X: unit_axis = Vec3d::UnitX(); break; - case BeltRotationAxis::Y: unit_axis = Vec3d::UnitY(); break; - case BeltRotationAxis::Z: unit_axis = Vec3d::UnitZ(); break; - default: break; - } - forward.linear() = Eigen::AngleAxisd(Geometry::deg2rad(rot_angle), unit_axis).toRotationMatrix(); - } - Transform3d inverse = forward.inverse(); - if (preview) - preview->get_canvas3d()->get_gcode_viewer().set_belt_inverse_transform(inverse); + // The belt "designed view" back-transform is rebuilt from the print config at + // G-code load time (GCodeViewer::compute_belt_back_transform), so no mesh-side + // inverse needs to be pushed to the viewer here. } else { bed.set_belt_printer(false, 0.f); - if (preview) { + if (preview) preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(false, 0.f); - preview->get_canvas3d()->get_gcode_viewer().set_belt_inverse_transform(Transform3d::Identity()); - } } } @@ -14166,6 +14148,11 @@ void Plater::reload_print() p->preview->reload_print(); } +void Plater::refresh_belt_view() +{ + p->preview->refresh_belt_view(); +} + // BBS wxString Plater::get_project_name() { diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index c947630794..45ff9f8458 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -329,6 +329,10 @@ public: void load_gcode(const wxString& filename); void reload_gcode_from_disk(); void reload_print(); + // 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); From 2ca843a38e06001eb7e3686283c34b8da6ccae24 Mon Sep 17 00:00:00 2001 From: Joseph Robertson <32232925+HarrierPigeon@users.noreply.github.com> Date: Wed, 24 Jun 2026 22:01:29 -0500 Subject: [PATCH 061/224] Belt Printing: Bugfix: Solid Organic Tree Base, Slim Tree Skirt, Renderer (#14395) * fix tree support brim * treesupport3d part 1: more diagnostic logging. (todo once things are fixed: remove this / gate it properly) * make area under Z=0 in rotated slice pipeline not solid * fix solid Z=0 layer for belt printers * fix renderer * clean up logging * final review pass --- src/libslic3r/Support/SupportCommon.cpp | 9 +++- src/libslic3r/Support/TreeModelVolumes.cpp | 16 +++++--- src/libslic3r/Support/TreeSupport.cpp | 48 ++++++++++++++++------ src/libslic3r/Support/TreeSupport3D.cpp | 36 ++++++++++++++-- src/slic3r/GUI/GCodeViewer.cpp | 43 +++++++++++++++---- 5 files changed, 121 insertions(+), 31 deletions(-) diff --git a/src/libslic3r/Support/SupportCommon.cpp b/src/libslic3r/Support/SupportCommon.cpp index eaf5159d9c..ed3a8fc01f 100644 --- a/src/libslic3r/Support/SupportCommon.cpp +++ b/src/libslic3r/Support/SupportCommon.cpp @@ -1775,7 +1775,14 @@ void generate_support_toolpaths( bool sheath = support_params.with_sheath; bool no_sort = false; bool done = false; - if (base_layer.layer->bottom_z < EPSILON) { + // Belt printers have no flat bed first layer — the belt is the tilted + // build surface — so the dense raft_first_layer_density flange must not + // fire anywhere, including the layer at z=0 (the belt-surface line). + // (belt_floor_shear_factor is non-zero only when belt_printer is on.) + // For every other printer type, support z is never negative, so this + // matches the original "first layer at z=0" behaviour unchanged. + const bool is_belt_printer = std::abs(slicing_params.belt_floor_shear_factor) > EPSILON; + if (! is_belt_printer && base_layer.layer->bottom_z < EPSILON) { // Base flange (the 1st layer). filler = filler_first_layer; filler->angle = Geometry::deg2rad(float(config.support_angle.value + 90.)); diff --git a/src/libslic3r/Support/TreeModelVolumes.cpp b/src/libslic3r/Support/TreeModelVolumes.cpp index ebc3d2c958..b1e3cdd6dd 100644 --- a/src/libslic3r/Support/TreeModelVolumes.cpp +++ b/src/libslic3r/Support/TreeModelVolumes.cpp @@ -172,11 +172,17 @@ TreeModelVolumes::TreeModelVolumes( if (ctx.is_active() && pcfg2.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { m_belt_floor = ctx.compute_per_layer_floors(num_layers, [&](size_t layer_idx) -> double { - // Use local print_z (subtract global offset from object layer). - return (layer_idx >= num_raft_layers) - ? print_object.get_layer(layer_idx - num_raft_layers)->print_z - - print_object.belt_global_z_offset() - : 0.; + // Object layers: local print_z (subtract global offset). + if (layer_idx >= num_raft_layers) + return print_object.get_layer(layer_idx - num_raft_layers)->print_z + - print_object.belt_global_z_offset(); + // Belt raft layers (below the object): each carries its own + // local print_z in m_raft_layers. The belt floor is a tilted + // plane, so the half-plane to clip grows as print_z drops — + // using 0 here clipped every below-object layer against the + // Z=0 belt surface, under-clipping the raft region and leaving + // a dense support mass below the floor. + return (layer_idx < m_raft_layers.size()) ? m_raft_layers[layer_idx] : 0.; }); } } diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 904da75f7f..457bc5d8d2 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -1506,6 +1506,16 @@ void TreeSupport::generate_toolpaths() // ORCA: base angle used for explicit interlaced interface orientation. const float base_support_angle = Geometry::deg2rad(object_config.support_angle.value); + // Belt floor: the lowest support layer rests on the moving, tilted belt, not + // on a flat bed — so it must NOT get the bed first-layer treatment (a brim on + // interface areas, a first-layer-flow sheath at raft_first_layer_density on + // base areas). That treatment draws a loop along the Z=0 belt-floor line that + // 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; + // generate tree support tool paths tbb::parallel_for( tbb::blocked_range(m_raft_layers, m_object->support_layer_count()), @@ -1539,7 +1549,7 @@ void TreeSupport::generate_toolpaths() filler_interface->angle = base_support_angle + M_PI_2; // default interface angle is perpendicular to support angle if (area_group.type != SupportLayer::BaseType) { // interface - if (layer_id == 0) { + if (layer_id == 0 && !belt_floor_active) { Flow flow = m_raft_layers == 0 ? m_object->print()->brim_flow() : support_flow; ExtrusionRole brim_role = (area_group.type == SupportLayer::RoofType && !area_group.interface_as_base) ? erSupportMaterialInterface : erSupportMaterial; @@ -1623,7 +1633,7 @@ void TreeSupport::generate_toolpaths() } else { // base_areas - bool support_base_on_bed = (layer_id == 0 && m_raft_layers == 0); + bool support_base_on_bed = (layer_id == 0 && m_raft_layers == 0 && !belt_floor_active); Flow flow = support_base_on_bed ? m_support_params.first_layer_flow : support_flow; bool need_infill = with_infill; if(m_object_config->support_base_pattern==smpDefault) @@ -1644,7 +1654,7 @@ void TreeSupport::generate_toolpaths() std::unique_ptr base_eec = std::make_unique(); base_eec->no_sort = true; ExtrusionEntitiesPtr &base_dst = base_eec->entities; - if (layer_id == 0) { + if (layer_id == 0 && !belt_floor_active) { float density = float(m_object_config->raft_first_layer_density.value * 0.01); fill_expolygons_with_sheath_generate_paths(base_dst, loops, filler_support.get(), density, erSupportMaterial, flow, m_support_params, true, false); @@ -1916,9 +1926,6 @@ void TreeSupport::generate() 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()); - BOOST_LOG_TRIVIAL(debug) << "[BELT-CALIB] wedge ext layers=" << belt_ext_layers.size() - << " z=" << belt_ext_layers.front()->print_z << ".." << belt_ext_layers.back()->print_z - << " seeded=" << seeded; } } } @@ -2177,6 +2184,16 @@ void TreeSupport::draw_circles() coordf_t support_extrusion_width = m_support_params.support_extrusion_width; const float tree_brim_width = config.tree_support_brim_width.value; + // Belt floor: the first object layer is not on a flat bed — it rests on the + // tilted, moving belt. So the first-object-layer adhesion features (the tree + // support brim, the hybrid first-layer base expansion) must be suppressed: + // their expanded contact rings project to a stray brim/skirt loop sitting in + // 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; + if (m_object->support_layer_count() <= m_raft_layers) return; BOOST_LOG_TRIVIAL(info) << "draw_circles for object: " << m_object->model_object()->name; @@ -2287,7 +2304,7 @@ void TreeSupport::draw_circles() circle.points[i] = circle.points[i] * scale + node.position; } } - if (obj_layer_nr == 0 && m_raft_layers == 0) { + if (obj_layer_nr == 0 && m_raft_layers == 0 && !belt_floor_active) { double brim_width = !config.tree_support_auto_brim ? tree_brim_width : std::max(MIN_BRANCH_RADIUS_FIRST_LAYER, std::min(node.radius + node.dist_mm_to_top / (scale * branch_radius) * 0.5, MAX_BRANCH_RADIUS_FIRST_LAYER) - node.radius); auto tmp=offset(circle, scale_(brim_width)); if(!tmp.empty()) @@ -2352,12 +2369,19 @@ void TreeSupport::draw_circles() base_areas = intersection_ex(base_areas, m_machine_border); // Belt floor: clip tree support polygons by the belt surface plane. - // ts_layer->print_z is at LOCAL Z (global offset applied later in - // _generate_support_material), so use init_local to subtract - // belt_global_z_offset from z_shift. + // Non-organic tree support layers inherit their print_z from the + // (already globally-offset) object layers — see plan_layer_heights() + // and add_tree_support_layer(); only ORGANIC layers get the global + // Z offset applied later in _generate_support_material(). So here + // ts_layer->print_z is in the GLOBAL frame and we must use init() + // (global), not init_local(): mixing a local-frame clip plane with + // a global print_z displaces the cutoff line by belt_global_z_offset + // along the shear axis, leaving an un-clipped wedge of support below + // the belt floor. In per-object (non-global) mode belt_global_z_offset + // is 0 so init() and init_local() coincide — this is a no-op there. { BeltFloorContext ctx; - if (ctx.init_local(m_slicing_params, *m_print_config, m_object->belt_global_z_offset()) + if (ctx.init(m_slicing_params, *m_print_config) && m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { Polygons belt_surface = ctx.surface_polygon(ts_layer->print_z); base_areas = diff_ex(base_areas, belt_surface); @@ -2500,7 +2524,7 @@ void TreeSupport::draw_circles() // part. area_poly is collected from ePolygon nodes above, which are the normal // support nodes in Hybrid mode. Apply the expansion before area_groups and // lslices are built so toolpaths and brim avoidance use the same footprint. - if (layer_nr == 0 && m_raft_layers == 0 && m_support_params.support_style == smsTreeHybrid && + if (layer_nr == 0 && m_raft_layers == 0 && !belt_floor_active && m_support_params.support_style == smsTreeHybrid && m_object_config->raft_first_layer_expansion.value > 0.f) { ExPolygons expanded_base_areas; const float inflate_factor_1st_layer = float(scale_(m_object_config->raft_first_layer_expansion.value)); diff --git a/src/libslic3r/Support/TreeSupport3D.cpp b/src/libslic3r/Support/TreeSupport3D.cpp index 89e6c014f1..3386d72837 100644 --- a/src/libslic3r/Support/TreeSupport3D.cpp +++ b/src/libslic3r/Support/TreeSupport3D.cpp @@ -4016,12 +4016,20 @@ void organic_draw_branches( std::vector bottom_extra_slices; Polygons rest_support; coord_t bottom_radius = support_element_radius(config, *branch.path.front()); + // Belt printer (GeneratorOnly belt floor): the tilted belt surface is the + // build surface, so a branch should terminate ON the belt with a thin tip, + // not stamp its full footprint straight down to Z=0 and weld neighbouring + // branches into a solid floor slab. m_belt_floor is only populated in that + // mode, so it doubles as the gate (no effect on other printer types). + const bool belt_mode = !volumes.m_belt_floor.empty(); // Don't propagate further than 1.5 * bottom radius. //LayerIndex layers_propagate_max = 2 * bottom_radius / config.layer_height; LayerIndex layers_propagate_max = 5 * bottom_radius / config.layer_height; - LayerIndex layer_bottommost = branch.path.front()->state.verylost ? + LayerIndex layer_bottommost = (branch.path.front()->state.verylost && !belt_mode) ? // If the tree bottom is hanging in the air, bring it down to some surface. 0 : + // In belt mode never force-drop to Z=0 (the belt clip below handles + // termination); otherwise the "verylost" branch welds into the slab. //FIXME the "verylost" branches should stop when crossing another support. std::max(0, layer_begin - layers_propagate_max); double support_area_min_radius = M_PI * sqr(double(config.branch_radius)); @@ -4033,14 +4041,28 @@ void organic_draw_branches( Polygons collision = volumes.getCollision(0, collision_layer, false); rest_support = diff_clipped(rest_support.empty() ? slice_front_contact : rest_support, collision, ApplySafetyOffset::Yes); // Belt floor: clip propagated support at belt surface. - if (layer_idx < LayerIndex(volumes.m_belt_floor.size()) && !volumes.m_belt_floor[layer_idx].empty()) + bool belt_cut = false; + if (layer_idx < LayerIndex(volumes.m_belt_floor.size()) && !volumes.m_belt_floor[layer_idx].empty()) { + double area_before = area(rest_support); rest_support = diff(rest_support, volumes.m_belt_floor[layer_idx]); + // The belt counts as "reached" only when it actually removes part + // of this branch's footprint. The belt half-plane is non-empty at + // every near-belt layer, so testing non-emptiness alone would + // terminate a laterally-distant branch ~1 layer above true contact, + // leaving a gap. Require a real area reduction instead. + belt_cut = belt_mode && area(rest_support) < area_before - tiny_area; + } remove_small(rest_support, tiny_area); double rest_support_area = area(rest_support); if (rest_support_area < support_area_stop) // Don't propagate a fraction of the tree contact surface. break; bottom_extra_slices.push_back({ rest_support, rest_support_area }); + // Belt mode: once the belt surface actually starts cutting this branch + // it has reached the belt — keep this last (belt-clipped) slice as the + // contact and stop, rather than stamping the footprint further down. + if (belt_cut) + break; } // Now remove those bottom slices that are not supported at all. #if 0 @@ -4058,7 +4080,10 @@ void organic_draw_branches( } } #endif - if (config.settings.support_floor_layers > 0) { + // Belt mode: no solid support-floor pad under these branches — it is what + // welds neighbouring belt-terminating branches into the dense Z=0 slab. + // They simply taper out onto the tilted belt as distributed thin contacts. + if (!belt_mode && config.settings.support_floor_layers > 0) { Polygons contacts; if (!bottom_extra_slices.empty()) { const int contact_idx = int(bottom_extra_slices.size()) - 1; // Use the lowest contact slice as the footprint. @@ -4097,7 +4122,10 @@ void organic_draw_branches( } // ORCA: retain bottom contacts even when no placeable areas intersect. - if (branch.has_root && config.support_rests_on_model && branch.path.front()->state.layer_idx > 0 && + // Skipped in belt mode (m_belt_floor populated) so we don't re-introduce a + // solid floor pad for branches that terminate on the tilted belt surface. + if (volumes.m_belt_floor.empty() && + branch.has_root && config.support_rests_on_model && branch.path.front()->state.layer_idx > 0 && config.settings.support_floor_layers > 0 && config.z_distance_bottom_layers > 0 && bottom_contacts.empty() && !slice_front_contact.empty()) bottom_contacts.emplace_back(slice_front_contact); diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 9d9090480f..22c2cc85db 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1289,16 +1289,41 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const size_t n_filtered = 0, n_clip = 0; const double y_lo = model_bb.defined ? model_bb.min.y() - ANCHOR_CLIP_MARGIN_MM : -1e30; const double y_hi = model_bb.defined ? model_bb.max.y() + ANCHOR_CLIP_MARGIN_MM : 1e30; - for (const GCodeProcessorResult::MoveVertex& mv : gcode_result.moves) - if (mv.type == EMoveType::Extrude && mv.layer_id >= 1) { // skip layer-0 prime/skirt - ++n_filtered; - const Vec3d p = belt_inv * mv.position.cast(); - tp_bb_full.merge(p); - if (p.y() >= y_lo && p.y() <= y_hi) { - tp_bb_clip.merge(p); - ++n_clip; + // The anchor aligns the toolpath body onto the object-only model bbox, so it must be + // built from OBJECT-body toolpaths only. Support, skirt, brim and wipe-tower extrusions + // are not part of the model mesh and extend past it (support especially reaches well + // beyond the object on a belt), so including them drags tp_bb.min and shifts the whole + // back-transformed g-code off the mesh — but only when those features are present. That + // is the "g-code shifts vs mesh as soon as supports are enabled" bug. Filter them out. + auto is_object_body = [](ExtrusionRole r) { + return r != erSupportMaterial && r != erSupportMaterialInterface + && r != erSupportTransition + && r != erSkirt && r != erBrim && r != erWipeTower; + }; + auto accumulate = [&](bool body_only) { + tp_bb_clip = BoundingBoxf3(); + tp_bb_full = BoundingBoxf3(); + n_filtered = 0; + n_clip = 0; + for (const GCodeProcessorResult::MoveVertex& mv : gcode_result.moves) + if (mv.type == EMoveType::Extrude && mv.layer_id >= 1 // skip layer-0 prime/skirt + && (!body_only || is_object_body(mv.extrusion_role))) { + ++n_filtered; + const Vec3d p = belt_inv * mv.position.cast(); + tp_bb_full.merge(p); + if (p.y() >= y_lo && p.y() <= y_hi) { + tp_bb_clip.merge(p); + ++n_clip; + } } - } + }; + accumulate(/*body_only=*/true); + // Fallback: a plate with no object-body extrusions above layer 0 (e.g. a + // support-only object) would leave tp_bb undefined and silently skip the + // anchor. Re-accumulate over ALL extrude moves so the min-corner anchor is + // still computed rather than letting the preview float by the placement offset. + if (n_filtered == 0) + accumulate(/*body_only=*/false); // Use the clipped bbox when it still holds the bulk of the moves (outliers removed). // If the object sits far from the belt entry the toolpaths are grossly offset and the // clip drops most of them — fall back to the full bbox so the min-corner anchor still From c950c3fb6bed70fc27cd16200808e780fadf120f Mon Sep 17 00:00:00 2001 From: Joseph Robertson <32232925+HarrierPigeon@users.noreply.github.com> Date: Thu, 25 Jun 2026 22:39:12 -0500 Subject: [PATCH 062/224] Add BabyBelt Pro Profile, Courtesy of Rexit (#14424) --- resources/profiles/Printcepts.json | 54 +++++++ .../Printcepts/BabyBelt Pro_bed_texture.svg | 70 +++++++++ .../Printcepts/BabyBelt Pro_cover.png | Bin 0 -> 55870 bytes .../filament/Generic PETG @BabyBelt Pro.json | 112 ++++++++++++++ .../filament/Generic PLA @BabyBelt Pro.json | 112 ++++++++++++++ .../filament/eSUN PLA @BabyBelt Pro.json | 34 +++++ .../machine/BabyBelt Pro 0.4 nozzle.json | 87 +++++++++++ .../Printcepts/machine/BabyBelt Pro.json | 12 ++ .../Printcepts/machine/fdm_belt_common.json | 99 ++++++++++++ .../machine/fdm_klipper_common.json | 141 ++++++++++++++++++ .../machine/fdm_machine_common.json | 119 +++++++++++++++ .../0.20mm Standard @BabyBelt Pro.json | 22 +++ .../process/fdm_process_common.json | 108 ++++++++++++++ 13 files changed, 970 insertions(+) create mode 100644 resources/profiles/Printcepts.json create mode 100644 resources/profiles/Printcepts/BabyBelt Pro_bed_texture.svg create mode 100644 resources/profiles/Printcepts/BabyBelt Pro_cover.png create mode 100644 resources/profiles/Printcepts/filament/Generic PETG @BabyBelt Pro.json create mode 100644 resources/profiles/Printcepts/filament/Generic PLA @BabyBelt Pro.json create mode 100644 resources/profiles/Printcepts/filament/eSUN PLA @BabyBelt Pro.json create mode 100644 resources/profiles/Printcepts/machine/BabyBelt Pro 0.4 nozzle.json create mode 100644 resources/profiles/Printcepts/machine/BabyBelt Pro.json create mode 100644 resources/profiles/Printcepts/machine/fdm_belt_common.json create mode 100644 resources/profiles/Printcepts/machine/fdm_klipper_common.json create mode 100644 resources/profiles/Printcepts/machine/fdm_machine_common.json create mode 100644 resources/profiles/Printcepts/process/0.20mm Standard @BabyBelt Pro.json create mode 100644 resources/profiles/Printcepts/process/fdm_process_common.json diff --git a/resources/profiles/Printcepts.json b/resources/profiles/Printcepts.json new file mode 100644 index 0000000000..3f2f47443f --- /dev/null +++ b/resources/profiles/Printcepts.json @@ -0,0 +1,54 @@ +{ + "name": "Printcepts", + "version": "01.00.00.00", + "force_update": "0", + "description": "Printcepts belt printer configurations", + "machine_model_list": [ + { + "name": "BabyBelt Pro", + "sub_path": "machine/BabyBelt Pro.json" + } + ], + "process_list": [ + { + "name": "fdm_process_common", + "sub_path": "process/fdm_process_common.json" + }, + { + "name": "0.20mm Standard @BabyBelt Pro", + "sub_path": "process/0.20mm Standard @BabyBelt Pro.json" + } + ], + "filament_list": [ + { + "name": "Generic PLA @BabyBelt Pro", + "sub_path": "filament/Generic PLA @BabyBelt Pro.json" + }, + { + "name": "eSUN PLA @BabyBelt Pro", + "sub_path": "filament/eSUN PLA @BabyBelt Pro.json" + }, + { + "name": "Generic PETG @BabyBelt Pro", + "sub_path": "filament/Generic PETG @BabyBelt Pro.json" + } + ], + "machine_list": [ + { + "name": "fdm_machine_common", + "sub_path": "machine/fdm_machine_common.json" + }, + { + "name": "fdm_klipper_common", + "sub_path": "machine/fdm_klipper_common.json" + }, + { + "name": "fdm_belt_common", + "sub_path": "machine/fdm_belt_common.json" + }, + { + "name": "BabyBelt Pro 0.4 nozzle", + "sub_path": "machine/BabyBelt Pro 0.4 nozzle.json" + } + ] +} diff --git a/resources/profiles/Printcepts/BabyBelt Pro_bed_texture.svg b/resources/profiles/Printcepts/BabyBelt Pro_bed_texture.svg new file mode 100644 index 0000000000..3193c6a284 --- /dev/null +++ b/resources/profiles/Printcepts/BabyBelt Pro_bed_texture.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/profiles/Printcepts/BabyBelt Pro_cover.png b/resources/profiles/Printcepts/BabyBelt Pro_cover.png new file mode 100644 index 0000000000000000000000000000000000000000..aaf5dd266fccbe248608e4aaca7d3b5a770109d7 GIT binary patch literal 55870 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

=Yiy&&5D5@W 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 literal 0 HcmV?d00001 diff --git a/resources/profiles/Printcepts/filament/Generic PETG @BabyBelt Pro.json b/resources/profiles/Printcepts/filament/Generic PETG @BabyBelt Pro.json new file mode 100644 index 0000000000..026e6ab07d --- /dev/null +++ b/resources/profiles/Printcepts/filament/Generic PETG @BabyBelt Pro.json @@ -0,0 +1,112 @@ +{ + "type": "filament", + "name": "Generic PETG @BabyBelt Pro", + "inherits": "Generic PETG @System", + "from": "system", + "instantiation": "true", + "compatible_printers": [ + "BabyBelt Pro 0.4 nozzle" + ], + "filament_type": [ + "PETG" + ], + "filament_vendor": [ + "Generic" + ], + "filament_settings_id": [ + "Generic PETG @BabyBelt Pro" + ], + "filament_diameter": [ + "1.75" + ], + "filament_density": [ + "1.27" + ], + "filament_flow_ratio": [ + "0.95" + ], + "filament_cost": [ + "25" + ], + "filament_max_volumetric_speed": [ + "10" + ], + "nozzle_temperature": [ + "240" + ], + "nozzle_temperature_initial_layer": [ + "245" + ], + "nozzle_temperature_range_low": [ + "220" + ], + "nozzle_temperature_range_high": [ + "260" + ], + "temperature_vitrification": [ + "70" + ], + "hot_plate_temp": [ + "80" + ], + "hot_plate_temp_initial_layer": [ + "80" + ], + "cool_plate_temp": [ + "80" + ], + "cool_plate_temp_initial_layer": [ + "80" + ], + "textured_plate_temp": [ + "80" + ], + "textured_plate_temp_initial_layer": [ + "80" + ], + "fan_min_speed": [ + "40" + ], + "fan_max_speed": [ + "60" + ], + "overhang_fan_threshold": [ + "25%" + ], + "overhang_fan_speed": [ + "80" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "full_fan_speed_layer": [ + "8" + ], + "slow_down_min_speed": [ + "20" + ], + "slow_down_layer_time": [ + "4" + ], + "fan_cooling_layer_time": [ + "100" + ], + "reduce_fan_stop_start_freq": [ + "1" + ], + "filament_retraction_length": [ + "2" + ], + "filament_retraction_speed": [ + "40" + ], + "filament_deretraction_speed": [ + "40" + ], + "filament_z_hop": [ + "0.4" + ], + "filament_start_gcode": [ + "; Generic PETG @BabyBelt Pro — belt PETG, bed 80C" + ] +} diff --git a/resources/profiles/Printcepts/filament/Generic PLA @BabyBelt Pro.json b/resources/profiles/Printcepts/filament/Generic PLA @BabyBelt Pro.json new file mode 100644 index 0000000000..e89fb51e19 --- /dev/null +++ b/resources/profiles/Printcepts/filament/Generic PLA @BabyBelt Pro.json @@ -0,0 +1,112 @@ +{ + "type": "filament", + "name": "Generic PLA @BabyBelt Pro", + "inherits": "Generic PLA @System", + "from": "system", + "instantiation": "true", + "compatible_printers": [ + "BabyBelt Pro 0.4 nozzle" + ], + "filament_type": [ + "PLA" + ], + "filament_vendor": [ + "Generic" + ], + "filament_settings_id": [ + "Generic PLA @BabyBelt Pro" + ], + "filament_diameter": [ + "1.75" + ], + "filament_density": [ + "1.24" + ], + "filament_flow_ratio": [ + "0.98" + ], + "filament_cost": [ + "20" + ], + "filament_max_volumetric_speed": [ + "12" + ], + "nozzle_temperature": [ + "215" + ], + "nozzle_temperature_initial_layer": [ + "220" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "temperature_vitrification": [ + "45" + ], + "hot_plate_temp": [ + "75" + ], + "hot_plate_temp_initial_layer": [ + "75" + ], + "cool_plate_temp": [ + "75" + ], + "cool_plate_temp_initial_layer": [ + "75" + ], + "textured_plate_temp": [ + "75" + ], + "textured_plate_temp_initial_layer": [ + "75" + ], + "fan_min_speed": [ + "100" + ], + "fan_max_speed": [ + "100" + ], + "overhang_fan_threshold": [ + "50%" + ], + "overhang_fan_speed": [ + "100" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "full_fan_speed_layer": [ + "8" + ], + "slow_down_min_speed": [ + "20" + ], + "slow_down_layer_time": [ + "4" + ], + "fan_cooling_layer_time": [ + "100" + ], + "reduce_fan_stop_start_freq": [ + "1" + ], + "filament_retraction_length": [ + "1.5" + ], + "filament_retraction_speed": [ + "35" + ], + "filament_deretraction_speed": [ + "30" + ], + "filament_z_hop": [ + "0.4" + ], + "filament_start_gcode": [ + "; Generic PLA @BabyBelt Pro — belt PLA, bed 75C" + ] +} diff --git a/resources/profiles/Printcepts/filament/eSUN PLA @BabyBelt Pro.json b/resources/profiles/Printcepts/filament/eSUN PLA @BabyBelt Pro.json new file mode 100644 index 0000000000..30cf1a9a3a --- /dev/null +++ b/resources/profiles/Printcepts/filament/eSUN PLA @BabyBelt Pro.json @@ -0,0 +1,34 @@ +{ + "type": "filament", + "name": "eSUN PLA @BabyBelt Pro", + "inherits": "Generic PLA @BabyBelt Pro", + "from": "system", + "instantiation": "true", + "compatible_printers": [ + "BabyBelt Pro 0.4 nozzle" + ], + "filament_type": [ + "PLA" + ], + "filament_vendor": [ + "eSUN" + ], + "filament_settings_id": [ + "eSUN PLA @BabyBelt Pro" + ], + "nozzle_temperature_initial_layer": [ + "200" + ], + "nozzle_temperature": [ + "200" + ], + "enable_pressure_advance": [ + "1" + ], + "pressure_advance": [ + "0.12" + ], + "filament_max_volumetric_speed": [ + "20" + ] +} diff --git a/resources/profiles/Printcepts/machine/BabyBelt Pro 0.4 nozzle.json b/resources/profiles/Printcepts/machine/BabyBelt Pro 0.4 nozzle.json new file mode 100644 index 0000000000..61b37d268b --- /dev/null +++ b/resources/profiles/Printcepts/machine/BabyBelt Pro 0.4 nozzle.json @@ -0,0 +1,87 @@ +{ + "type": "machine", + "name": "BabyBelt Pro 0.4 nozzle", + "inherits": "fdm_belt_common", + "from": "system", + "setting_id": "GMPC0BBP01", + "instantiation": "true", + "printer_model": "BabyBelt Pro", + "printer_variant": "0.4", + "nozzle_diameter": [ + "0.4" + ], + "default_filament_profile": [ + "Generic PLA @BabyBelt Pro" + ], + "default_print_profile": "0.20mm Standard @BabyBelt Pro", + "printable_area": [ + "0x0", + "95x0", + "95x500", + "0x500" + ], + "printable_height": "100", + "best_object_pos": "0.5,0.05", + "nozzle_type": [ + "hardened_steel" + ], + "printer_extruder_id": [ + "1" + ], + "printer_extruder_variant": [ + "Direct Drive Standard" + ], + "thumbnails": [ + "48x48/PNG", + "300x300/PNG" + ], + "machine_max_acceleration_e": [ + "500", + "5000" + ], + "machine_max_acceleration_extruding": [ + "500", + "20000" + ], + "machine_max_acceleration_retracting": [ + "500", + "5000" + ], + "machine_max_acceleration_x": [ + "500", + "20000" + ], + "machine_max_acceleration_y": [ + "500", + "20000" + ], + "machine_max_junction_deviation": [ + "0.01" + ], + "machine_max_speed_x": [ + "50", + "200" + ], + "machine_max_speed_y": [ + "50", + "200" + ], + "machine_max_speed_z": [ + "5", + "12" + ], + "retraction_length": [ + "1.5" + ], + "retraction_speed": [ + "20" + ], + "deretraction_speed": [ + "25" + ], + "retract_lift_enforce": [ + "Top and Bottom" + ], + "support_chamber_temp_control": "0", + "machine_start_gcode": ";Start GCode\nPRINT_START ANGLE=[belt_slice_rotation_angle] EXTRUDER=[nozzle_temperature_initial_layer] BED=[hot_plate_temp_initial_layer] MATERIAL=[filament_type]\n" +} diff --git a/resources/profiles/Printcepts/machine/BabyBelt Pro.json b/resources/profiles/Printcepts/machine/BabyBelt Pro.json new file mode 100644 index 0000000000..eb711541bd --- /dev/null +++ b/resources/profiles/Printcepts/machine/BabyBelt Pro.json @@ -0,0 +1,12 @@ +{ + "type": "machine_model", + "name": "BabyBelt Pro", + "model_id": "Printcepts_BabyBelt_Pro", + "nozzle_diameter": "0.4", + "machine_tech": "FFF", + "family": "Printcepts", + "bed_model": "", + "bed_texture": "BabyBelt Pro_bed_texture.svg", + "hotend_model": "", + "default_materials": "Generic PLA @BabyBelt Pro;Generic PETG @BabyBelt Pro" +} diff --git a/resources/profiles/Printcepts/machine/fdm_belt_common.json b/resources/profiles/Printcepts/machine/fdm_belt_common.json new file mode 100644 index 0000000000..b010742d78 --- /dev/null +++ b/resources/profiles/Printcepts/machine/fdm_belt_common.json @@ -0,0 +1,99 @@ +{ + "type": "machine", + "name": "fdm_belt_common", + "inherits": "fdm_klipper_common", + "from": "system", + "instantiation": "false", + "gcode_flavor": "klipper", + "single_extruder_multi_material": "0", + "default_filament_profile": [ + "Generic PLA @System" + ], + "default_print_profile": "0.20mm Standard @System", + "max_layer_height": [ + "0.32" + ], + "min_layer_height": [ + "0.08" + ], + "deretraction_speed": [ + "30" + ], + "extruder_colour": [ + "#FCE94F" + ], + "extruder_offset": [ + "0x0" + ], + "long_retractions_when_cut": [ + "0" + ], + "nozzle_diameter": [ + "0.4" + ], + "retract_before_wipe": [ + "70%" + ], + "retract_length_toolchange": [ + "2" + ], + "retract_lift_above": [ + "0" + ], + "retract_lift_below": [ + "0" + ], + "retract_lift_enforce": [ + "All Surfaces" + ], + "retract_restart_extra": [ + "0" + ], + "retract_restart_extra_toolchange": [ + "0" + ], + "retract_when_changing_layer": [ + "1" + ], + "retraction_distances_when_cut": [ + "18" + ], + "retraction_length": [ + "0.8" + ], + "retraction_minimum_travel": [ + "1" + ], + "retraction_speed": [ + "30" + ], + "travel_slope": [ + "3" + ], + "wipe": [ + "1" + ], + "wipe_distance": [ + "1" + ], + "z_hop": [ + "0.4" + ], + "z_hop_types": [ + "Normal Lift" + ], + "gcode_remap_x": "rev_x", + "gcode_remap_y": "pos_z", + "gcode_remap_z": "pos_y", + "printer_extruder_id": [ + "1" + ], + "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", + "auxiliary_fan": "0" +} diff --git a/resources/profiles/Printcepts/machine/fdm_klipper_common.json b/resources/profiles/Printcepts/machine/fdm_klipper_common.json new file mode 100644 index 0000000000..36f3fe13c6 --- /dev/null +++ b/resources/profiles/Printcepts/machine/fdm_klipper_common.json @@ -0,0 +1,141 @@ +{ + "type": "machine", + "name": "fdm_klipper_common", + "inherits": "fdm_machine_common", + "from": "system", + "instantiation": "false", + "gcode_flavor": "klipper", + "machine_max_acceleration_e": [ + "5000", + "5000" + ], + "machine_max_acceleration_extruding": [ + "20000", + "20000" + ], + "machine_max_acceleration_retracting": [ + "5000", + "5000" + ], + "machine_max_acceleration_travel": [ + "20000", + "20000" + ], + "machine_max_acceleration_x": [ + "20000", + "20000" + ], + "machine_max_acceleration_y": [ + "20000", + "20000" + ], + "machine_max_acceleration_z": [ + "500", + "200" + ], + "machine_max_speed_e": [ + "25", + "25" + ], + "machine_max_speed_x": [ + "500", + "200" + ], + "machine_max_speed_y": [ + "500", + "200" + ], + "machine_max_speed_z": [ + "12", + "12" + ], + "machine_max_jerk_e": [ + "2.5", + "2.5" + ], + "machine_max_jerk_x": [ + "9", + "9" + ], + "machine_max_jerk_y": [ + "9", + "9" + ], + "machine_max_jerk_z": [ + "0.2", + "0.4" + ], + "machine_min_extruding_rate": [ + "0", + "0" + ], + "machine_min_travel_rate": [ + "0", + "0" + ], + "max_layer_height": [ + "0.32" + ], + "min_layer_height": [ + "0.08" + ], + "printable_height": "250", + "extruder_clearance_radius": "65", + "extruder_clearance_height_to_rod": "36", + "extruder_clearance_height_to_lid": "140", + "printer_settings_id": "", + "printer_technology": "FFF", + "printer_variant": "0.4", + "retraction_minimum_travel": [ + "1" + ], + "retract_before_wipe": [ + "70%" + ], + "retract_when_changing_layer": [ + "1" + ], + "retraction_length": [ + "0.8" + ], + "retract_length_toolchange": [ + "2" + ], + "z_hop": [ + "0.4" + ], + "retract_restart_extra": [ + "0" + ], + "retract_restart_extra_toolchange": [ + "0" + ], + "retraction_speed": [ + "30" + ], + "deretraction_speed": [ + "30" + ], + "z_hop_types": "Normal Lift", + "silent_mode": "0", + "single_extruder_multi_material": "1", + "change_filament_gcode": "", + "wipe": [ + "1" + ], + "default_filament_profile": [ + "Generic PLA @System" + ], + "default_print_profile": "0.20mm Standard @MyKlipper", + "bed_exclude_area": [ + "0x0" + ], + "machine_start_gcode": "M190 S[bed_temperature_initial_layer_single]\nM109 S[nozzle_temperature_initial_layer]\nPRINT_START EXTRUDER=[nozzle_temperature_initial_layer] BED=[bed_temperature_initial_layer_single]\n", + "machine_end_gcode": "PRINT_END", + "layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]", + "before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n", + "machine_pause_gcode": "PAUSE", + "scan_first_layer": "0", + "nozzle_type": "undefine", + "auxiliary_fan": "0" +} diff --git a/resources/profiles/Printcepts/machine/fdm_machine_common.json b/resources/profiles/Printcepts/machine/fdm_machine_common.json new file mode 100644 index 0000000000..963f39c9d8 --- /dev/null +++ b/resources/profiles/Printcepts/machine/fdm_machine_common.json @@ -0,0 +1,119 @@ +{ + "type": "machine", + "name": "fdm_machine_common", + "from": "system", + "instantiation": "false", + "printer_technology": "FFF", + "deretraction_speed": [ + "40" + ], + "extruder_colour": [ + "#FCE94F" + ], + "extruder_offset": [ + "0x0" + ], + "gcode_flavor": "marlin", + "silent_mode": "0", + "machine_max_acceleration_e": [ + "5000" + ], + "machine_max_acceleration_extruding": [ + "10000" + ], + "machine_max_acceleration_retracting": [ + "1000" + ], + "machine_max_acceleration_x": [ + "10000" + ], + "machine_max_acceleration_y": [ + "10000" + ], + "machine_max_acceleration_z": [ + "500" + ], + "machine_max_speed_e": [ + "60" + ], + "machine_max_speed_x": [ + "500" + ], + "machine_max_speed_y": [ + "500" + ], + "machine_max_speed_z": [ + "10" + ], + "machine_max_jerk_e": [ + "5" + ], + "machine_max_jerk_x": [ + "8" + ], + "machine_max_jerk_y": [ + "8" + ], + "machine_max_jerk_z": [ + "0.4" + ], + "machine_min_extruding_rate": [ + "0" + ], + "machine_min_travel_rate": [ + "0" + ], + "max_layer_height": [ + "0.32" + ], + "min_layer_height": [ + "0.08" + ], + "printable_height": "250", + "extruder_clearance_radius": "65", + "extruder_clearance_height_to_rod": "36", + "extruder_clearance_height_to_lid": "140", + "nozzle_diameter": [ + "0.4" + ], + "printer_settings_id": "", + "printer_variant": "0.4", + "retraction_minimum_travel": [ + "2" + ], + "retract_before_wipe": [ + "70%" + ], + "retract_when_changing_layer": [ + "1" + ], + "retraction_length": [ + "1" + ], + "retract_length_toolchange": [ + "1" + ], + "z_hop": [ + "0" + ], + "retract_restart_extra": [ + "0" + ], + "retract_restart_extra_toolchange": [ + "0" + ], + "retraction_speed": [ + "60" + ], + "single_extruder_multi_material": "1", + "change_filament_gcode": "", + "wipe": [ + "1" + ], + "default_print_profile": "", + "machine_start_gcode": "G0 Z20 F9000\nG92 E0; G1 E-10 F1200\nG28\nM970 Q1 A10 B10 C130 K0\nM970 Q1 A10 B131 C250 K1\nM974 Q1 S1 P0\nM970 Q0 A10 B10 C130 H20 K0\nM970 Q0 A10 B131 C250 K1\nM974 Q0 S1 P0\nM220 S100 ;Reset Feedrate\nM221 S100 ;Reset Flowrate\nG29 ;Home\nG90;\nG92 E0 ;Reset Extruder \nG1 Z2.0 F3000 ;Move Z Axis up \nG1 X10.1 Y20 Z0.28 F5000.0 ;Move to start position\nM109 S205;\nG1 X10.1 Y200.0 Z0.28 F1500.0 E15 ;Draw the first line\nG1 X10.4 Y200.0 Z0.28 F5000.0 ;Move to side a little\nG1 X10.4 Y20 Z0.28 F1500.0 E30 ;Draw the second line\nG92 E0 ;Reset Extruder \nG1 X110 Y110 Z2.0 F3000 ;Move Z Axis up", + "machine_end_gcode": "M400 ; wait for buffer to clear\nG92 E0 ; zero the extruder\nG1 E-4.0 F3600; retract \nG91\nG1 Z3;\nM104 S0 ; turn off hotend\nM140 S0 ; turn off bed\nM106 S0 ; turn off fan\nG90 \nG0 X110 Y200 F3600 \nprint_end", + "layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]", + "before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n", + "machine_pause_gcode": "M601" +} diff --git a/resources/profiles/Printcepts/process/0.20mm Standard @BabyBelt Pro.json b/resources/profiles/Printcepts/process/0.20mm Standard @BabyBelt Pro.json new file mode 100644 index 0000000000..7f7a810762 --- /dev/null +++ b/resources/profiles/Printcepts/process/0.20mm Standard @BabyBelt Pro.json @@ -0,0 +1,22 @@ +{ + "type": "process", + "name": "0.20mm Standard @BabyBelt Pro", + "inherits": "fdm_process_common", + "from": "system", + "instantiation": "true", + "layer_height": "0.2", + "initial_layer_print_height": "0.2", + "initial_layer_line_width": "0.42", + "wall_loops": "2", + "reduce_infill_retraction": "1", + "detect_overhang_wall": "1", + "skirt_loops": "0", + "skirt_distance": "0", + "sparse_infill_pattern": "grid", + "sparse_infill_speed": "200", + "support_base_pattern": "rectilinear", + "support_interface_pattern": "rectilinear", + "compatible_printers": [ + "BabyBelt Pro 0.4 nozzle" + ] +} diff --git a/resources/profiles/Printcepts/process/fdm_process_common.json b/resources/profiles/Printcepts/process/fdm_process_common.json new file mode 100644 index 0000000000..b8e882770a --- /dev/null +++ b/resources/profiles/Printcepts/process/fdm_process_common.json @@ -0,0 +1,108 @@ +{ + "type": "process", + "name": "fdm_process_common", + "from": "system", + "instantiation": "false", + "adaptive_layer_height": "0", + "reduce_crossing_wall": "0", + "max_travel_detour_distance": "0", + "bottom_surface_pattern": "monotonic", + "bottom_shell_thickness": "0", + "bridge_speed": "50", + "brim_width": "5", + "brim_object_gap": "0.1", + "compatible_printers": [], + "compatible_printers_condition": "", + "print_sequence": "by layer", + "default_acceleration": "1000", + "initial_layer_acceleration": "500", + "top_surface_acceleration": "1000", + "travel_acceleration": "1000", + "inner_wall_acceleration": "1000", + "outer_wall_acceleration": "700", + "bridge_no_support": "0", + "draft_shield": "disabled", + "elefant_foot_compensation": "0", + "enable_arc_fitting": "0", + "wall_infill_order": "inner wall/outer wall/infill", + "infill_direction": "45", + "sparse_infill_density": "15%", + "sparse_infill_pattern": "crosshatch", + "initial_layer_print_height": "0.2", + "infill_combination": "0", + "infill_wall_overlap": "25%", + "interface_shells": "0", + "ironing_flow": "10%", + "ironing_spacing": "0.15", + "ironing_speed": "30", + "ironing_type": "no ironing", + "reduce_infill_retraction": "1", + "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[initial_tool]}_{printer_model}_{print_time}.gcode", + "detect_overhang_wall": "1", + "slowdown_for_curled_perimeters": "1", + "overhang_1_4_speed": "0", + "overhang_2_4_speed": "50", + "overhang_3_4_speed": "30", + "overhang_4_4_speed": "10", + "line_width": "110%", + "inner_wall_line_width": "110%", + "outer_wall_line_width": "100%", + "top_surface_line_width": "93.75%", + "sparse_infill_line_width": "110%", + "initial_layer_line_width": "120%", + "internal_solid_infill_line_width": "120%", + "support_line_width": "96%", + "wall_loops": "3", + "print_settings_id": "", + "raft_layers": "0", + "seam_position": "aligned", + "skirt_distance": "2", + "skirt_height": "3", + "min_skirt_length": "4", + "skirt_loops": "0", + "minimum_sparse_infill_area": "15", + "spiral_mode": "0", + "standby_temperature_delta": "-5", + "enable_support": "0", + "resolution": "0.012", + "support_type": "normal(auto)", + "support_on_build_plate_only": "0", + "support_top_z_distance": "0.2", + "support_bottom_z_distance": "0.2", + "support_filament": "0", + "support_interface_loop_pattern": "0", + "support_interface_filament": "0", + "support_interface_top_layers": "2", + "support_interface_bottom_layers": "2", + "support_interface_spacing": "0.5", + "support_interface_speed": "80", + "support_base_pattern": "default", + "support_base_pattern_spacing": "2.5", + "support_speed": "150", + "support_threshold_angle": "30", + "support_object_xy_distance": "0.35", + "tree_support_branch_angle": "30", + "tree_support_wall_count": "0", + "tree_support_with_infill": "0", + "detect_thin_wall": "0", + "top_surface_pattern": "monotonicline", + "top_shell_thickness": "0.8", + "enable_prime_tower": "1", + "wipe_tower_no_sparse_layers": "0", + "prime_tower_width": "60", + "xy_hole_compensation": "0", + "xy_contour_compensation": "0", + "layer_height": "0.2", + "bottom_shell_layers": "3", + "top_shell_layers": "4", + "bridge_flow": "1", + "initial_layer_speed": "45", + "initial_layer_infill_speed": "45", + "outer_wall_speed": "45", + "inner_wall_speed": "80", + "sparse_infill_speed": "150", + "internal_solid_infill_speed": "150", + "top_surface_speed": "50", + "gap_infill_speed": "30", + "travel_speed": "200" +} From b61ba98183041234405decc3e08d4b0d4550ad55 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sun, 2 Aug 2026 16:20:22 -0500 Subject: [PATCH 063/224] belt: adapt BeltGCodeWriter to upstream's per-extruder speed options Upstream retyped travel_speed and travel_speed_z to ConfigOptionFloatsNullable and initial_layer_travel_speed to ConfigOptionFloatsOrPercentsNullable, so the scalar .value / get_abs_value() accessors no longer compile. BeltGCodeWriter.cpp is belt-only and merged without conflict, so this only surfaced at build time. Index them the way the base GCodeWriter does -- .get_at(m_cached_extruder_idx) and get_abs_value_at(..., m_cached_extruder_idx) -- keeping belt's per-point first_layer_for_point test rather than the base class's m_is_first_layer. m_cached_extruder_idx moves from private to the existing protected block that already exposes writer state to subclasses, so the belt writer resolves the per-extruder index identically to the base writer instead of guessing one. --- src/libslic3r/BeltGCodeWriter.cpp | 15 ++++++++------- src/libslic3r/GCodeWriter.hpp | 7 +++++-- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/libslic3r/BeltGCodeWriter.cpp b/src/libslic3r/BeltGCodeWriter.cpp index 7c645edf43..f46b1f2c35 100644 --- a/src/libslic3r/BeltGCodeWriter.cpp +++ b/src/libslic3r/BeltGCodeWriter.cpp @@ -87,7 +87,8 @@ std::string BeltGCodeWriter::travel_to_xy(const Vec2d &point, const std::string m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer, Vec3d(point.x(), point.y(), m_pos.z())); auto speed = first_layer_for_point - ? this->config.get_abs_value("initial_layer_travel_speed") : this->config.travel_speed.value; + ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) + : this->config.travel_speed.get_at(m_cached_extruder_idx); w.emit_f(speed * 60.0); w.emit_comment(GCodeWriter::full_gcode_comment, comment); return w.string(); @@ -110,13 +111,13 @@ std::string BeltGCodeWriter::_travel_to_z(double z, const std::string &comment) { m_pos(2) = z; - double speed = this->config.travel_speed_z.value; + double speed = this->config.travel_speed_z.get_at(m_cached_extruder_idx); if (speed == 0.) { const bool first_layer_for_point = belt_point_on_first_layer( m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer, Vec3d(m_pos.x(), m_pos.y(), z)); - speed = first_layer_for_point ? this->config.get_abs_value("initial_layer_travel_speed") - : this->config.travel_speed.value; + speed = first_layer_for_point ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) + : this->config.travel_speed.get_at(m_cached_extruder_idx); } // Belt printer: a Z-only move in slicing frame needs to emit both Y and Z in machine coords. @@ -182,8 +183,8 @@ std::string BeltGCodeWriter::travel_to_xyz(const Vec3d &point, const std::string const bool first_layer_for_point = belt_point_on_first_layer( m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer, point); auto travel_speed = - first_layer_for_point ? this->config.get_abs_value("initial_layer_travel_speed") - : this->config.travel_speed.value; + first_layer_for_point ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) + : this->config.travel_speed.get_at(m_cached_extruder_idx); // Handle pending z_hop if (std::abs(m_to_lift) > EPSILON) { @@ -253,7 +254,7 @@ std::string BeltGCodeWriter::travel_to_xyz(const Vec3d &point, const std::string // Belt mode: always emit full XYZ GCodeG1Formatter w; w.emit_xyz(point_on_plate); - w.emit_f(this->config.travel_speed.value * 60.0); + w.emit_f(this->config.travel_speed.get_at(m_cached_extruder_idx) * 60.0); w.emit_comment(GCodeWriter::full_gcode_comment, comment); m_pos = dest_point; diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index 2671851f53..da2ce35cd6 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -167,14 +167,17 @@ protected: // Apply axis remap to a point. Returns pos unchanged if remap is identity. Vec3d apply_axis_remap(const Vec3d &pos) const; + // Motion uses the global/base process variant until a filament becomes active. + // Protected so BeltGCodeWriter indexes the per-extruder speed options (travel_speed, + // travel_speed_z, initial_layer_travel_speed) exactly as the base writer does. + size_t m_cached_extruder_idx; + private: // Extruders are sorted by their ID, so that binary search is possible. std::vector m_filament_extruders; bool m_single_extruder_multi_material; std::vector m_curr_filament_extruder; int m_curr_extruder_id; - // Motion uses the global/base process variant until a filament becomes active. - size_t m_cached_extruder_idx; unsigned int m_last_acceleration; unsigned int m_last_travel_acceleration; std::vector m_max_travel_acceleration; From 8f6802fff834194628850163c7059bd1d790bd20 Mon Sep 17 00:00:00 2001 From: HarrierPigeon Date: Mon, 25 May 2026 17:01:13 -0600 Subject: [PATCH 064/224] step one: post-process analysis --- src/libslic3r/GCode/GCodeProcessor.cpp | 60 +++++++++++++------ src/libslic3r/GCode/GCodeProcessor.hpp | 7 +++ src/libslic3r/GCode/MachineFrameTransform.cpp | 17 ++++-- src/libslic3r/GCode/MachineFrameTransform.hpp | 10 +++- 4 files changed, 71 insertions(+), 23 deletions(-) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index ba2ae5db45..bc84963da0 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -2765,6 +2765,32 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int return ps; }; + // Belt-printer post-gcode shear/scale/post_remap is applied as the final + // step of BeltGCodeWriter::to_machine_coords, so MoveVertex.position is + // in the printer's machine frame. Undo it here so XY area and Z height + // checks operate in the build-volume frame that printable_area / + // printable_height are defined in. For non-belt printers + // (is_active() == false) apply_inverse is identity and behaviour is + // unchanged from before. + const bool machine_frame_active = m_machine_frame_transform.is_active(); + auto compare_pos = [&](const GCodeProcessorResult::MoveVertex &move) -> Vec3d { + Vec3d pos = move.position.cast(); + if (!machine_frame_active) + return pos; + Vec3d extruder_off = Vec3d::Zero(); + if (size_t(move.extruder_id) < m_extruder_offsets.size()) + extruder_off = m_extruder_offsets[move.extruder_id].cast(); + // Strip plate + extruder offsets to recover the raw machine-frame + // coordinate that was emitted into the G-code (see store_move_vertex). + Vec3d machine(pos.x() - m_x_offset - extruder_off.x(), + pos.y() - m_y_offset - extruder_off.y(), + pos.z() - extruder_off.z() + m_z_offset); + Vec3d build = m_machine_frame_transform.apply_inverse(machine); + // Re-apply plate offset so the result matches plate_printable_poly, + // which is translated by plate_offset below. + return Vec3d(build.x() + m_x_offset, build.y() + m_y_offset, build.z()); + }; + struct GCodePosInfo { Points pos; @@ -2775,28 +2801,23 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int std::map> gcode_path_pos; // object_id, filament_id, pos for (const GCodeProcessorResult::MoveVertex &move : m_result.moves) { // sometimes, the start line extrude was outside the edge of plate a little, this is allowed, so do not include into the gcode_path_pos - if (move.type == EMoveType::Extrude /* && move.extrusion_role != ExtrusionRole::erFlush || move.type == EMoveType::Travel*/) + if (move.type == EMoveType::Extrude /* && move.extrusion_role != ExtrusionRole::erFlush || move.type == EMoveType::Travel*/) { + const Vec3d cp = compare_pos(move); + // For belt printers we read Z from the inverse-transformed position + // (post-origin-snap, pre-machine-frame). Otherwise keep the + // original print_z source (the slicer's layer-Z comment) so + // non-belt behaviour is bit-for-bit unchanged. + const float z_for_height = machine_frame_active ? float(cp.z()) : move.print_z; if (move.extrusion_role == ExtrusionRole::erCustom) { - /*if (move.is_arc_move_with_interpolation_points()) { - for (int i = 0; i < move.interpolation_points.size(); i++) { - gcode_path_pos[move.object_label_id][int(move.extruder_id)].pos_custom.emplace_back(to_2d(move.interpolation_points[i].cast())); - } - } else {*/ - gcode_path_pos[move.object_label_id][int(move.extruder_id)].pos_custom.emplace_back(to_2d(move.position.cast())); - //} + gcode_path_pos[move.object_label_id][int(move.extruder_id)].pos_custom.emplace_back(to_2d(cp)); gcode_path_pos[move.object_label_id][int(move.extruder_id)].max_print_z_custom = - std::max(gcode_path_pos[move.object_label_id][int(move.extruder_id)].max_print_z_custom, move.print_z); + std::max(gcode_path_pos[move.object_label_id][int(move.extruder_id)].max_print_z_custom, z_for_height); } else { - /*if (move.is_arc_move_with_interpolation_points()) { - for (int i = 0; i < move.interpolation_points.size(); i++) { - gcode_path_pos[move.object_label_id][int(move.extruder_id)].pos.emplace_back(to_2d(move.interpolation_points[i].cast())); - } - } else {*/ - gcode_path_pos[move.object_label_id][int(move.extruder_id)].pos.emplace_back(to_2d(move.position.cast())); - //} + gcode_path_pos[move.object_label_id][int(move.extruder_id)].pos.emplace_back(to_2d(cp)); gcode_path_pos[move.object_label_id][int(move.extruder_id)].max_print_z = std::max(gcode_path_pos[move.object_label_id][int(move.extruder_id)].max_print_z, - move.print_z); + z_for_height); } + } } bool valid = true; @@ -3036,6 +3057,11 @@ void GCodeProcessor::apply_config(const PrintConfig& config) m_result.printable_height = config.printable_height; + // Belt printer: cache the post-gcode machine-frame transform so the + // multi-extruder validator can undo it and compare against build-volume + // bounds rather than machine-frame positions. + m_machine_frame_transform.init_from_config(config); + auto filament_maps = config.option("filament_map"); if (filament_maps != nullptr) { m_filament_maps = filament_maps->values; diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index ce61d25ee4..63f38bf5ef 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -7,6 +7,7 @@ #include "libslic3r/PrintConfig.hpp" #include "libslic3r/CustomGCode.hpp" #include "libslic3r/MultiNozzleUtils.hpp" +#include "libslic3r/GCode/MachineFrameTransform.hpp" #include #include @@ -1145,6 +1146,12 @@ class Print; double m_x_offset{ 0 }; double m_y_offset{ 0 }; + // Belt-printer post-gcode shear/scale/post_remap. Used by + // check_multi_extruder_gcode_valid to undo the machine-frame + // transform on move positions so bounds checks operate in the + // pre-machine-frame (build-volume) frame. + MachineFrameTransform m_machine_frame_transform; + unsigned int m_line_id; unsigned int m_last_line_id; float m_feedrate; // mm/s diff --git a/src/libslic3r/GCode/MachineFrameTransform.cpp b/src/libslic3r/GCode/MachineFrameTransform.cpp index bdc172d7a4..3d322b4303 100644 --- a/src/libslic3r/GCode/MachineFrameTransform.cpp +++ b/src/libslic3r/GCode/MachineFrameTransform.cpp @@ -7,8 +7,9 @@ namespace Slic3r { bool MachineFrameTransform::init_from_config(const PrintConfig &config) { - m_active = false; - m_transform = Transform3d::Identity(); + m_active = false; + m_transform = Transform3d::Identity(); + m_transform_inverse = Transform3d::Identity(); if (!config.belt_printer.value) return false; @@ -59,8 +60,9 @@ bool MachineFrameTransform::init_from_config(const PrintConfig &config) if (combined.isApprox(Transform3d::Identity())) return false; - m_transform = combined; - m_active = true; + m_transform = combined; + m_transform_inverse = combined.inverse(); + m_active = true; return true; } @@ -71,4 +73,11 @@ Vec3d MachineFrameTransform::apply(const Vec3d &pos) const return m_transform * pos; } +Vec3d MachineFrameTransform::apply_inverse(const Vec3d &pos) const +{ + if (!m_active) + return pos; + return m_transform_inverse * pos; +} + } // namespace Slic3r diff --git a/src/libslic3r/GCode/MachineFrameTransform.hpp b/src/libslic3r/GCode/MachineFrameTransform.hpp index f5e71398da..744a7976c5 100644 --- a/src/libslic3r/GCode/MachineFrameTransform.hpp +++ b/src/libslic3r/GCode/MachineFrameTransform.hpp @@ -31,6 +31,11 @@ public: // Apply the transform to a point. Returns pos unchanged if not active. Vec3d apply(const Vec3d &pos) const; + // Apply the inverse transform. Returns pos unchanged if not active. + // Used by validators that need to compare emitted machine-frame + // coordinates against build-volume bounds. + Vec3d apply_inverse(const Vec3d &pos) const; + bool is_active() const { return m_active; } // The composed shear*scale transform (identity when inactive). Exposed so the @@ -39,8 +44,9 @@ public: const Transform3d& transform() const { return m_transform; } private: - bool m_active = false; - Transform3d m_transform = Transform3d::Identity(); + bool m_active = false; + Transform3d m_transform = Transform3d::Identity(); + Transform3d m_transform_inverse = Transform3d::Identity(); }; } // namespace Slic3r From 79fd847ce3d466e52a75709806422fa5337d071f Mon Sep 17 00:00:00 2001 From: HarrierPigeon Date: Mon, 25 May 2026 17:19:58 -0600 Subject: [PATCH 065/224] fix pre-slice warnings --- src/libslic3r/Print.cpp | 93 +++++++++++++++++++++-------------------- 1 file changed, 47 insertions(+), 46 deletions(-) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 75d73e546e..c9fd0118d0 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -13,6 +13,7 @@ #include "Time.hpp" #include "GCode.hpp" #include "BeltGCode.hpp" +#include "BeltTransform.hpp" #include "GCode/WipeTower.hpp" #include "GCode/WipeTower2.hpp" #include "Utils.hpp" @@ -1430,57 +1431,57 @@ StringObjectException Print::validate(std::vector *warnin return profile; }; - // Checks that the print does not exceed the max print height + // Checks that the print does not exceed the max print height. + // For belt printers the slicing-frame Z spans the sheared X-length and + // is not comparable to printable_height (which is gantry clearance in the + // build-volume frame). Compare against the model's pre-shear Z instead, + // mirroring the bbox computed in PrintObject::update_slicing_parameters. + const bool belt_printer = this->config().belt_printer.value; + const double shrinkage_compensation_z = this->shrinkage_compensation().z(); for (size_t print_object_idx = 0; print_object_idx < m_objects.size(); ++ print_object_idx) { const PrintObject &print_object = *m_objects[print_object_idx]; - // Belt printer: the sliced (virtual) Z is belt travel along the conveyor, not - // build height, so the loop below (which measures the sliced layer stack) is - // meaningless here. The real ceiling is the usable vertical clearance above the - // belt, which printable_height holds directly: the gantry travels up the tilted - // plane, so a part of height z needs gantry reach z / cos(tilt), and the machine's - // gantry-axis range is sized to accommodate that (e.g. IdeaFormer IR3 V2: ~354 mm - // of gantry travel = 250 mm vertical at 45°, and printable_height = 250). So compare - // the upright object height against printable_height directly. - if (m_config.belt_printer.value) { - const double max_h = m_config.printable_height.value; - double obj_top = 0.0; - const ModelObject *mo = print_object.model_object(); - for (const ModelInstance *mi : mo->instances) - obj_top = std::max(obj_top, mo->instance_bounding_box(*mi).max.z()); - if (obj_top > max_h + EPSILON) - return StringObjectException{ - Slic3r::format(_u8L("The object %1% exceeds the maximum printable height " - "above the belt (%2% mm)."), - print_object.model_object()->name, max_h), - print_object.model_object(), "" }; - continue; - } - //FIXME It is quite expensive to generate object layers just to get the print height! - if (auto layers = generate_object_layers(print_object.slicing_parameters(), layer_height_profile(print_object_idx), print_object.config().precise_z_height.value); - !layers.empty()) { - Vec3d test =this->shrinkage_compensation(); - const double shrinkage_compensation_z = this->shrinkage_compensation().z(); - - if (shrinkage_compensation_z != 1. && layers.back() > (this->config().printable_height / shrinkage_compensation_z + EPSILON)) { - // The object exceeds the maximum build volume height because of shrinkage compensation. - return StringObjectException{ - Slic3r::format(_u8L("While the object %1% itself fits the build volume, it exceeds the maximum build volume height because of material shrinkage compensation."), print_object.model_object()->name), - print_object.model_object(), - "" - }; - } else if (layers.back() > this->config().printable_height + EPSILON) { - // Test whether the last slicing plane is below or above the print volume. - return StringObjectException{ - 0.5 * (layers[layers.size() - 2] + layers.back()) > this->config().printable_height + EPSILON ? - Slic3r::format(_u8L("The object %1% exceeds the maximum build volume height."), print_object.model_object()->name) : - Slic3r::format(_u8L("While the object %1% itself fits the build volume, its last layer exceeds the maximum build volume height."), print_object.model_object()->name) + - " " + _u8L("You might want to reduce the size of your model or change current print settings and retry."), - print_object.model_object(), - "" - }; + double effective_max_z = 0; + bool last_layer_below_max = false; + 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(); + effective_max_z = raw_z; + have_height = raw_z > 0; + } else { + //FIXME It is quite expensive to generate object layers just to get the print height! + auto layers = generate_object_layers(print_object.slicing_parameters(), layer_height_profile(print_object_idx), print_object.config().precise_z_height.value); + if (!layers.empty()) { + effective_max_z = layers.back(); + last_layer_below_max = layers.size() >= 2 && + 0.5 * (layers[layers.size() - 2] + layers.back()) <= this->config().printable_height + EPSILON; + have_height = true; } } + + if (!have_height) + continue; + + if (shrinkage_compensation_z != 1. && effective_max_z > (this->config().printable_height / shrinkage_compensation_z + EPSILON)) { + // The object exceeds the maximum build volume height because of shrinkage compensation. + return StringObjectException{ + Slic3r::format(_u8L("While the object %1% itself fits the build volume, it exceeds the maximum build volume height because of material shrinkage compensation."), print_object.model_object()->name), + print_object.model_object(), + "" + }; + } else if (effective_max_z > this->config().printable_height + EPSILON) { + return StringObjectException{ + last_layer_below_max ? + Slic3r::format(_u8L("While the object %1% itself fits the build volume, its last layer exceeds the maximum build volume height."), print_object.model_object()->name) + + " " + _u8L("You might want to reduce the size of your model or change current print settings and retry.") : + Slic3r::format(_u8L("The object %1% exceeds the maximum build volume height."), print_object.model_object()->name), + print_object.model_object(), + "" + }; + } } // Some of the objects has variable layer height applied by painting or by a table. From 0342e06d87c516a6d68fdaf81ef5e3a4dd233645 Mon Sep 17 00:00:00 2001 From: HarrierPigeon Date: Tue, 26 May 2026 03:46:32 -0600 Subject: [PATCH 066/224] last step in fixing the g-code stuff up --- src/libslic3r/GCode/GCodeProcessor.cpp | 2 ++ src/libslic3r/GCode/GCodeProcessor.hpp | 6 ++++++ src/libslic3r/Print.cpp | 11 ++++++++++- src/slic3r/GUI/GCodeViewer.cpp | 2 ++ src/slic3r/GUI/GCodeViewer.hpp | 2 ++ src/slic3r/GUI/GLCanvas3D.cpp | 6 +++++- 6 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index bc84963da0..9cad57e693 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -2530,6 +2530,7 @@ void GCodeProcessorResult::reset() { long_retraction_when_cut = false; timelapse_warning_code = 0; printable_height = 0.0f; + machine_frame_transform_active = false; settings_ids.reset(); filaments_count = 0; backtrace_enabled = false; @@ -3061,6 +3062,7 @@ void GCodeProcessor::apply_config(const PrintConfig& config) // multi-extruder validator can undo it and compare against build-volume // bounds rather than machine-frame positions. m_machine_frame_transform.init_from_config(config); + m_result.machine_frame_transform_active = m_machine_frame_transform.is_active(); auto filament_maps = config.option("filament_map"); if (filament_maps != nullptr) { diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index 63f38bf5ef..7b90840ad2 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -285,6 +285,11 @@ class Print; // as gcode_Z + this offset, so the designed-view back-transform must subtract it // to recover the model's belt coordinate. float belt_z_origin{ 0.f }; + // Belt printer: post-gcode shear/scale/post_remap is configured and + // non-identity. When set, the layer Z values in `moves` are in the + // 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 }; @@ -377,6 +382,7 @@ class Print; initial_layer_time = other.initial_layer_time; belt_tilt_angle = other.belt_tilt_angle; belt_z_origin = other.belt_z_origin; + machine_frame_transform_active = other.machine_frame_transform_active; preslice_remap_x = other.preslice_remap_x; preslice_remap_y = other.preslice_remap_y; preslice_remap_z = other.preslice_remap_z; diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index c9fd0118d0..ba28f96804 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -14,6 +14,7 @@ #include "GCode.hpp" #include "BeltGCode.hpp" #include "BeltTransform.hpp" +#include "GCode/MachineFrameTransform.hpp" #include "GCode/WipeTower.hpp" #include "GCode/WipeTower2.hpp" #include "Utils.hpp" @@ -1436,9 +1437,17 @@ StringObjectException Print::validate(std::vector *warnin // is not comparable to printable_height (which is gantry clearance in the // build-volume frame). Compare against the model's pre-shear Z instead, // mirroring the bbox computed in PrintObject::update_slicing_parameters. + // When the post-gcode MachineFrameTransform is active the printer's + // physical Z mapping is non-trivial — skip the check entirely. const bool belt_printer = this->config().belt_printer.value; + bool skip_max_height_check = false; + if (belt_printer) { + MachineFrameTransform machine_frame; + machine_frame.init_from_config(this->config()); + skip_max_height_check = machine_frame.is_active(); + } const double shrinkage_compensation_z = this->shrinkage_compensation().z(); - for (size_t print_object_idx = 0; print_object_idx < m_objects.size(); ++ print_object_idx) { + for (size_t print_object_idx = 0; !skip_max_height_check && print_object_idx < m_objects.size(); ++ print_object_idx) { const PrintObject &print_object = *m_objects[print_object_idx]; double effective_max_z = 0; diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index a034f3dce8..42a4f6b9fb 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1521,6 +1521,7 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const m_custom_gcode_per_print_z = gcode_result.custom_gcode_per_print_z; m_max_print_height = gcode_result.printable_height; + m_machine_frame_transform_active = gcode_result.machine_frame_transform_active; m_z_offset = gcode_result.z_offset; @@ -1736,6 +1737,7 @@ void GCodeViewer::reset() m_paths_bounding_box = BoundingBoxf3(); m_max_bounding_box = BoundingBoxf3(); m_max_print_height = 0.0f; + m_machine_frame_transform_active = false; m_z_offset = 0.0f; m_extruders_count = 0; m_filament_diameters = std::vector(); diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 66d9cbb444..9ee680d889 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -203,6 +203,7 @@ private: //BBS: add shell bounding box BoundingBoxf3 m_shell_bounding_box; float m_max_print_height{ 0.0f }; + bool m_machine_frame_transform_active{ false }; float m_z_offset{ 0.0f }; ConfigOptionMode m_user_mode; @@ -289,6 +290,7 @@ public: std::vector get_plater_extruder(); const float get_max_print_height() const { return m_max_print_height; } + bool is_machine_frame_transform_active() const { return m_machine_frame_transform_active; } const BoundingBoxf3& get_paths_bounding_box() const { return m_paths_bounding_box; } const BoundingBoxf3& get_max_bounding_box() const { return m_max_bounding_box; } const BoundingBoxf3& get_shell_bounding_box() const { return m_shell_bounding_box; } diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 3345a88386..e3e84bc1c7 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -10315,7 +10315,11 @@ void GLCanvas3D::_set_warning_notification_if_needed(EWarning warning) if (current_printer_technology() != ptSLA) { unsigned int max_z_layer = m_gcode_viewer.get_layers_z_range().back(); if (warning == EWarning::ToolHeightOutside) // check if max z_layer height exceed max print height - show = m_gcode_viewer.has_data() && (m_gcode_viewer.get_layers_zs()[max_z_layer] - m_gcode_viewer.get_max_print_height() >= 1e-6); + // Belt printer with active post-gcode machine-frame transform: layer Z values + // live in the machine frame, not the build-volume frame, so the comparison + // against printable_height is meaningless. Suppress the warning entirely. + show = m_gcode_viewer.has_data() && !m_gcode_viewer.is_machine_frame_transform_active() + && (m_gcode_viewer.get_layers_zs()[max_z_layer] - m_gcode_viewer.get_max_print_height() >= 1e-6); else if (warning == EWarning::ToolpathOutside) { // check if max x,y coords exceed bed area show = m_gcode_viewer.has_data() && !m_gcode_viewer.is_contained_in_bed() && (m_gcode_viewer.get_max_print_height() -m_gcode_viewer.get_layers_zs()[max_z_layer] >= 1e-6); From 04554abae62e0e7f5b77ab74dd9a56385d55a46d Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 27 Jun 2026 01:47:20 -0500 Subject: [PATCH 067/224] Fix belt printer illegal gantry move at print start MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On a belt printer the first travel of the print emitted a bogus move to the bed corner with the nozzle far up the gantry, e.g. G1 X95 Y168.19 Z237.857 F12000 right after the first "; printing object" line. Y168 (≈ the layer Z) is out of the gantry's range. Root cause: the layer-change z-hop is deferred via lazy_lift and consumed by the first BeltGCodeWriter::travel_to_xyz, whose NormalLift branch does a separate lift-in-place via _travel_to_z(target.z()). On a normal printer _travel_to_z emits a Z-only move, but in belt mode Z is coupled to Y/X, so _travel_to_z re-emits the current m_pos through the belt shear. At print start (and after custom gcode) m_pos.xy is still the uninitialised origin (0,0), which the back-transform + axis-remap shear into machine (X=bed_max, Y=layer_z) — the illegal move. Guard the NormalLift branch on is_current_position_clear(), matching the SlopeLift branch directly above it which already does so. When the position isn't established there is nothing to lift over, and the xy_z_move that follows travels straight to the destination with full XYZ, establishing the correct position. Bookkeeping is unaffected: in this path m_lifted stays 0, so no spurious restore move is produced. Verified by re-slicing the repro project: the start-of-print move is now G1 X44.946 Y.621 Z237.857 (straight to the first object point), no move touches the bed-max X edge, and the max Y over the whole file is 62.8mm (printable_height 100). --- src/libslic3r/BeltGCodeWriter.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/BeltGCodeWriter.cpp b/src/libslic3r/BeltGCodeWriter.cpp index f46b1f2c35..e1dad4177f 100644 --- a/src/libslic3r/BeltGCodeWriter.cpp +++ b/src/libslic3r/BeltGCodeWriter.cpp @@ -217,7 +217,17 @@ std::string BeltGCodeWriter::travel_to_xyz(const Vec3d &point, const std::string w0.emit_comment(GCodeWriter::full_gcode_comment, comment); slop_move = w0.string(); } - else if (m_to_lift_type == LiftType::NormalLift) { + else if (m_to_lift_type == LiftType::NormalLift && this->is_current_position_clear()) { + // Only lift-in-place when the current position is known. On a normal + // printer _travel_to_z emits a Z-only move, but in belt mode Z is coupled + // to Y/X, so _travel_to_z re-emits the current m_pos through the belt + // shear. At print start (and after custom gcode) m_pos.xy is still the + // uninitialised origin (0,0), which shears into a bogus machine point + // (e.g. X=bed_max, Y=layer_z) far up the gantry. Skipping the separate + // lift here is safe: there is nothing to lift over yet, and the + // xy_z_move below travels straight to the destination with full XYZ, + // establishing the correct position. This mirrors the SlopeLift branch + // above, which already guards on is_current_position_clear(). slop_move = _travel_to_z(target.z(), "normal lift Z"); } } From 02e313a115f3ad18cc58e4b76e3b0d7f0077d443 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 27 Jun 2026 17:50:49 -0500 Subject: [PATCH 068/224] Add belt-printer regression test for start-of-print gantry move MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Locks in the fix from the previous commit. A fresh BeltGCodeWriter has an unestablished planar position (is_current_position_clear() == false) and its m_pos.xy is the origin (0,0). With a pending NormalLift z-hop, travel_to_xyz used to lift in place via _travel_to_z(), which in belt mode shears the origin into a machine Y ~= the layer Z — a move far up the gantry. The test configures an X-tilt 45 deg belt transform, defers a z-hop via lazy_lift, travels to a near-belt first point (transformed gantry Y ~= 1mm), and asserts no emitted move has Y anywhere near the layer Z. Verified to fail without the fix (max emitted Y = 100.0 vs the destination's ~1.0) and pass with it. --- tests/fff_print/test_gcodewriter.cpp | 73 ++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/tests/fff_print/test_gcodewriter.cpp b/tests/fff_print/test_gcodewriter.cpp index b09b38e794..b2a11becbc 100644 --- a/tests/fff_print/test_gcodewriter.cpp +++ b/tests/fff_print/test_gcodewriter.cpp @@ -15,6 +15,11 @@ #include #include "test_helpers.hpp" +#include +#include +#include "libslic3r/BeltGCodeWriter.hpp" +#include "libslic3r/GCodeReader.hpp" +#include "libslic3r/PrintConfig.hpp" using namespace Slic3r; using namespace Slic3r::Test; @@ -847,3 +852,71 @@ TEST_CASE("Custom G-code motion limits are restored before generated moves", "[G REQUIRE(gcode.find("M204 S6000 ; adjust acceleration", custom_gcode_pos) != std::string::npos); REQUIRE(gcode.find("M205 X8 Y8 ; adjust jerk", custom_gcode_pos) != std::string::npos); } + +// Regression test for the belt-printer "illegal gantry move at print start" bug. +// +// On a belt printer the layer-change z-hop is deferred (lazy_lift) and consumed +// by the first travel_to_xyz, whose NormalLift branch lifts in place via +// _travel_to_z(). On a normal printer _travel_to_z emits a Z-only move, but in +// belt mode Z is coupled to Y/X, so _travel_to_z re-emits the current m_pos +// through the belt shear. At print start (and after custom gcode) +// is_current_position_clear() is false and m_pos.xy is still the uninitialised +// origin (0,0), which shears into machine (X=bed_max, Y=layer_z) — a move far up +// the gantry, e.g. "G1 X95 Y168.19 Z237.857". The fix guards that lift on +// is_current_position_clear(), mirroring the SlopeLift branch. +SCENARIO("Belt: the first travel does not lift through the uninitialised origin", "[GCodeWriter][belt]") +{ + GIVEN("A fresh BeltGCodeWriter configured for an X-tilt 45 degree belt") { + // 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; + + BeltGCodeWriter writer; + writer.set_machine_frame_transform(belt_config); + writer.set_belt_back_transform(belt_config); + + std::vector extruder_ids { 0 }; + writer.set_extruders(extruder_ids); + writer.set_extruder(0); + writer.config.travel_speed.value = 100.0; + writer.config.z_hop.values = { 0.4 }; + writer.config.retract_lift_above.values = { 0.0 }; + writer.config.retract_lift_below.values = { 0.0 }; + + // A fresh writer has not established its planar position yet — this is the + // precondition that made the origin leak into the first move. + REQUIRE_FALSE(writer.is_current_position_clear()); + + WHEN("a layer-change z-hop is pending and we travel to the first object point") { + // Defer a z-hop, exactly as a retract on layer change leaves it. + writer.lazy_lift(LiftType::NormalLift); + + // First object point in slicing coordinates: a near-belt point (y ~= -z) + // so its transformed gantry Y is small (~1mm). The bogus origin lift, in + // contrast, would shear to machine Y ~= nominal_z. + const double nominal_z = 100.0; + std::string gcode = writer.travel_to_xyz(Vec3d(10.0, -(nominal_z - 1.0), nominal_z)); + + THEN("no emitted move flies up the gantry; machine Y stays near the part") { + double max_y = std::numeric_limits::lowest(); + GCodeReader reader; + reader.parse_buffer(gcode, [&max_y](GCodeReader &, const GCodeReader::GCodeLine &line) { + if (line.cmd_is("G1") && line.has(Y)) + max_y = std::max(max_y, double(line.y())); + }); + // The destination shears to machine Y ~= 1mm. The old origin-lift bug + // produced a separate move at machine Y ~= nominal_z (100mm), so any + // Y well above the part means the origin leaked into a move. + REQUIRE(max_y > 0.0); // the destination move was emitted and parsed + REQUIRE(max_y < 10.0); // ... and nothing flew up the gantry + } + } + } +} From a83cd8aa296e5415311515ceb7b5b3ddedea4903 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Mon, 29 Jun 2026 02:58:58 -0500 Subject: [PATCH 069/224] Fix belt printer phantom extrusion line from Y=0 in preview On a belt printer the sliced preview drew a stray extrusion-colored line from Y~=0 to the model, rendered in the first extrusion role's color. It is not a travel and does not occur on non-belt printers. GCodeProcessor::store_move_vertex pins a move's stored Z to the first-layer height during the start-G-code "prepare" stage. That is a harmless cosmetic tidy-up on a normal printer, but on a belt printer the designed-view back-transform couples machine Z into the rendered model Y (the belt tilt mixes the height and belt-feed axes). Pinning Z back-transforms the last prepare-stage move (the unretract before the first extrusion) to model Y ~= 0, and libvgcode then draws a phantom extrusion segment from Y ~= 0 to the first real toolpath. Keep the real Z for belt printers (gated on belt_tilt_angle, parsed from the G-code header before the body) so prepare-stage moves back-transform correctly. Non-belt processing is byte-identical. The emitted G-code was already correct; this is a preview-geometry fix. --- src/libslic3r/GCode/GCodeProcessor.cpp | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 9cad57e693..43536c30b9 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -7067,6 +7067,22 @@ void GCodeProcessor::store_move_vertex(EMoveType type, EMovePathType path_type, m_result.print_statistics.total_travel_distance += m_travel_dist; } + // During the start G-code "prepare" stage the toolhead Z is not yet a real + // print height on a normal printer, so it is pinned to the first-layer height + // to keep the preview tidy. Belt printers are the exception: there the Z is + // written explicitly by BeltGCodeWriter and the designed-view back-transform + // couples machine Z into the rendered model Y (the belt tilt mixes the height + // and belt-feed axes). Overriding Z therefore back-transforms the last + // prepare-stage move (the unretract before the first extrusion) to model + // Y ~= 0, and the libvgcode path builder then draws a phantom extrusion + // segment from Y ~= 0 to the first real toolpath. Keep the real Z for belt + // printers so prepare-stage moves map correctly. Gated on belt_tilt_angle (set + // from the G-code header before the body is processed) so non-belt processing + // is byte-identical. + const float store_z = (m_processing_start_custom_gcode && m_result.belt_tilt_angle == 0.f) + ? m_first_layer_height + : m_end_position[Z] - m_z_offset; + m_result.moves.push_back({ m_last_line_id, type, @@ -7074,7 +7090,7 @@ void GCodeProcessor::store_move_vertex(EMoveType type, EMovePathType path_type, static_cast(filament_id), m_cp_color.current, //BBS: add plate's offset to the rendering vertices - Vec3f(m_end_position[X] + m_x_offset, m_end_position[Y] + m_y_offset, m_processing_start_custom_gcode ? m_first_layer_height : m_end_position[Z]- m_z_offset) + m_extruder_offsets[filament_id], + Vec3f(m_end_position[X] + m_x_offset, m_end_position[Y] + m_y_offset, store_z) + m_extruder_offsets[filament_id], static_cast(m_end_position[E] - m_start_position[E]), m_feedrate, 0.0f, // actual feedrate From 613dad92a13cfcdaa9df326d5bacb32941041ba7 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Mon, 29 Jun 2026 02:59:07 -0500 Subject: [PATCH 070/224] Add belt-printer regression test for prepare-stage move Z Processes a minimal belt start sequence through GCodeProcessor::process_buffer and asserts the move preceding the first extrusion keeps its real Z, so it can no longer back-transform to model Y~=0 and produce the phantom extrusion line. Belt printers are non-Bambu, so the processor uses the compatible reserved tags ("TYPE:"); the test sets s_IsBBLPrinter=false (saved/restored via an RAII guard) to mirror the real printer. Proven to fail without the fix (the prepare-stage move's Z is pinned to the first-layer height, 0 here) and pass with it. --- tests/fff_print/test_gcodewriter.cpp | 79 +++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/tests/fff_print/test_gcodewriter.cpp b/tests/fff_print/test_gcodewriter.cpp index b2a11becbc..6af11d8d23 100644 --- a/tests/fff_print/test_gcodewriter.cpp +++ b/tests/fff_print/test_gcodewriter.cpp @@ -15,6 +15,8 @@ #include #include "test_helpers.hpp" +#include +#include "libslic3r/GCode/GCodeProcessor.hpp" #include #include #include "libslic3r/BeltGCodeWriter.hpp" @@ -885,7 +887,8 @@ SCENARIO("Belt: the first travel does not lift through the uninitialised origin" std::vector extruder_ids { 0 }; writer.set_extruders(extruder_ids); writer.set_extruder(0); - writer.config.travel_speed.value = 100.0; + // travel_speed became per-extruder (ConfigOptionFloatsNullable) upstream. + writer.config.travel_speed.values = { 100.0 }; writer.config.z_hop.values = { 0.4 }; writer.config.retract_lift_above.values = { 0.0 }; writer.config.retract_lift_below.values = { 0.0 }; @@ -920,3 +923,77 @@ SCENARIO("Belt: the first travel does not lift through the uninitialised origin" } } } + +// Regression test for the belt-printer "phantom extrusion line from Y=0" bug. +// +// GCodeProcessor::store_move_vertex pins a move's stored Z to the first-layer +// height while m_processing_start_custom_gcode is set (the start G-code "prepare" +// stage), because on a normal printer the toolhead Z there is not yet a real print +// height. On a belt printer that override is wrong: Z is written explicitly and the +// designed-view back-transform couples machine Z into the rendered model Y (the +// belt tilt mixes the height and belt-feed axes). Overriding it back-transforms the +// last prepare-stage move (the unretract right before the first extrusion) to +// model Y ~= 0, and libvgcode then draws a phantom extrusion segment from Y ~= 0 to +// the first real toolpath — rendered in the first extrusion role's color. The fix +// keeps the real Z for belt printers (gated on belt_tilt_angle). Here we assert the +// prepare-stage move keeps its real Z so it can no longer leak to Y ~= 0. +SCENARIO("Belt: start-gcode prepare-stage moves keep their real Z", "[GCode][belt]") +{ + // Belt printers are non-Bambu, so the G-code uses the "compatible" reserved + // tags ("TYPE:" for the extrusion role). The processor selects the tag table + // from the static s_IsBBLPrinter flag, so mirror the belt-printer setting here + // (saved/restored so test ordering stays unaffected). + struct BBLPrinterGuard { + bool prev = GCodeProcessor::s_IsBBLPrinter; + BBLPrinterGuard() { GCodeProcessor::s_IsBBLPrinter = false; } + ~BBLPrinterGuard() { GCodeProcessor::s_IsBBLPrinter = prev; } + } bbl_guard; + + GIVEN("A belt G-code whose start sequence travels to a high machine Z before the first extrusion") { + // The leading "; belt_slice_rotation_angle = 45" header sets belt_tilt_angle + // (parsed before the body), enabling the belt code path. ;TYPE:Custom before + // any G1 turns on the prepare stage; ;TYPE:Outer wall turns it off, exactly + // as a sliced belt print is laid out. + const std::string gcode = + "; belt_slice_rotation_angle = 45\n" + "G90\n" + "G21\n" + "M83\n" + ";TYPE:Custom\n" + "G1 E-1.5 F2100\n" // retract at the (0,0,0) origin + "G1 X45 Y0.3 Z50 F12000\n" // travel to the approach point (prepare stage) + "G1 E1.5 F1800\n" // unretract in place (prepare stage) + ";TYPE:Outer wall\n" + "G1 X46 Y0.3 Z50 E0.05\n"; // first extrusion, same Z as the approach + + GCodeProcessor processor; + processor.process_buffer(gcode); + const GCodeProcessorResult& result = processor.get_result(); + + THEN("the belt code path is active") { + REQUIRE_THAT(result.belt_tilt_angle, Catch::Matchers::WithinAbs(45.0, 1e-4)); + } + + WHEN("locating the first extrusion and the move that precedes it") { + size_t first_extrude = result.moves.size(); + for (size_t i = 0; i < result.moves.size(); ++i) + if (result.moves[i].type == EMoveType::Extrude) { first_extrude = i; break; } + + THEN("an extrusion and a preceding move exist") { + REQUIRE(first_extrude < result.moves.size()); + REQUIRE(first_extrude > 0); + } + + THEN("the preceding prepare-stage move shares the extrusion's real Z (no leak to Y=0)") { + const float extrude_z = result.moves[first_extrude].position.z(); + const float prev_z = result.moves[first_extrude - 1].position.z(); + // The first extrusion is at the real Z=50; before the fix the + // prepare-stage move's Z was pinned to the first-layer height + // (0 here) instead, which back-transforms to model Y ~= 0 and + // produces the phantom extrusion segment. + REQUIRE_THAT(extrude_z, Catch::Matchers::WithinAbs(50.0, 1e-3)); + REQUIRE_THAT(prev_z, Catch::Matchers::WithinAbs(50.0, 1e-3)); + } + } + } +} From f563df04f674ec01caeec25a4e807596e53232c9 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Mon, 3 Aug 2026 01:52:43 -0500 Subject: [PATCH 071/224] belt: default first_layer_plane to Auto, not BeltAffine BeltAffine activates the FirstLayerPlane evaluator unconditionally, so on a non-belt printer on_first_layer(point) stopped agreeing with the legacy slicing-layer-0 test. Every per-path first-layer call site in _extrude then took the non-first-layer branch, and first-layer speeds were skipped: brim came out at the volumetric fallback (24.6 mm/s) instead of initial_layer_speed (10 mm/s). This is the shared speed path, so it affected all printers on this branch, not just belt ones. Auto resolves to BeltAffine only when belt_printer is set with a non-zero slicing rotation, and to XY (evaluator inactive, legacy behaviour) otherwise -- exactly what the option's own description already promised. Caught by "Brim uses first layer speed" (upstream #14616), which arrived with the upstream merge; the bad default dates back to a9bae54f20 (#30). Verified against a pristine upstream/main build, which passes the same test. tests/fff_print: 100/100 test cases, 1085 assertions (was 99/100). Both belt regression tests still pass, confirming Auto still resolves to BeltAffine for belt printers. Note: this changes a config default. Projects and profiles that stored first_layer_plane explicitly are unaffected; those relying on the default will now get correct first-layer speeds on non-belt printers, so their G-code changes accordingly. --- src/libslic3r/PrintConfig.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 2b513afe1d..b2984164ee 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -7209,7 +7209,13 @@ void PrintConfigDef::init_fff_params() 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; - def->set_default_value(new ConfigOptionEnum(FirstLayerPlaneMode::BeltAffine)); + // 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::Auto)); def = this->add("first_layer_plane_offset", coFloat); def->label = L("Belt plane offset"); From 725df641086532a61dcd20648ef862187934961b Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Tue, 4 Aug 2026 17:00:49 -0500 Subject: [PATCH 072/224] profiles: fix belt printer CI failures (slice check + setting_id) The belt-printer branch is failing two profile gates. Both stem from the three belt-only vendors (Custom's generic belt printer, IdeaFormer, Printcepts) not existing upstream, so upstream maintenance passed them by. Slice check: 4 of 1015 printers failed - Custom's MyBeltPrinter 0.2/0.4/ 0.6/0.8 nozzle all fell back to "Default Setting". No process profile in the Custom vendor listed any MyBeltPrinter in compatible_printers, and Custom's fdm_belt_common pointed default_print_profile at "0.20mm Standard @System", which does not exist in that vendor's index, so the generic belt printer had no usable process at all. This gap dates to when MyBeltPrinter was added (2026-04-07); it only started failing now because the slice-check job is newer than that. Adds two process profiles modelled on the sibling @MyKlipper ones: - 0.20mm Standard @MyBeltPrinter - 0.4/0.6/0.8 nozzles - 0.12mm Fine @MyBeltPrinter - 0.2/0.4 nozzles The split is forced by hardware: the 0.2 nozzle preset caps max_layer_height at 0.16, so a single 0.20mm profile cannot legally cover it. fdm_belt_common now defaults to the standard profile and the 0.2 nozzle preset overrides to the fine one. setting_id: 14 files failed the rules introduced in #14432. That migration renumbered 7425 files across 61 vendors but skipped these three, leaving BabyBelt Pro, IdeaFormer IR3 V2 and MyBeltPrinter squatting the "G*" id space reserved for Bambu (GMPC0BBP01, GMIF001, GM_BELT_00x) and four instantiated filament/process presets carrying no setting_id at all. Regenerated with scripts/assign_vendor_setting_ids.py. Also repoints the identical dangling "0.20mm Standard @System" in Printcepts' and IdeaFormer's fdm_belt_common at their own real process profiles. That is a no-op today because both concrete printers override it, but it is the same landmine that took out MyBeltPrinter. Vendor index versions bumped so check_installed_vendor_profiles() will re-install the corrected profiles over an existing install. Note: changing a shipped preset's setting_id can orphan user presets that reference it as base_id. #14432 accepted that tradeoff for 61 vendors; this keeps these three consistent with the rest. Verified: orca_extra_profile_check.py reports 0 errors across 66 vendors (was 14 files with errors), and OrcaSlicer_profile_validator -s slices all 1015 printer presets successfully (was 4 failures). --- resources/profiles/Custom.json | 10 +++++++++- .../machine/MyBeltPrinter 0.2 nozzle.json | 3 ++- .../machine/MyBeltPrinter 0.4 nozzle.json | 2 +- .../machine/MyBeltPrinter 0.6 nozzle.json | 2 +- .../machine/MyBeltPrinter 0.8 nozzle.json | 2 +- .../Custom/machine/fdm_belt_common.json | 2 +- .../process/0.12mm Fine @MyBeltPrinter.json | 20 +++++++++++++++++++ .../0.20mm Standard @MyBeltPrinter.json | 17 ++++++++++++++++ resources/profiles/IdeaFormer.json | 2 +- .../Generic PETG @IdeaFormer IR3 V2.json | 1 + .../Generic PLA @IdeaFormer IR3 V2.json | 1 + .../filament/eSUN PLA @IdeaFormer IR3 V2.json | 1 + .../machine/IdeaFormer IR3 V2 0.4 nozzle.json | 2 +- .../IdeaFormer/machine/fdm_belt_common.json | 2 +- .../0.20mm Standard @IdeaFormer IR3 V2.json | 1 + resources/profiles/Printcepts.json | 2 +- .../filament/Generic PETG @BabyBelt Pro.json | 1 + .../filament/Generic PLA @BabyBelt Pro.json | 1 + .../filament/eSUN PLA @BabyBelt Pro.json | 1 + .../machine/BabyBelt Pro 0.4 nozzle.json | 2 +- .../Printcepts/machine/fdm_belt_common.json | 2 +- .../0.20mm Standard @BabyBelt Pro.json | 1 + 22 files changed, 66 insertions(+), 12 deletions(-) create mode 100644 resources/profiles/Custom/process/0.12mm Fine @MyBeltPrinter.json create mode 100644 resources/profiles/Custom/process/0.20mm Standard @MyBeltPrinter.json diff --git a/resources/profiles/Custom.json b/resources/profiles/Custom.json index ba1ddc14f4..9030b881e3 100644 --- a/resources/profiles/Custom.json +++ b/resources/profiles/Custom.json @@ -1,6 +1,6 @@ { "name": "Custom Printer", - "version": "02.04.00.03", + "version": "02.04.00.04", "force_update": "0", "description": "My configurations", "machine_model_list": [ @@ -66,6 +66,14 @@ "name": "0.16mm Optimal @MyKlipper", "sub_path": "process/0.16mm Optimal @MyKlipper.json" }, + { + "name": "0.12mm Fine @MyBeltPrinter", + "sub_path": "process/0.12mm Fine @MyBeltPrinter.json" + }, + { + "name": "0.20mm Standard @MyBeltPrinter", + "sub_path": "process/0.20mm Standard @MyBeltPrinter.json" + }, { "name": "0.20mm Standard @MyKlipper", "sub_path": "process/0.20mm Standard @MyKlipper.json" diff --git a/resources/profiles/Custom/machine/MyBeltPrinter 0.2 nozzle.json b/resources/profiles/Custom/machine/MyBeltPrinter 0.2 nozzle.json index 5e7655fbe1..1226f4dd68 100644 --- a/resources/profiles/Custom/machine/MyBeltPrinter 0.2 nozzle.json +++ b/resources/profiles/Custom/machine/MyBeltPrinter 0.2 nozzle.json @@ -3,9 +3,10 @@ "name": "MyBeltPrinter 0.2 nozzle", "inherits": "fdm_belt_common", "from": "system", - "setting_id": "GM_BELT_001", + "setting_id": "3w1uyJdmm14QhDnH", "instantiation": "true", "printer_model": "Generic Belt Printer", + "default_print_profile": "0.12mm Fine @MyBeltPrinter", "nozzle_diameter": [ "0.2" ], diff --git a/resources/profiles/Custom/machine/MyBeltPrinter 0.4 nozzle.json b/resources/profiles/Custom/machine/MyBeltPrinter 0.4 nozzle.json index 73be69ba61..e8149509ca 100644 --- a/resources/profiles/Custom/machine/MyBeltPrinter 0.4 nozzle.json +++ b/resources/profiles/Custom/machine/MyBeltPrinter 0.4 nozzle.json @@ -3,7 +3,7 @@ "name": "MyBeltPrinter 0.4 nozzle", "inherits": "fdm_belt_common", "from": "system", - "setting_id": "GM_BELT_002", + "setting_id": "6nRHUtvJOUffocbu", "instantiation": "true", "printer_model": "Generic Belt Printer", "nozzle_diameter": [ diff --git a/resources/profiles/Custom/machine/MyBeltPrinter 0.6 nozzle.json b/resources/profiles/Custom/machine/MyBeltPrinter 0.6 nozzle.json index 49646db06d..b8ca999dca 100644 --- a/resources/profiles/Custom/machine/MyBeltPrinter 0.6 nozzle.json +++ b/resources/profiles/Custom/machine/MyBeltPrinter 0.6 nozzle.json @@ -3,7 +3,7 @@ "name": "MyBeltPrinter 0.6 nozzle", "inherits": "fdm_belt_common", "from": "system", - "setting_id": "GM_BELT_003", + "setting_id": "K0m9HbUNwKT4UCJV", "instantiation": "true", "printer_model": "Generic Belt Printer", "nozzle_diameter": [ diff --git a/resources/profiles/Custom/machine/MyBeltPrinter 0.8 nozzle.json b/resources/profiles/Custom/machine/MyBeltPrinter 0.8 nozzle.json index 412291754e..b546b5b288 100644 --- a/resources/profiles/Custom/machine/MyBeltPrinter 0.8 nozzle.json +++ b/resources/profiles/Custom/machine/MyBeltPrinter 0.8 nozzle.json @@ -3,7 +3,7 @@ "name": "MyBeltPrinter 0.8 nozzle", "inherits": "fdm_belt_common", "from": "system", - "setting_id": "GM_BELT_004", + "setting_id": "rHAweDz4eNwttPNA", "instantiation": "true", "printer_model": "Generic Belt Printer", "nozzle_diameter": [ diff --git a/resources/profiles/Custom/machine/fdm_belt_common.json b/resources/profiles/Custom/machine/fdm_belt_common.json index b010742d78..86245e7377 100644 --- a/resources/profiles/Custom/machine/fdm_belt_common.json +++ b/resources/profiles/Custom/machine/fdm_belt_common.json @@ -9,7 +9,7 @@ "default_filament_profile": [ "Generic PLA @System" ], - "default_print_profile": "0.20mm Standard @System", + "default_print_profile": "0.20mm Standard @MyBeltPrinter", "max_layer_height": [ "0.32" ], diff --git a/resources/profiles/Custom/process/0.12mm Fine @MyBeltPrinter.json b/resources/profiles/Custom/process/0.12mm Fine @MyBeltPrinter.json new file mode 100644 index 0000000000..15814e6725 --- /dev/null +++ b/resources/profiles/Custom/process/0.12mm Fine @MyBeltPrinter.json @@ -0,0 +1,20 @@ +{ + "type": "process", + "name": "0.12mm Fine @MyBeltPrinter", + "inherits": "fdm_process_klipper_common", + "from": "system", + "setting_id": "EugqqdLJ423bgEwN", + "instantiation": "true", + "layer_height": "0.12", + "initial_layer_print_height": "0.12", + "bottom_shell_layers": "5", + "top_shell_layers": "6", + "support_top_z_distance": "0.08", + "support_bottom_z_distance": "0.08", + "skirt_loops": "0", + "skirt_distance": "0", + "compatible_printers": [ + "MyBeltPrinter 0.2 nozzle", + "MyBeltPrinter 0.4 nozzle" + ] +} diff --git a/resources/profiles/Custom/process/0.20mm Standard @MyBeltPrinter.json b/resources/profiles/Custom/process/0.20mm Standard @MyBeltPrinter.json new file mode 100644 index 0000000000..6c10b250d6 --- /dev/null +++ b/resources/profiles/Custom/process/0.20mm Standard @MyBeltPrinter.json @@ -0,0 +1,17 @@ +{ + "type": "process", + "name": "0.20mm Standard @MyBeltPrinter", + "inherits": "fdm_process_klipper_common", + "from": "system", + "setting_id": "YzCDAgH3uLOM53pF", + "instantiation": "true", + "layer_height": "0.2", + "initial_layer_print_height": "0.2", + "skirt_loops": "0", + "skirt_distance": "0", + "compatible_printers": [ + "MyBeltPrinter 0.4 nozzle", + "MyBeltPrinter 0.6 nozzle", + "MyBeltPrinter 0.8 nozzle" + ] +} diff --git a/resources/profiles/IdeaFormer.json b/resources/profiles/IdeaFormer.json index a021e7c164..4a8519353b 100644 --- a/resources/profiles/IdeaFormer.json +++ b/resources/profiles/IdeaFormer.json @@ -1,6 +1,6 @@ { "name": "IdeaFormer", - "version": "02.00.00.02", + "version": "02.00.00.03", "force_update": "0", "description": "IdeaFormer belt printer configurations", "machine_model_list": [ diff --git a/resources/profiles/IdeaFormer/filament/Generic PETG @IdeaFormer IR3 V2.json b/resources/profiles/IdeaFormer/filament/Generic PETG @IdeaFormer IR3 V2.json index c51b20abbc..22ec0737d7 100644 --- a/resources/profiles/IdeaFormer/filament/Generic PETG @IdeaFormer IR3 V2.json +++ b/resources/profiles/IdeaFormer/filament/Generic PETG @IdeaFormer IR3 V2.json @@ -3,6 +3,7 @@ "name": "Generic PETG @IdeaFormer IR3 V2", "inherits": "Generic PETG @System", "from": "system", + "setting_id": "n4zaXcUUzTqAxq5f", "instantiation": "true", "compatible_printers": [ "IdeaFormer IR3 V2 0.4 nozzle" diff --git a/resources/profiles/IdeaFormer/filament/Generic PLA @IdeaFormer IR3 V2.json b/resources/profiles/IdeaFormer/filament/Generic PLA @IdeaFormer IR3 V2.json index c86ba651da..74c2011bca 100644 --- a/resources/profiles/IdeaFormer/filament/Generic PLA @IdeaFormer IR3 V2.json +++ b/resources/profiles/IdeaFormer/filament/Generic PLA @IdeaFormer IR3 V2.json @@ -3,6 +3,7 @@ "name": "Generic PLA @IdeaFormer IR3 V2", "inherits": "Generic PLA @System", "from": "system", + "setting_id": "1xjycsEAFh6KQIhp", "instantiation": "true", "compatible_printers": [ "IdeaFormer IR3 V2 0.4 nozzle" diff --git a/resources/profiles/IdeaFormer/filament/eSUN PLA @IdeaFormer IR3 V2.json b/resources/profiles/IdeaFormer/filament/eSUN PLA @IdeaFormer IR3 V2.json index d34131842a..7431aff5b1 100644 --- a/resources/profiles/IdeaFormer/filament/eSUN PLA @IdeaFormer IR3 V2.json +++ b/resources/profiles/IdeaFormer/filament/eSUN PLA @IdeaFormer IR3 V2.json @@ -3,6 +3,7 @@ "name": "eSUN PLA @IdeaFormer IR3 V2", "inherits": "Generic PLA @IdeaFormer IR3 V2", "from": "system", + "setting_id": "XqkviBmFHEglXueX", "instantiation": "true", "compatible_printers": [ "IdeaFormer IR3 V2 0.4 nozzle" diff --git a/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json b/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json index b6f128ca86..5f477ef0bb 100644 --- a/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json +++ b/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json @@ -3,7 +3,7 @@ "name": "IdeaFormer IR3 V2 0.4 nozzle", "inherits": "fdm_belt_common", "from": "system", - "setting_id": "GMIF001", + "setting_id": "MDQZgwRgg72lmjtu", "instantiation": "true", "printer_model": "IdeaFormer IR3 V2", "printer_variant": "0.4", diff --git a/resources/profiles/IdeaFormer/machine/fdm_belt_common.json b/resources/profiles/IdeaFormer/machine/fdm_belt_common.json index b010742d78..25daeb34a7 100644 --- a/resources/profiles/IdeaFormer/machine/fdm_belt_common.json +++ b/resources/profiles/IdeaFormer/machine/fdm_belt_common.json @@ -9,7 +9,7 @@ "default_filament_profile": [ "Generic PLA @System" ], - "default_print_profile": "0.20mm Standard @System", + "default_print_profile": "0.20mm Standard @IdeaFormer IR3 V2", "max_layer_height": [ "0.32" ], diff --git a/resources/profiles/IdeaFormer/process/0.20mm Standard @IdeaFormer IR3 V2.json b/resources/profiles/IdeaFormer/process/0.20mm Standard @IdeaFormer IR3 V2.json index b06b2e2192..c2e46e4154 100644 --- a/resources/profiles/IdeaFormer/process/0.20mm Standard @IdeaFormer IR3 V2.json +++ b/resources/profiles/IdeaFormer/process/0.20mm Standard @IdeaFormer IR3 V2.json @@ -3,6 +3,7 @@ "name": "0.20mm Standard @IdeaFormer IR3 V2", "inherits": "fdm_process_common", "from": "system", + "setting_id": "91atcIwv5728phqX", "instantiation": "true", "layer_height": "0.2", "initial_layer_print_height": "0.2", diff --git a/resources/profiles/Printcepts.json b/resources/profiles/Printcepts.json index 3f2f47443f..61d418b8ab 100644 --- a/resources/profiles/Printcepts.json +++ b/resources/profiles/Printcepts.json @@ -1,6 +1,6 @@ { "name": "Printcepts", - "version": "01.00.00.00", + "version": "01.00.00.01", "force_update": "0", "description": "Printcepts belt printer configurations", "machine_model_list": [ diff --git a/resources/profiles/Printcepts/filament/Generic PETG @BabyBelt Pro.json b/resources/profiles/Printcepts/filament/Generic PETG @BabyBelt Pro.json index 026e6ab07d..939edbf233 100644 --- a/resources/profiles/Printcepts/filament/Generic PETG @BabyBelt Pro.json +++ b/resources/profiles/Printcepts/filament/Generic PETG @BabyBelt Pro.json @@ -3,6 +3,7 @@ "name": "Generic PETG @BabyBelt Pro", "inherits": "Generic PETG @System", "from": "system", + "setting_id": "gCzHpDNgVwQR6tgk", "instantiation": "true", "compatible_printers": [ "BabyBelt Pro 0.4 nozzle" diff --git a/resources/profiles/Printcepts/filament/Generic PLA @BabyBelt Pro.json b/resources/profiles/Printcepts/filament/Generic PLA @BabyBelt Pro.json index e89fb51e19..aa11e4c598 100644 --- a/resources/profiles/Printcepts/filament/Generic PLA @BabyBelt Pro.json +++ b/resources/profiles/Printcepts/filament/Generic PLA @BabyBelt Pro.json @@ -3,6 +3,7 @@ "name": "Generic PLA @BabyBelt Pro", "inherits": "Generic PLA @System", "from": "system", + "setting_id": "24PpcnhVx9v5f4fD", "instantiation": "true", "compatible_printers": [ "BabyBelt Pro 0.4 nozzle" diff --git a/resources/profiles/Printcepts/filament/eSUN PLA @BabyBelt Pro.json b/resources/profiles/Printcepts/filament/eSUN PLA @BabyBelt Pro.json index 30cf1a9a3a..7ab984eea7 100644 --- a/resources/profiles/Printcepts/filament/eSUN PLA @BabyBelt Pro.json +++ b/resources/profiles/Printcepts/filament/eSUN PLA @BabyBelt Pro.json @@ -3,6 +3,7 @@ "name": "eSUN PLA @BabyBelt Pro", "inherits": "Generic PLA @BabyBelt Pro", "from": "system", + "setting_id": "EH3X7oE0DU5tSpjW", "instantiation": "true", "compatible_printers": [ "BabyBelt Pro 0.4 nozzle" diff --git a/resources/profiles/Printcepts/machine/BabyBelt Pro 0.4 nozzle.json b/resources/profiles/Printcepts/machine/BabyBelt Pro 0.4 nozzle.json index 61b37d268b..620498e0d4 100644 --- a/resources/profiles/Printcepts/machine/BabyBelt Pro 0.4 nozzle.json +++ b/resources/profiles/Printcepts/machine/BabyBelt Pro 0.4 nozzle.json @@ -3,7 +3,7 @@ "name": "BabyBelt Pro 0.4 nozzle", "inherits": "fdm_belt_common", "from": "system", - "setting_id": "GMPC0BBP01", + "setting_id": "34OWINlJpJgA9DwQ", "instantiation": "true", "printer_model": "BabyBelt Pro", "printer_variant": "0.4", diff --git a/resources/profiles/Printcepts/machine/fdm_belt_common.json b/resources/profiles/Printcepts/machine/fdm_belt_common.json index b010742d78..fe3a58272e 100644 --- a/resources/profiles/Printcepts/machine/fdm_belt_common.json +++ b/resources/profiles/Printcepts/machine/fdm_belt_common.json @@ -9,7 +9,7 @@ "default_filament_profile": [ "Generic PLA @System" ], - "default_print_profile": "0.20mm Standard @System", + "default_print_profile": "0.20mm Standard @BabyBelt Pro", "max_layer_height": [ "0.32" ], diff --git a/resources/profiles/Printcepts/process/0.20mm Standard @BabyBelt Pro.json b/resources/profiles/Printcepts/process/0.20mm Standard @BabyBelt Pro.json index 7f7a810762..f592cfe87e 100644 --- a/resources/profiles/Printcepts/process/0.20mm Standard @BabyBelt Pro.json +++ b/resources/profiles/Printcepts/process/0.20mm Standard @BabyBelt Pro.json @@ -3,6 +3,7 @@ "name": "0.20mm Standard @BabyBelt Pro", "inherits": "fdm_process_common", "from": "system", + "setting_id": "JGfGtqX6CWjCt437", "instantiation": "true", "layer_height": "0.2", "initial_layer_print_height": "0.2", From 55b4dca9bc04e0db47febbe6f035e68951e585c8 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Thu, 6 Aug 2026 01:08:44 -0500 Subject: [PATCH 073/224] Belt printers: brim laid onto the tilted belt, with a leading apron A belt printer slices in a rotated frame, so the belt surface is a tilted plane rather than the Z=0 bed plane. Each slicing layer touches the belt only along a narrow strip at its leading edge - about 0.2mm at 45 degrees - so a part's first layer is really a first line, with almost no contact patch to hold it down while the belt drags it forward. Brim was hard-disabled on belt printers, leaving no remedy at all. Generate the brim on the belt plane instead. The object's belt footprint is the union over layers of each slice clipped to that layer's contact band; the brim is offset from it in a "flattened" frame where the shear axis is stretched by 1/cos(tilt), so ordinary Clipper offsets measure true on-belt distance. It is emitted as cross-belt lines, one per layer band, anchored to a fixed fraction of the band so every line shares a nozzle-to-belt clearance and therefore comes out the same width; flow is matched to the resulting band pitch, keeping the sheet uniform and gap-free. Three new controls, all belt-only: * Leading brim length - extends the brim ahead of the part along the belt, on every downhill-facing edge of its contact area. This apron necessarily prints BELOW the object's first layer, since layer 0 is the part's leading contact, so it needs brim-only bands of its own. * Extra brim width - widens the brim sideways across the belt only. * Brim type "Leading edge only" - brim at the part's first belt contact and nothing after it. Appended last in BrimType so no existing value shifts; degrades to an outer brim off belt printers, with a warning. The apron bands are lightweight records rather than a Layer subclass, so no fabricated Layer::id() can leak into initial-layer temperature selection, the spiral vase probe, cooling or gradual interpolation. They are generated in posSupportMaterial because their print_z values must exist before ToolOrdering is built at psWipeTower, and they are emitted from a short dedicated branch in process_layer that runs before any layer pointer is dereferenced. The footprint is closed before offsetting outwards: a belt contact patch is often a broken-up strip, and the merged offset rings of two islands closer than 2 x brim_width would otherwise fill the space between them - space that lies under the part. Also fixes a pre-existing bug where PrintObject::get_first_layer_bbox() overwrote a valid bbox with an unassigned one on any belt printer with a brim configured, because has_brim() was true while make_brim() returned early. Belt brim is refused alongside the prime tower and spiral vase, and requires one instance per PrintObject - translating an instance along the belt axis changes its physical belt-floor Z. Untilted belt printers are unchanged: they still get no brim, since the plate brim is emitted out of skirt_brim_groups(), which _make_skirt() never builds for a belt printer. --- src/libslic3r/BeltBrim.cpp | 491 ++++++++++++++++++ src/libslic3r/BeltBrim.hpp | 169 ++++++ src/libslic3r/Brim.cpp | 13 +- src/libslic3r/CMakeLists.txt | 2 + src/libslic3r/GCode.cpp | 145 +++++- src/libslic3r/GCode.hpp | 36 +- src/libslic3r/GCode/ToolOrdering.cpp | 45 +- src/libslic3r/GCode/ToolOrdering.hpp | 4 + src/libslic3r/Preset.cpp | 2 +- src/libslic3r/Print.cpp | 119 ++++- src/libslic3r/Print.hpp | 31 ++ src/libslic3r/PrintConfig.cpp | 42 ++ src/libslic3r/PrintConfig.hpp | 6 + src/libslic3r/PrintObject.cpp | 55 ++ src/libslic3r/Support/BeltFloorContext.cpp | 2 +- src/libslic3r/Support/BeltFloorContext.hpp | 6 + src/libslic3r/Support/SupportCommon.cpp | 4 +- .../Support/SupportSpotsGenerator.cpp | 4 +- src/slic3r/GUI/ConfigManipulation.cpp | 45 +- src/slic3r/GUI/GUI_Factories.cpp | 18 +- src/slic3r/GUI/Tab.cpp | 2 + tests/fff_print/test_skirt_brim.cpp | 192 +++++++ tests/libslic3r/CMakeLists.txt | 1 + tests/libslic3r/test_belt_brim.cpp | 341 ++++++++++++ 24 files changed, 1744 insertions(+), 31 deletions(-) create mode 100644 src/libslic3r/BeltBrim.cpp create mode 100644 src/libslic3r/BeltBrim.hpp create mode 100644 tests/libslic3r/test_belt_brim.cpp diff --git a/src/libslic3r/BeltBrim.cpp b/src/libslic3r/BeltBrim.cpp new file mode 100644 index 0000000000..40ecc60f58 --- /dev/null +++ b/src/libslic3r/BeltBrim.cpp @@ -0,0 +1,491 @@ +#include "BeltBrim.hpp" + +#include "ClipperUtils.hpp" +#include "Flow.hpp" +#include "Layer.hpp" +#include "Polygon.hpp" +#include "Print.hpp" +#include "ShortestPath.hpp" +#include "Support/BeltFloorContext.hpp" + +#include + +namespace Slic3r { + +// ---------------------------------------------------------------- scaling + +static inline Point scale_u_point(const Point &p, int from_axis, double factor) +{ + // llround, not a cast: casting truncates toward zero, so a round trip would + // walk every vertex toward the origin by up to one unit per pass. + return from_axis == 0 ? + Point(coord_t(std::llround(double(p.x()) * factor)), p.y()) : + Point(p.x(), coord_t(std::llround(double(p.y()) * factor))); +} + +static inline void scale_u_polygon(Polygon &poly, int from_axis, double factor) +{ + for (Point &p : poly.points) + p = scale_u_point(p, from_axis, factor); +} + +ExPolygons belt_scale_u(const ExPolygons &src, const BeltBrimFrame &frame, double factor) +{ + ExPolygons out = src; + for (ExPolygon &ex : out) { + scale_u_polygon(ex.contour, frame.from_axis, factor); + for (Polygon &hole : ex.holes) + scale_u_polygon(hole, frame.from_axis, factor); + } + return out; +} + +Polylines belt_scale_u(const Polylines &src, const BeltBrimFrame &frame, double factor) +{ + Polylines out = src; + for (Polyline &pl : out) + for (Point &p : pl.points) + p = scale_u_point(p, frame.from_axis, factor); + return out; +} + +// ---------------------------------------------------------------- sweep + +ExPolygons sweep_ex(const ExPolygons &src, const Point &t) +{ + if (src.empty()) + return {}; + if (t == Point(0, 0)) + return src; + + // One parallelogram per boundary edge. Together with P and P + t these + // cover the Minkowski sum exactly: for any q = p + s*t with p in P and + // s in [0, 1], let s* be the smallest lambda >= 0 with q - lambda*t in P. + // Either s* == 0 (so q is in P) or q - s* * t lies on some boundary edge e, + // putting q in that edge's parallelogram. Hole edges must be included, or + // holes narrower than t along t would wrongly survive the sweep. + Polygons quads; + for (const ExPolygon &ex : src) + for (size_t c = 0; c < ex.num_contours(); ++ c) + for (const Line &e : ex.contour_or_hole(c).lines()) { + if (e.a == e.b) + continue; + Polygon q; + q.points = { e.a, e.b, e.b + t, e.a + t }; + // The non-zero fill rule counts a clockwise ring as -1, which + // would punch a hole instead of adding material. Edges parallel + // to t give a zero-area quad; Clipper discards those harmlessly. + if (q.is_clockwise()) + q.reverse(); + quads.emplace_back(std::move(q)); + } + + ExPolygons shifted = src; + for (ExPolygon &ex : shifted) + ex.translate(t); + + // union_ex(ExPolygons, Polygons) uses pftNonZero, which is the fill rule the + // argument above relies on. + return union_ex(union_ex(src, shifted), quads); +} + +// ---------------------------------------------------------------- brim region + +ExPolygons belt_brim_region(const ExPolygons &footprint_flat, + bool has_outer, + bool has_inner, + coord_t brim_width, + coord_t object_gap, + coord_t leading, + coord_t lateral, + const BeltBrimFrame &frame) +{ + if (footprint_flat.empty() || (! has_outer && ! has_inner)) + return {}; + + ExPolygons out; + + if (has_outer) { + // Offset the outer ring from the contours only, so a hole cannot punch + // through it. Same reasoning as the plate brim in Brim.cpp. + Polygons contours; + contours.reserve(footprint_flat.size()); + for (const ExPolygon &ex : footprint_flat) + contours.emplace_back(ex.contour); + + // Inner and outer boundary offset from the same polygon, to avoid + // round-off mismatch between them. + ExPolygons inner = offset_ex(contours, float(object_gap), jtRound, SCALED_RESOLUTION); + + // Close the interior before offsetting outwards. A belt contact patch is often a + // narrow, broken-up strip, and the offset rings of two islands less than + // 2 x brim_width apart merge and fill the space between them - space that lies + // UNDER the part, which is not what "outer brim" means. Closing also swallows + // holes in the patch for the same reason. Concavity-filling only, so an apron or + // any other outward protrusion is untouched. + ExPolygons envelope = brim_width > 0 ? closing_ex(inner, float(brim_width)) : inner; + + ExPolygons base = envelope; + if (leading > 0) { + // Sweep downhill from the gapped keep-out, so the apron is contiguous with + // the ring instead of starting inside the gap. + const Point t = frame.from_axis == 0 ? + Point(frame.downhill_sign() * leading, 0) : + Point(0, frame.downhill_sign() * leading); + base = union_ex(base, sweep_ex(envelope, t)); + } + if (lateral > 0) { + // Across the belt, both ways. Swept from `base` so the apron is widened + // too, and in the flattened frame the cross-belt axis is unscaled, so this + // distance is already a true on-belt distance. + const Point t = frame.from_axis == 0 ? Point(0, lateral) : Point(lateral, 0); + ExPolygons widened = union_ex(sweep_ex(base, t), sweep_ex(base, Point(-t.x(), -t.y()))); + base = union_ex(base, to_polygons(widened)); + } + ExPolygons outer = offset_ex(base, float(brim_width), jtRound, SCALED_RESOLUTION); + expolygons_append(out, diff_ex(outer, envelope)); + } + + if (has_inner) { + // Holes reversed so a negative offset grows inward, mirroring Brim.cpp. + // No apron here: an apron growing into a hole interior is never useful. + Polygons holes; + for (const ExPolygon &ex : footprint_flat) + polygons_append(holes, ex.holes); + polygons_reverse(holes); + if (! holes.empty()) { + ExPolygons hole_inner = offset_ex(holes, - float(brim_width + object_gap)); + ExPolygons hole_outer = offset_ex(holes, - float(object_gap)); + expolygons_append(out, intersection_ex(diff_ex(hole_outer, hole_inner), holes)); + } + } + + return union_ex(out); +} + +// ---------------------------------------------------------------- line lattice + +std::vector belt_brim_line_positions(coord_t u_lo, + coord_t u_hi, + coord_t pitch_u, + coord_t u_anchor) +{ + std::vector out; + if (pitch_u <= 0 || u_hi <= u_lo) + return out; + + // Walk the lattice from just below u_lo. Integer arithmetic throughout, so + // the half-open interval needs no epsilon: a point landing exactly on u_hi + // belongs to the next band. + int64_t k = int64_t(std::floor(double(u_lo - u_anchor) / double(pitch_u))) - 1; + while (u_anchor + coord_t(k) * pitch_u < u_lo) + ++ k; + for (;; ++ k) { + const coord_t u = u_anchor + coord_t(k) * pitch_u; + if (u >= u_hi) + break; + out.emplace_back(u); + } + return out; +} + +// ---------------------------------------------------------------- pipeline + +// A band of the belt surface as an explicit box, clamped to `bounds` along the +// shear axis. Deliberately not BeltFloorContext::surface_polygon(): those +// half-planes span +-1000 mm, which is wasteful to clip against and dangerous to +// feed through the flattening scale. +static Polygon band_box(const BoundingBox &bounds, int from_axis, coordf_t u_lo, coordf_t u_hi) +{ + coord_t lo = scale_(u_lo); + coord_t hi = scale_(u_hi); + const coord_t bmin = from_axis == 0 ? bounds.min.x() : bounds.min.y(); + const coord_t bmax = from_axis == 0 ? bounds.max.x() : bounds.max.y(); + lo = std::max(lo, bmin); + hi = std::min(hi, bmax); + Polygon poly; + if (hi <= lo) + return poly; + if (from_axis == 0) + poly.points = { Point(lo, bounds.min.y()), Point(hi, bounds.min.y()), + Point(hi, bounds.max.y()), Point(lo, bounds.max.y()) }; + else + poly.points = { Point(bounds.min.x(), lo), Point(bounds.max.x(), lo), + Point(bounds.max.x(), hi), Point(bounds.min.x(), hi) }; + return poly; +} + +// Everything the per-band line generator needs, gathered once per object. +struct BeltBrimContext +{ + BeltFloorContext ctx; + BeltBrimFrame frame; + ExPolygons region; // brim region, object-local slicing XY + BoundingBox region_bbox; + Flow brim_flow; + coord_t pitch_u = 0; + coord_t u_anchor = 0; + double in_plane_pitch = 0.; // mm +}; + +// Emit the cross-belt brim lines that belong to the band [print_z - height, print_z]. +static void belt_brim_band_paths(const BeltBrimContext &bc, + coordf_t print_z, + coordf_t height, + const Polygons &obstacles, + ExtrusionEntityCollection &out, + ExPolygons &areas_out) +{ + coordf_t u_lo = bc.ctx.cutoff_u(print_z - height); + coordf_t u_hi = bc.ctx.cutoff_u(print_z); + if (u_lo > u_hi) + std::swap(u_lo, u_hi); + + // How wide this band is measured ON the belt, versus one nominal bead. + const double band_in_plane = (u_hi - u_lo) * bc.frame.u_stretch(); + + // Fraction of the layer height at which a line sits above the belt. Toward the + // downhill edge, so the sheet is reasonably thick while the nozzle stays clear of + // the belt itself. + static constexpr double BAND_CLEARANCE_FRACTION = 0.75; + + std::vector us; + double uniform_clearance = 0.; // 0 => derive per line from its own position + double line_pitch = bc.in_plane_pitch; + if (band_in_plane <= bc.in_plane_pitch + EPSILON) { + // Steep belt, which is the normal case: the band is narrower than one bead, so + // exactly one line fits. Place it at a FIXED fraction of the band rather than + // on a nominal-spacing lattice. On a lattice each line lands at an arbitrary + // point in its band, the clearance sweeps [0, height] from band to band, and the + // bead width therefore varies by 2x - visible as ragged, uneven brim lines. + // Anchoring to the band makes the clearance identical everywhere, so every bead + // is the same width. + // + // The spacing is then whatever the bands give (height / sin(tilt) on the belt) + // rather than the nominal bead spacing, so the flow below is matched to THAT + // pitch. Matched flow at the real pitch is what keeps the sheet uniform and + // gap-free; using nominal flow at band spacing would over-feed it. + us.push_back(scale_(bc.ctx.cutoff_u(print_z - BAND_CLEARANCE_FRACTION * height))); + uniform_clearance = BAND_CLEARANCE_FRACTION * height; + line_pitch = band_in_plane; + } else { + // Shallow belt: the band is wider than a bead, so it takes several lines and they + // have to sit on the nominal lattice. Their clearances then differ, and so do + // their widths - unavoidable here, but shallow belts are the rare case. + us = belt_brim_line_positions(scale_(u_lo), scale_(u_hi), bc.pitch_u, bc.u_anchor); + } + if (us.empty()) + return; + + const Polygons region_polys = to_polygons(bc.region); + + // One lattice line at a time: the clearance - and therefore the extrusion + // volume - is a property of the line's u, so the pieces of different lines + // must not be pooled before the flow is resolved. + // Overshoot the region so the clip, not the line's ends, decides the extent. + const coord_t margin = coord_t(SCALED_EPSILON) + 1; + for (const coord_t u : us) { + Polyline line; + if (bc.frame.from_axis == 0) + line.points = { Point(u, coord_t(bc.region_bbox.min.y() - margin)), + Point(u, coord_t(bc.region_bbox.max.y() + margin)) }; + else + line.points = { Point(coord_t(bc.region_bbox.min.x() - margin), u), + Point(coord_t(bc.region_bbox.max.x() + margin), u) }; + + Polylines pieces = intersection_pl(Polylines{ line }, region_polys); + if (! obstacles.empty()) + pieces = diff_pl(pieces, obstacles); + if (pieces.empty()) + continue; + + // Nozzle-to-belt clearance for this line. Constant along the line, because the + // belt height depends only on the shear-axis coordinate. Band-anchored lines + // share one clearance by construction; lattice lines (shallow belts) each get + // their own, clamped so neither end of a band yields an unprintable bead. + double clearance = uniform_clearance; + if (clearance <= 0.) { + const Point probe = bc.frame.from_axis == 0 ? Point(u, 0) : Point(0, u); + clearance = print_z - bc.ctx.floor_print_z(probe); + clearance = std::min(std::max(clearance, 0.5 * height), height); + } + + // with_cross_section, not with_height: it reaches the prescribed volume while + // KEEPING the extrusion spacing, so the bead is sized to fill exactly one + // pitch x clearance cell of the sheet. + const Flow f = bc.brim_flow.with_cross_section(float(line_pitch * clearance)); + + // Footprint of these beads, for the first-layer convex hull and bbox. + for (const Polygon &p : offset(pieces, 0.5f * float(f.scaled_width()))) + areas_out.emplace_back(ExPolygon(p)); + + extrusion_entities_append_paths(out.entities, chain_polylines(std::move(pieces)), + erBrim, f.mm3_per_mm(), f.width(), float(clearance)); + } +} + +// Union of everything actually extruded at `print_z` by any object, expressed in +// `self`'s local slicing frame. Includes `self` itself: its slice at this Z can +// overhang outside the belt footprint and land in the brim ring, which the +// flattened brim_object_gap - a belt-plane separation - does not cover. +static Polygons belt_brim_obstacles(const Print &print, const PrintObject &self, coordf_t print_z, coordf_t tol) +{ + const Point shift_self = self.instances().empty() ? Point(0, 0) + : self.instances().front().shift_without_plate_offset(); + Polygons out; + for (const PrintObject *o : print.objects()) + for (const PrintInstance &inst : o->instances()) { + const Point delta = inst.shift_without_plate_offset() - shift_self; + if (const Layer *l = o->get_layer_at_printz(print_z, tol)) { + Polygons ps = to_polygons(l->lslices); + for (Polygon &p : ps) + p.translate(delta); + polygons_append(out, std::move(ps)); + } + if (const SupportLayer *sl = o->get_support_layer_at_printz(print_z, tol)) { + Polygons ps = sl->support_fills.polygons_covered_by_spacing(); + for (Polygon &p : ps) + p.translate(delta); + polygons_append(out, std::move(ps)); + } + } + return union_(out); +} + +void make_belt_brim(PrintObject &object) +{ + object.clear_belt_brim(); + if (! object.has_belt_brim()) + return; + + const Print &print = *object.print(); + BeltBrimContext bc; + if (! bc.ctx.init(object.slicing_parameters(), print.config())) + return; + bc.frame = BeltBrimFrame{ bc.ctx.shear_factor(), bc.ctx.from_axis() }; + + const size_t nlayers = object.layers().size(); + if (nlayers == 0) + return; + + // 1. Belt footprint: the union of each layer's slice clipped to that layer's + // own contact band. This is the object's bottom face, which on a belt is + // spread over every layer instead of sitting in layer 0. + ExPolygons footprint_acc; + for (size_t i = 0; i < nlayers; ++ i) { + const Layer &layer = *object.layers()[i]; + if (layer.lslices.empty()) + continue; + // print_z - height, not the previous layer's print_z: variable layer + // heights make the latter wrong. + coordf_t u_lo = bc.ctx.cutoff_u(layer.print_z - layer.height); + coordf_t u_hi = bc.ctx.cutoff_u(layer.print_z); + if (u_lo > u_hi) + std::swap(u_lo, u_hi); + BoundingBox bb = get_extents(layer.lslices); + bb.offset(scale_(1.)); + const Polygon band = band_box(bb, bc.frame.from_axis, u_lo, u_hi); + if (band.empty()) + continue; + expolygons_append(footprint_acc, intersection_ex(layer.lslices, Polygons{ band })); + } + const ExPolygons footprint = union_ex(footprint_acc); + if (footprint.empty()) + return; + + // 2. Brim region, offset in the flattened (true on-belt) metric. + const PrintObjectConfig &cfg = object.config(); + bc.brim_flow = print.brim_flow(); + const double flow_w = bc.brim_flow.scaled_spacing() * SCALING_FACTOR; + // Quantize to an even number of lines, as the plate brim does. + const coord_t width = scale_(std::floor(cfg.brim_width.value / flow_w / 2) * flow_w * 2); + const coord_t leading = scale_(cfg.leading_brim_length.value); + const coord_t lateral = scale_(cfg.extra_brim_width.value); + const coord_t gap = scale_(cfg.brim_object_gap.value); + + // Belt printers collapse Auto / Mouse ear / Painted to outer-only: the auto width + // heuristic and flat ear discs have no meaning on a tilted plane. Leading-edge-only + // is an outer brim too; it is narrowed down to the first contact below. + const BrimType bt = cfg.brim_type.value; + const bool has_outer = bt == btOuterOnly || bt == btOuterAndInner + || bt == btAutoBrim || bt == btEar || bt == btPainted + || bt == btLeadingEdgeOnly; + const bool has_inner = bt == btInnerOnly || bt == btOuterAndInner; + + bc.region = belt_unflatten( + belt_brim_region(belt_flatten(footprint, bc.frame), has_outer, has_inner, + width, gap, leading, lateral, bc.frame), + bc.frame); + + if (bt == btLeadingEdgeOnly && ! bc.region.empty()) { + // Keep only what lies at or downhill of the object's FIRST contact with the + // belt, so the part is supported as it lands and nothing is printed alongside + // it afterwards. The cut is the uphill edge of the first layer's contact band: + // everything past it belongs to later contacts. + const coordf_t u_cut = bc.ctx.cutoff_u(object.layers().front()->print_z); + BoundingBox keep_bb = get_extents(bc.region); + keep_bb.offset(scale_(1.)); + const bool low_side = bc.frame.shear > 0.; // downhill is -u + const Polygon keep = band_box(keep_bb, bc.frame.from_axis, + low_side ? unscale(bc.frame.from_axis == 0 ? keep_bb.min.x() : keep_bb.min.y()) : u_cut, + low_side ? u_cut : unscale(bc.frame.from_axis == 0 ? keep_bb.max.x() : keep_bb.max.y())); + bc.region = keep.empty() ? ExPolygons{} : intersection_ex(bc.region, Polygons{ keep }); + } + + if (bc.region.empty()) + return; + bc.region_bbox = get_extents(bc.region); + + // 3. Line lattice. Fixed pitch in the flattened metric, anchored at the + // footprint's leading-most edge so lines stay collinear across + // disconnected islands and across the apron prologue. + bc.pitch_u = std::max(1, coord_t(bc.brim_flow.scaled_spacing() * bc.frame.cos_tilt())); + bc.in_plane_pitch = unscale(bc.pitch_u) * bc.frame.u_stretch(); + { + const BoundingBox fbb = get_extents(footprint); + const bool low_side = bc.frame.shear > 0.; + bc.u_anchor = bc.frame.from_axis == 0 ? (low_side ? fbb.min.x() : fbb.max.x()) + : (low_side ? fbb.min.y() : fbb.max.y()); + } + + // 4. Bands coincident with an object layer. + std::vector by_layer(nlayers); + std::vector areas_by_layer(nlayers); + for (size_t i = 0; i < nlayers; ++ i) { + const Layer &layer = *object.layers()[i]; + const Polygons obstacles = belt_brim_obstacles(print, object, layer.print_z, 0.5 * layer.height); + belt_brim_band_paths(bc, layer.print_z, layer.height, obstacles, by_layer[i], areas_by_layer[i]); + } + + // 5. Apron prologue: the part of the region downhill of the object's first + // layer, which has no object layer to ride on. + std::vector prologue; + { + const Layer &first = *object.layers().front(); + const coordf_t h = first.height; + const bool low_side = bc.frame.shear > 0.; + const coord_t u_lead_s = bc.frame.from_axis == 0 + ? (low_side ? bc.region_bbox.min.x() : bc.region_bbox.max.x()) + : (low_side ? bc.region_bbox.min.y() : bc.region_bbox.max.y()); + const coordf_t u_lead = unscale(u_lead_s); + // print_z at which the belt surface crosses the region's leading edge. + const coordf_t z_lead = bc.ctx.shear_factor() * u_lead + + bc.ctx.floor_offset() + bc.ctx.z_shift(); + if (h > EPSILON) + for (coordf_t z = first.print_z - h; z > z_lead - h; z -= h) { + const Polygons obstacles = belt_brim_obstacles(print, object, z, 0.5 * h); + BeltBrimBand band; + band.print_z = z; + band.height = h; + belt_brim_band_paths(bc, z, h, obstacles, band.fills, band.areas); + if (! band.fills.empty()) + prologue.emplace_back(std::move(band)); + } + // Lowest Z first, so collect_layers_to_print sees them in print order. + std::reverse(prologue.begin(), prologue.end()); + } + + object.set_belt_brim(std::move(by_layer), std::move(areas_by_layer), std::move(prologue)); +} + +} // namespace Slic3r diff --git a/src/libslic3r/BeltBrim.hpp b/src/libslic3r/BeltBrim.hpp new file mode 100644 index 0000000000..d6f0980bc8 --- /dev/null +++ b/src/libslic3r/BeltBrim.hpp @@ -0,0 +1,169 @@ +#ifndef slic3r_BeltBrim_hpp_ +#define slic3r_BeltBrim_hpp_ + +#include "ExPolygon.hpp" +#include "ExtrusionEntityCollection.hpp" +#include "Point.hpp" +#include "Polyline.hpp" + +#include +#include + +// Belt-printer brim geometry. +// +// A belt printer slices in a ROTATED frame, so the belt surface is not the +// Z=0 bed plane but a tilted plane in slicing space: +// +// z_slicing(u) = shear * u + floor_offset + z_shift, u = X or Y +// +// where `shear == tan(tilt)` (SlicingParameters::belt_floor_shear_factor) and +// the axis is selected by SlicingParameters::belt_floor_from_axis. See +// Support/BeltFloorContext.hpp for the canonical accessors. +// +// Consequences that drive everything in this file: +// +// * A horizontal slicing layer touches the belt only along a narrow strip at +// its leading edge, `layer_height / shear` wide (~0.2 mm at 45 degrees). +// The object's belt footprint - its bottom face - is therefore spread over +// every layer, not contained in layer 0. +// * Distances measured in slicing XY are NOT on-belt distances: moving `du` +// along the shear axis travels `du / cos(tilt)` across the belt. So brim +// offsets have to be taken in a "flattened" space where the shear axis is +// stretched by `1 / cos(tilt)`, then mapped back. +// * Brim ahead of the part (downhill) lies at slicing Z BELOW the object's +// first layer, because the object's layer 0 is precisely its leading +// contact with the belt. +// +// Everything here is pure geometry on ExPolygons/Polylines so it can be unit +// tested without a Print. Keep user-visible strings out of this file: it is +// not listed in localization/i18n/list.txt. + +namespace Slic3r { + +// Tilt window within which the BELT plane, not the bed plane, is the adhesion +// surface. Below ~1 degree a belt is a flat bed as far as adhesion goes, and the +// contact band would be layer_height/sin(tilt) - tens of millimetres - so the +// ordinary plate brim is both correct and cheaper. Above ~85 degrees the whole +// brim compresses into a sliver and is not worth generating. +inline constexpr double BELT_BRIM_MIN_TILT_DEG = 1.; +inline constexpr double BELT_BRIM_MAX_TILT_DEG = 85.; + +// Description of the tilted belt plane, reduced to what the brim geometry needs. +struct BeltBrimFrame +{ + // tan(tilt). Sign selects which way is downhill. + double shear = 0.; + // 0 = X, 1 = Y. Matches BeltFloorContext::from_axis(). + int from_axis = 1; + + // 1 / cos(tilt). Stretch factor that turns a projected distance along + // `from_axis` into the true distance travelled across the belt. + double u_stretch() const { return std::sqrt(1. + shear * shear); } + // cos(tilt). The inverse mapping. + double cos_tilt() const { return 1. / this->u_stretch(); } + // Downhill is where the belt surface is lower, i.e. printed earlier, i.e. + // the leading edge of the part. For shear > 0 that is -u. + int downhill_sign() const { return shear > 0. ? -1 : +1; } +}; + +// Scale only the `from_axis` component by `factor`, rounding to nearest. +// +// Deliberately not MultiPoint::scale(fx, fy) / ExPolygon::scale(fx, fy): those +// truncate toward zero, which is asymmetric about the origin and loses up to a +// full coordinate unit per vertex on every round trip. +ExPolygons belt_scale_u(const ExPolygons &src, const BeltBrimFrame &frame, double factor); +Polylines belt_scale_u(const Polylines &src, const BeltBrimFrame &frame, double factor); + +// Into / out of the space where Euclidean offsets equal true on-belt distances. +inline ExPolygons belt_flatten(const ExPolygons &src, const BeltBrimFrame &frame) + { return belt_scale_u(src, frame, frame.u_stretch()); } +inline ExPolygons belt_unflatten(const ExPolygons &src, const BeltBrimFrame &frame) + { return belt_scale_u(src, frame, frame.cos_tilt()); } + +// Minkowski sum of `src` with the segment [0, t]: the region swept by sliding +// `src` along t. Used to grow the brim downhill for "extra brim width". +// +// Implemented as union_(P, P + t, {parallelogram per boundary edge}) over ALL +// contours including holes, with every parallelogram forced counter-clockwise +// so the non-zero fill rule closes holes narrower than t along the sweep +// direction. A hole survives exactly when it is wider than |t| measured along +// t - not when it is wider in its narrowest Euclidean direction. +ExPolygons sweep_ex(const ExPolygons &src, const Point &t); + +// Brim region for one already-flattened belt footprint. All lengths are scaled +// and measured in the flattened (true on-belt) metric. +// +// `has_outer` / `has_inner` are the resolved BrimType: belt printers collapse +// Auto / Mouse ear / Painted to outer-only, so the caller does that mapping and +// this function never needs PrintConfig. +// +// Two directional extras are applied to the footprint before the outer offset, so +// each one buys reach in one direction only: +// +// `leading` (leading_brim_length) sweeps the footprint DOWNHILL along the belt, +// so every leading-facing edge gains an apron ahead of it. +// `lateral` (extra_brim_width) sweeps it BOTH WAYS across the belt, widening +// the brim sideways without pushing it further ahead or behind. +// +// Neither is applied to the inner (hole) ring. +ExPolygons belt_brim_region(const ExPolygons &footprint_flat, + bool has_outer, + bool has_inner, + coord_t brim_width, + coord_t object_gap, + coord_t leading, + coord_t lateral, + const BeltBrimFrame &frame); + +// Brim line positions for one layer band. +// +// Lines sit on a fixed lattice `u_anchor + k * pitch_u` so the on-belt spacing +// between neighbouring brim lines is constant regardless of how the lattice +// falls across layer bands. Snapping to band centres instead would quantise +// the spacing to whole bands and under-deposit by ~35% at 45 degrees. +// +// The band is half-open, [u_lo, u_hi), so every lattice point belongs to +// exactly one band: none duplicated at a boundary, none dropped. A band +// narrower than the pitch simply yields nothing; a band much wider (shallow +// tilt) yields several lines. +std::vector belt_brim_line_positions(coord_t u_lo, + coord_t u_hi, + coord_t pitch_u, + coord_t u_anchor); + +// ---------------------------------------------------------------- pipeline + +// One brim-only layer printed BEFORE the object's first layer, carrying the +// apron that has to be stuck to the belt ahead of the part. +// +// Deliberately not a Layer subclass. A synthetic Layer would inherit id() +// semantics that leak into initial-layer temperature selection, the spiral vase +// probe, gradual interpolation, avoid-crossing-perimeters and cooling, all of +// which key off Layer::id() == 0 or off a layer's regions. A plain record +// carries only what the emitter needs. +// +// `height` is the LAYER height, used for the Z move and ordering metadata only. +// Each extrusion path inside `fills` carries its own height, equal to that +// line's nozzle-to-belt clearance, which varies across the band. +struct BeltBrimBand +{ + coordf_t print_z = 0.; + coordf_t height = 0.; + // erBrim paths in the object's local slicing frame, untranslated. + ExtrusionEntityCollection fills; + // Footprint of those paths, for the first-layer convex hull / bbox. + ExPolygons areas; +}; + +class PrintObject; + +// Generate the belt brim for one object: fills its per-object-layer bands and +// its apron prologue. No-op unless PrintObject::has_belt_brim(). +// +// Runs inside posSupportMaterial rather than the brim step, because the prologue +// print_z values must exist before ToolOrdering is built at psWipeTower. +void make_belt_brim(PrintObject &object); + +} // namespace Slic3r + +#endif // slic3r_BeltBrim_hpp_ diff --git a/src/libslic3r/Brim.cpp b/src/libslic3r/Brim.cpp index 13a6aa819a..1fd71ab13a 100644 --- a/src/libslic3r/Brim.cpp +++ b/src/libslic3r/Brim.cpp @@ -453,7 +453,9 @@ static ExPolygons outer_inner_brim_area(const Print& print, const bool use_auto_brim_ears = object->config().brim_type == btEar; const bool use_brim_ears = object->config().brim_type == btPainted; const bool has_inner_brim = brim_type == btInnerOnly || brim_type == btOuterAndInner || use_auto_brim_ears || use_brim_ears; - const bool has_outer_brim = brim_type == btOuterOnly || brim_type == btOuterAndInner || brim_type == btAutoBrim || use_auto_brim_ears || use_brim_ears; + // btLeadingEdgeOnly is a belt-printer mode; on a flat bed there is no leading + // edge, so it degrades to an ordinary outer brim rather than silently to none. + const bool has_outer_brim = brim_type == btOuterOnly || brim_type == btOuterAndInner || brim_type == btAutoBrim || brim_type == btLeadingEdgeOnly || use_auto_brim_ears || use_brim_ears; coord_t ear_detection_length = scale_(object->config().brim_ears_detection_length.value); coordf_t brim_ears_max_angle = object->config().brim_ears_max_angle.value; //ORCA: Select brim base slices from EFC-compensated outline when enabled. @@ -868,7 +870,14 @@ void make_brim(const Print& print, PrintTryCancel try_cancel, Polygons& islands_ std::vector& printExtruders, std::map* objectBrimAreasByInstanceOut) { - // Belt printer: brim is not compatible with belt printing. + // Belt printers never use the flat plate brim. + // + // With a tilted belt the brim has to be laid onto the belt plane over many layers, + // which BeltBrim.cpp does during posSupportMaterial. With an untilted belt this + // could in principle fall through and produce an ordinary brim, but it would never + // reach the G-code: the plate brim is emitted out of skirt_brim_groups(), which + // _make_skirt() builds, and that returns early for every belt printer. Running the + // generator anyway would just burn time on geometry nobody prints. if (print.config().belt_printer.value) return; diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index e3f00179ff..c8796265db 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -80,6 +80,8 @@ set(lisbslic3r_sources BoundingBox.hpp BridgeDetector.cpp BridgeDetector.hpp + BeltBrim.cpp + BeltBrim.hpp BeltGCode.cpp BeltGCode.hpp BeltGCodeWriter.cpp diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index c85ed1159e..f2caca404f 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2125,6 +2125,17 @@ std::vector GCode::collect_layers_to_print(const PrintObjec std::vector> warning_ranges; + // Belt printers: the brim apron is stuck to the belt AHEAD of the part, which + // on a tilted belt means below the object's first layer. Those bands carry no + // object or support layer, so they are emitted first and handled by + // process_layer()'s brim-only branch. Already ordered lowest print_z first. + for (const BeltBrimBand &band : object.belt_brim_prologue()) { + LayerToPrint prologue_layer; + prologue_layer.belt_brim_band = &band; + prologue_layer.original_object = &object; + layers_to_print.push_back(prologue_layer); + } + // Pair the object layers with the support layers by z. size_t idx_object_layer = 0; size_t idx_support_layer = 0; @@ -2967,6 +2978,9 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato zs.push_back(layer->print_z); for (auto layer : object->support_layers()) zs.push_back(layer->print_z); + // Belt brim apron bands each get their own change_layer() call. + for (const BeltBrimBand &band : object->belt_brim_prologue()) + zs.push_back(band.print_z); std::sort(zs.begin(), zs.end()); //BBS: merge numerically very close Z values. auto end_it = std::unique(zs.begin(), zs.end()); @@ -2986,6 +3000,9 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato zs.push_back(layer->print_z); for (auto layer : object->support_layers()) zs.push_back(layer->print_z); + // See the ByObject branch: apron bands are real printed layers. + for (const BeltBrimBand &band : object->belt_brim_prologue()) + zs.push_back(band.print_z); } if (!zs.empty()) { @@ -5163,8 +5180,39 @@ std::string GCode::generate_object_skirt_group(const Print &print, object_skirt_tools, layer, extruder_id, m_skirt_group_done[group_idx]); } -std::string GCode::generate_object_brim(const Print &print, const PrintObject &object, size_t instance_id, bool first_layer) +std::string GCode::generate_object_brim(const Print &print, const PrintObject &object, size_t instance_id, bool first_layer, + const Layer *object_layer) { + // Belt printers lay the brim onto the tilted belt over many layers, so there is + // nothing special about the first one. The bands that coincide with an object + // layer are emitted here; those below the object's first layer are apron and go + // through process_belt_brim_layer() instead. + if (object.has_belt_brim()) { + if (object_layer == nullptr) + return {}; + const std::vector &by_layer = object.belt_brim_by_layer(); + const size_t layer_idx = object_layer->id(); + if (layer_idx >= by_layer.size() || by_layer[layer_idx].empty()) + return {}; + std::string gcode; + // The band geometry is in the object's local slicing frame, exactly like its + // perimeters, so it needs this instance's origin. The caller does not set it + // until later, and the plate brim path deliberately uses (0, 0) because its + // geometry is already in plate coordinates. + m_config.apply(print.default_region_config()); + m_config.apply(object.config(), true); + const Point &offset = object.instances()[instance_id].shift; + this->set_origin(unscale(offset)); + this->on_set_origin(&object, offset); + m_avoid_crossing_perimeters.use_external_mp(); + for (const ExtrusionEntity *ee : by_layer[layer_idx].entities) + if (ee != nullptr) + gcode += this->extrude_entity(*ee, "brim", NOZZLE_CONFIG(support_speed)); + m_avoid_crossing_perimeters.use_external_mp(false); + m_avoid_crossing_perimeters.disable_once(); + return gcode; + } + if (!first_layer) return {}; @@ -5201,6 +5249,86 @@ std::string GCode::generate_object_brim(const Print &print, const PrintObject &o return {}; } +// Belt printers: emit one brim-only apron layer. On a tilted belt the brim ahead +// of the part lands at slicing Z below the object's first layer, because the +// object's layer 0 IS its leading contact with the belt. Those layers carry brim +// and nothing else. +// +// This is intentionally a short path rather than a variant of process_layer(): an +// 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. +LayerResult GCode::process_belt_brim_layer( + const Print &print, + const std::vector &layers, + const LayerTools &layer_tools, + const bool last_layer, + const size_t single_object_instance_idx) +{ + // layer_id 0: the apron precedes every object layer and nothing downstream + // indexes by it. spiral_vase_enable false: spiral vase is refused alongside + // belt brim in Print::validate(). cooling_buffer_flush true: an apron layer is + // a complete layer, and the default (object_layer || raft_layer || last_layer) + // would be false here, so fan and slowdown would never be applied to it. + LayerResult result { {}, 0, false, true }; + if (layer_tools.extruders.empty()) + // Nothing to extrude. + return result; + + coordf_t print_z = 0.; + for (const LayerToPrint <p : layers) + if (ltp.belt_brim_band != nullptr) { + print_z = ltp.belt_brim_band->print_z; + break; + } + + m_cur_layer_idx = m_belt_brim_layer_idx ++; + + // Publish the band's Z for _extrude()'s first-layer-plane probe, and make sure + // it cannot leak past this layer even if an extrusion throws. + struct BeltBrimZGuard { + std::optional &slot; + ~BeltBrimZGuard() { slot.reset(); } + } z_guard { m_belt_brim_z }; + m_belt_brim_z = print_z; + m_layer = nullptr; + + std::string gcode; + const unsigned int extruder_id = layer_tools.extruders.front(); + if (m_writer->filament() == nullptr || m_writer->filament()->id() != extruder_id) + gcode += this->set_extruder(extruder_id, print_z); + gcode += this->change_layer(print_z); + + for (const LayerToPrint <p : layers) { + const BeltBrimBand *band = ltp.belt_brim_band; + if (band == nullptr || band->fills.empty() || ltp.original_object == nullptr) + continue; + const PrintObject &object = *ltp.original_object; + // Speeds, flow and retraction all read m_config. + m_config.apply(print.default_region_config()); + m_config.apply(object.config(), true); + const size_t i_begin = single_object_instance_idx == size_t(-1) ? 0 : single_object_instance_idx; + const size_t i_end = single_object_instance_idx == size_t(-1) ? object.instances().size() + : single_object_instance_idx + 1; + for (size_t i = i_begin; i < i_end && i < object.instances().size(); ++ i) { + // Band geometry is object-local, like the object's own extrusions. + const Point &offset = object.instances()[i].shift; + this->set_origin(unscale(offset)); + this->on_set_origin(&object, offset); + m_avoid_crossing_perimeters.use_external_mp(); + for (const ExtrusionEntity *ee : band->fills.entities) + if (ee != nullptr) + gcode += this->extrude_entity(*ee, "brim", NOZZLE_CONFIG(support_speed)); + m_avoid_crossing_perimeters.use_external_mp(false); + m_avoid_crossing_perimeters.disable_once(); + } + } + + result.gcode = std::move(gcode); + return result; +} + // Bedslinger model. The heavier the bed load, the lower the achievable Y acceleration for a given // drive force (a = F / (bed_mass + printed_mass)). Reads machine_max_force_Y / machine_bed_mass_Y (both // default 0, i.e. absent on every existing printer), in which case it just returns the min configured Y @@ -5523,6 +5651,13 @@ LayerResult GCode::process_layer( } } + // Belt printers: a brim-only apron layer has neither an object nor a support + // layer, so it must be handled before layer_ptr is dereferenced below. + if (object_layer == nullptr && support_layer == nullptr && + std::any_of(layers.begin(), layers.end(), + [](const LayerToPrint &l) { return l.belt_brim_band != nullptr; })) + return this->process_belt_brim_layer(print, layers, layer_tools, last_layer, single_object_instance_idx); + const Layer* layer_ptr = nullptr; if (object_layer != nullptr) layer_ptr = object_layer; @@ -6481,7 +6616,8 @@ LayerResult GCode::process_layer( const LayerToPrint &layer_to_print = layers[instance_to_print.layer_id]; if (visit.first_visit && print_wipe_extrusions == (is_anything_overridden ? 1 : 0)) { gcode += generate_object_skirt_group(print, instance_to_print.print_object, instance_to_print.instance_id, layer_tools, layer, extruder_id); - gcode += generate_object_brim(print, instance_to_print.print_object, instance_to_print.instance_id, first_layer); + gcode += generate_object_brim(print, instance_to_print.print_object, instance_to_print.instance_id, first_layer, + layer_to_print.object_layer); } // To control print speed of the 1st object layer printed over raft interface. @@ -7591,10 +7727,13 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // evaluator is inactive (non-belt printers, or belt printers without // a Z-axis shear) `path_on_first_layer` falls back to the legacy // layer-id check, so behavior is bit-identical to the pre-feature path. + // A belt brim apron band has no Layer of its own, so it publishes its Z + // through m_belt_brim_z instead; without that the plane would be probed at + // Z=0 and the apron mis-classified for fan and speed. const Vec3d path_point_mm{ unscale(path.first_point().x()), unscale(path.first_point().y()), - m_layer ? m_layer->print_z : 0.0 + m_layer ? m_layer->print_z : (m_belt_brim_z ? *m_belt_brim_z : 0.0) }; const bool path_on_first_layer = this->on_first_layer(path_point_mm); diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 8655683727..d4c7bf9bc3 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -33,6 +33,7 @@ #include #include +#include #include #include #include @@ -290,6 +291,13 @@ public: const Layer* object_layer; const SupportLayer* support_layer; const PrintObject* original_object; //BBS: used for shared object logic + // Belt printers only: an apron band that prints BELOW the object's first + // layer, so it has no object or support layer of its own. Deliberately + // not a Layer, so it cannot leak Layer::id() semantics into initial-layer + // temperature, spiral vase, cooling or interpolation logic. When this is + // the only thing set, layer() is null and process_layer() takes its + // dedicated brim-only branch. + const BeltBrimBand* belt_brim_band { nullptr }; const Layer* layer() const { if (object_layer != nullptr) @@ -319,6 +327,12 @@ public: count++; } + // A brim-only apron band contributes no object/support layer, and + // averaging zero terms would yield NaN. Never folded into the + // average, so the non-belt result is bit-identical. + if (count == 0 && belt_brim_band != nullptr) + return belt_brim_band->print_z; + return sum_z / count; } }; @@ -389,7 +403,20 @@ protected: std::string generate_object_brim(const Print &print, const PrintObject &object, size_t instance_id, - bool first_layer); + bool first_layer, + const Layer *object_layer); + + // Belt printers: emit one brim-only apron layer. These print below the + // object's first layer, so there is no object or support layer for the normal + // process_layer() machinery to work from. Kept to the minimum a layer needs - + // tool, Z move, extrusions - so that nothing here can perturb the + // Layer::id()-based logic the ordinary path relies on. + LayerResult process_belt_brim_layer( + const Print &print, + const std::vector &layers, + const LayerTools &layer_tools, + const bool last_layer, + const size_t single_object_instance_idx); LayerResult process_layer( const Print &print, @@ -780,6 +807,13 @@ protected: // resolvers. Distinct from m_layer_index (an export progress counter starting at -1). size_t m_cur_layer_idx{0}; + // Belt brim apron layers only. They have no Layer, so the print_z that + // _extrude() needs for the first-layer-plane probe is published here instead. + // Scoped by BeltBrimZGuard in process_belt_brim_layer(), never left set. + std::optional m_belt_brim_z; + // Counter standing in for Layer::id() on apron layers, which precede layer 0. + size_t m_belt_brim_layer_idx{0}; + std::set m_initial_layer_extruders; std::vector> m_sorted_layer_filaments; // BBS diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index f37025d4e7..e0520a3694 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -468,6 +468,11 @@ ToolOrdering::ToolOrdering(const PrintObject &object, unsigned int first_extrude zs.emplace_back(layer->print_z); for (auto layer : object.support_layers()) zs.emplace_back(layer->print_z); + // Belt brim apron bands sit below the object's first layer and have no + // layer of their own, but tools_for_layer() asserts an exact Z match, so + // their print_z must be part of the ordering. + for (const BeltBrimBand &band : object.belt_brim_prologue()) + zs.emplace_back(band.print_z); this->initialize_layers(zs); } @@ -512,6 +517,10 @@ ToolOrdering::ToolOrdering(const Print &print, unsigned int first_extruder, bool zs.emplace_back(layer->print_z); for (auto layer : object->support_layers()) zs.emplace_back(layer->print_z); + // See the single-object ctor: belt brim apron bands need their own + // ordering entries or tools_for_layer() will assert. + for (const BeltBrimBand &band : object->belt_brim_prologue()) + zs.emplace_back(band.print_z); max_layer_height = std::max(max_layer_height, object->config().layer_height.value); } @@ -860,6 +869,30 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto } } + // Belt brim apron bands own their layers outright: they print below the + // object's first layer, so no object or support layer claims an extruder there + // and process_layer() would bail out at "Nothing to extrude". Claim the + // object's outer wall filament, in the same raw 1-based domain the loops above + // push. Deliberately not layer_tools.has_object, which drives skirt marking + // and wiping overrides. + if (! object.belt_brim_prologue().empty()) { + unsigned int brim_filament = 0; + for (size_t i = 0; i < object.num_printing_regions(); ++ i) { + const unsigned int f = object.printing_region(i).config().outer_wall_filament_id.value; + if (f > 0 && (brim_filament == 0 || f < brim_filament)) + brim_filament = f; + } + if (brim_filament == 0) + brim_filament = 1; + for (const BeltBrimBand &band : object.belt_brim_prologue()) { + if (band.fills.empty()) + continue; + LayerTools &layer_tools = this->tools_for_layer(band.print_z); + layer_tools.extruders.push_back(brim_filament); + layer_tools.has_belt_brim = true; + } + } + for (auto& layer : m_layer_tools) { // Sort and remove duplicates sort_remove_duplicates(layer.extruders); @@ -902,12 +935,20 @@ void ToolOrdering::fill_wipe_tower_partitions(const PrintConfig &config, coordf_ } //FIXME this is a hack to get the ball rolling. + // The `print_z < object_bottom_z` clause reads "below the object" as "raft + // gap". On a belt printer that is wrong: the brim apron legitimately prints + // below the object's first layer, and treating those layers as raft would put a + // wipe tower at negative Z. Belt brim and the prime tower are mutually + // exclusive (rejected in Print::validate()), so simply drop the clause there. + const bool belt_no_raft_gap = config.belt_printer.value; for (LayerTools < : m_layer_tools) lt.has_wipe_tower |= (lt.has_object && (config.timelapse_type == TimelapseType::tlSmooth || lt.wipe_tower_partitions > 0)) - || lt.print_z < object_bottom_z + EPSILON; + || (! belt_no_raft_gap && lt.print_z < object_bottom_z + EPSILON); // Test for a raft, insert additional wipe tower layer to fill in the raft separation gap. - for (size_t i = 0; i + 1 < m_layer_tools.size(); ++ i) { + // Skipped on belt printers for the same reason as the clause above: layers + // below the object are brim apron, not raft. + for (size_t i = 0; ! belt_no_raft_gap && i + 1 < m_layer_tools.size(); ++ i) { const LayerTools < = m_layer_tools[i]; const LayerTools <_next = m_layer_tools[i + 1]; if (lt.print_z < object_bottom_z + EPSILON && lt_next.print_z >= object_bottom_z + EPSILON) { diff --git a/src/libslic3r/GCode/ToolOrdering.hpp b/src/libslic3r/GCode/ToolOrdering.hpp index c77b152fe9..0d7526c72e 100644 --- a/src/libslic3r/GCode/ToolOrdering.hpp +++ b/src/libslic3r/GCode/ToolOrdering.hpp @@ -161,6 +161,10 @@ public: // Should a skirt be printed at this layer? // Layers are marked for infinite skirt aka draft shield. Not all the layers have to be printed. bool has_skirt = false; + // Belt printers: is this one of the brim-only apron layers below the object's + // first layer? Kept separate from has_object so skirt marking and wiping + // overrides are unaffected. + bool has_belt_brim = false; // Will there be anything extruded on this layer for the wipe tower? // Due to the support layers possibly interleaving the object layers, // wipe tower will be disabled for some support only layers. diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index dc89c2b1df..3e6e2b9aaf 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1090,7 +1090,7 @@ static std::vector s_Preset_print_options{ "top_surface_speed", "support_speed", "support_object_xy_distance", "support_object_first_layer_gap", "support_interface_speed", "bridge_speed", "internal_bridge_speed", "gap_infill_speed", "travel_speed", "travel_speed_z", "initial_layer_speed", "outer_wall_acceleration", "initial_layer_acceleration", "top_surface_acceleration", "default_acceleration", "skirt_type", "skirt_loops", "skirt_speed","min_skirt_length", "skirt_distance", "skirt_start_angle", "skirt_height","single_loop_draft_shield", "draft_shield", - "brim_width", "brim_object_gap", "brim_flow_ratio", "brim_use_efc_outline", "combine_brims", "brim_type", "brim_ears_max_angle", "brim_ears_detection_length", "enable_support", "support_type", "support_threshold_angle", "support_threshold_overlap","enforce_support_layers", + "brim_width", "leading_brim_length", "extra_brim_width", "brim_object_gap", "brim_flow_ratio", "brim_use_efc_outline", "combine_brims", "brim_type", "brim_ears_max_angle", "brim_ears_detection_length", "enable_support", "support_type", "support_threshold_angle", "support_threshold_overlap","enforce_support_layers", "raft_layers", "raft_first_layer_density", "raft_first_layer_expansion", "raft_contact_distance", "raft_expansion", "support_base_pattern", "support_base_pattern_spacing", "support_expansion", "support_style", // BBS diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index ba28f96804..189f9e178e 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -641,6 +641,24 @@ bool Print::has_brim() const return std::any_of(m_objects.begin(), m_objects.end(), [](PrintObject *object) { return object->has_brim(); }); } +bool Print::has_tilted_belt() const +{ + if (! m_config.belt_printer.value) + return false; + // A Z rotation leaves the belt floor flat (BeltTransform forces shear = 0) and no + // rotation at all means the machine is geometrically a flat bed. + const BeltRotationAxis axis = m_config.belt_slice_rotation.value; + if (axis != BeltRotationAxis::X && axis != BeltRotationAxis::Y) + return false; + const double tilt = std::abs(m_config.belt_slice_rotation_angle.value); + return tilt >= BELT_BRIM_MIN_TILT_DEG && tilt <= BELT_BRIM_MAX_TILT_DEG; +} + +bool Print::has_belt_brim() const +{ + return std::any_of(m_objects.begin(), m_objects.end(), [](PrintObject *object) { return object->has_belt_brim(); }); +} + //BBS std::vector Print::layers_sorted_for_object(float start, float end, std::vector &layers_of_objects, std::vector &boundingBox_for_objects, VecOfPoints &objects_instances_shift) { @@ -1342,6 +1360,65 @@ StringObjectException Print::validate(std::vector *warnin } if (m_config.draft_shield != dsDisabled) return { L("Draft shield is not compatible with belt printer mode.") }; + + // Belt brim spans many layers and owns the layers below the object, which + // neither the prime tower nor spiral vase can share. + if (this->has_belt_brim()) { + if (m_config.enable_prime_tower.value) + return { L("Brim is not compatible with the prime tower on a belt printer. " + "Disable one of them.") }; + if (m_config.spiral_mode.value) + return { L("Brim is not compatible with spiral vase mode on a belt printer. " + "Disable one of them.") }; + } + + for (const PrintObject *object : m_objects) { + const PrintObjectConfig &ocfg = object->config(); + const bool wants_brim = ocfg.brim_type != btNoBrim + && (ocfg.brim_width.value > 0. || ocfg.leading_brim_length.value > 0. + || ocfg.extra_brim_width.value > 0.); + if (! wants_brim) + continue; + + if (! this->has_tilted_belt()) { + if (std::abs(m_config.belt_slice_rotation_angle.value) > BELT_BRIM_MAX_TILT_DEG) + warn(L("The belt is too steep for a brim, so no brim will be generated."), + "brim_width", object->model_object()); + else + warn(L("A brim is only generated when the belt is tilted. Set a belt tilt angle, " + "or remove the brim setting."), + "brim_type", object->model_object()); + } + + if (ocfg.brim_type == btAutoBrim || ocfg.brim_type == btEar || ocfg.brim_type == btPainted) + warn(L("Belt printers support outer and inner brim only. Auto, Mouse ear and Painted " + "brim are printed as Outer brim only, using Brim width."), + "brim_type", object->model_object()); + + if (ocfg.leading_brim_length.value > 0. && ocfg.brim_object_gap.value > 0.) + warn(L("Brim-object gap separates the leading brim from the object's leading edge, " + "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()); + + if (ocfg.leading_brim_length.value > 0. && object->instances().size() > 1) + warn(L("This object has several instances sharing one belt position, so no brim is " + "generated for it. Arrange the copies along the belt instead."), + "leading_brim_length", 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 " + "not reserve that space. Leave room between objects."), + "leading_brim_length"); + } else { + // "Leading edge only" describes where a part meets a moving belt, so it has no + // meaning on a fixed bed. Brim.cpp prints it as an ordinary outer brim rather + // than silently producing nothing; say so. + for (const PrintObject *object : m_objects) + if (object->config().brim_type == btLeadingEdgeOnly) + warn(L("\"Leading edge only\" brim applies to belt printers. On this printer it is " + "printed as an ordinary outer brim."), + "brim_type", object->model_object()); } if (nozzles < 2 && extruders.size() > 1) { @@ -2251,8 +2328,28 @@ BoundingBox PrintObject::get_first_layer_bbox(float& a, float& layer_height, std a += area(slice); } } - if (has_brim()) + // Guard on `defined`: make_brim() can return before assigning this (it does on + // belt printers, where has_brim() is still true but the plate brim is skipped), + // and overwriting a valid bbox with an undefined one corrupted the first-layer + // centre and the GUI's first-layer area readout. + if (has_brim() && firstLayerObjectBrimBoundingBox.defined) bbox = firstLayerObjectBrimBoundingBox; + // Belt brim: the apron reaches ahead of the object along the belt. + if (has_belt_brim()) { + const Point shift = instances().empty() ? Point(0, 0) : instances()[0].shift_without_plate_offset(); + for (const ExPolygons &areas : m_belt_brim_areas_by_layer) + for (const ExPolygon &ex : areas) { + BoundingBox bb = get_extents(ex.contour); + bb.translate(shift.x(), shift.y()); + bbox.merge(bb); + } + for (const BeltBrimBand &band : m_belt_brim_prologue) + for (const ExPolygon &ex : band.areas) { + BoundingBox bb = get_extents(ex.contour); + bb.translate(shift.x(), shift.y()); + bbox.merge(bb); + } + } return bbox; } @@ -2804,6 +2901,26 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) } + // Belt brim: bound the first-layer convex hull by the lowest apron band, so + // bed levelling and the initial purge line account for brim that reaches + // ahead of every object. + if (this->has_belt_brim()) { + for (PrintObject *object : m_objects) { + if (! object->has_belt_brim() || object->belt_brim_prologue().empty()) + continue; + const BeltBrimBand &lowest = object->belt_brim_prologue().front(); + for (const PrintInstance &instance : object->instances()) + for (const ExPolygon &ex : lowest.areas) { + Polygon poly = ex.contour; + poly.translate(instance.shift); + append(m_first_layer_convex_hull.points, std::move(poly.points)); + } + } + } + + // Unchanged for belt printers: _make_skirt() already returns early for them, and + // the belt brim does not populate m_brimMapByInstance, which is what the + // skirt/brim grouping reads. if (has_skirt() || has_infinite_skirt() || has_brim()) { // Generate skirt/brim groups after brim so per-object and draft-shield footprints // include brims when grouping and offsetting skirt loops. diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index bb61c210a7..72a12918c7 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -17,6 +17,7 @@ #include "GCode/ThumbnailData.hpp" #include "GCode/GCodeProcessor.hpp" #include "MultiMaterialSegmentation.hpp" +#include "BeltBrim.hpp" #include "BeltTransform.hpp" #include "ObjectID.hpp" #include "libslic3r.h" @@ -384,6 +385,21 @@ public: && ! this->has_raft(); } + // Belt brim. A tilted belt needs its brim laid onto the belt plane over many + // layers instead of as one flat first-layer ring, so it is generated by + // BeltBrim.cpp and stored per layer here. Deliberately separate from + // has_brim(): that predicate feeds PrintRegion extruder collection, support + // trimming and the spiral vase probe, and widening it would perturb belt + // support output. + bool has_belt_brim() const; + const std::vector& belt_brim_by_layer() const { return m_belt_brim_by_layer; } + const std::vector& belt_brim_areas_by_layer() const { return m_belt_brim_areas_by_layer; } + const std::vector& belt_brim_prologue() const { return m_belt_brim_prologue; } + void clear_belt_brim(); + void set_belt_brim(std::vector &&by_layer, + std::vector &&areas, + std::vector &&prologue); + // BBS const ExtrusionEntityCollection& object_skirt() const { return m_skirt; @@ -576,6 +592,12 @@ private: SlicingParameters m_slicing_params; LayerPtrs m_layers; SupportLayerPtrs m_support_layers; + // Belt brim, generated in posSupportMaterial by BeltBrim.cpp. Object-local + // slicing frame, one entry per object layer plus a prologue of brim-only + // bands that print below the object's first layer. + std::vector m_belt_brim_by_layer; + std::vector m_belt_brim_areas_by_layer; + std::vector m_belt_brim_prologue; // BBS std::shared_ptr m_tree_support_preview_cache; @@ -971,6 +993,15 @@ public: bool has_infinite_skirt() const; bool has_skirt() const; bool has_brim() const; + // True when the belt is tilted enough that the BELT plane, not the bed plane, is + // the adhesion surface. The flat plate brim is geometrically meaningless then + // and must not run, whatever the per-object brim settings say - in particular + // brim_type "Auto", which has_brim() reports as enabled even at width 0. + bool has_tilted_belt() const; + // True when at least one object actually gets a tilted-belt brim generated. + // Implies has_tilted_belt(), but additionally requires the object's own brim + // settings to ask for one. + bool has_belt_brim() const; //BBS bool has_auto_brim() const { return std::any_of(m_objects.begin(), m_objects.end(), [](PrintObject* object) { return object->config().brim_type == btAutoBrim; }); diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index b2984164ee..f641699d91 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -508,6 +508,7 @@ static const t_config_enum_values s_keys_map_BrimType = { {"auto_brim", btAutoBrim}, // BBS {"brim_ears", btEar}, // Orca {"painted", btPainted}, // BBS + {"leading_edge_only", btLeadingEdgeOnly}, // belt printers }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BrimType) @@ -1899,6 +1900,45 @@ void PrintConfigDef::init_fff_params() def->mode = comSimple; def->set_default_value(new ConfigOptionFloat(0.)); + def = this->add("leading_brim_length", coFloat); + def->label = L("Leading brim length"); + def->category = L("Support"); + def->tooltip = L("Belt printers only. Extends the brim AHEAD of the object along the belt, on " + "every downhill-facing edge of its contact area - both the object's first " + "contact with the belt and any island that lands later. This apron is laid " + "onto the belt before the object reaches it, so the leading edge has " + "something already stuck down to hold on to.\n\n" + "Measured on the belt surface, and added on top of Brim width: the brim " + "reaches Brim-object gap + Leading brim length + Brim width ahead of the " + "object. Set Brim-object gap to 0, or the apron will not touch the object it " + "is meant to anchor.\n\n" + "On a tilted belt each layer lays one strip of the brim, so the thickness of " + "the resulting brim sheet is set by flow rather than by layer height. Use " + "Brim flow ratio to tune it.\n\n" + "Set to 0 to disable."); + def->sidetext = L("mm"); // millimeters, CIS languages need translation + def->min = 0; + def->max = 100; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0.)); + + def = this->add("extra_brim_width", coFloat); + def->label = L("Extra brim width"); + def->category = L("Support"); + def->tooltip = L("Belt printers only. Widens the brim SIDEWAYS, across the belt, without " + "extending it further ahead of or behind the object. Use it when a part needs " + "more grip along its length than Brim width alone gives.\n\n" + "Measured on the belt surface, and added on top of Brim width: the brim " + "reaches Brim-object gap + Brim width + Extra brim width to either side of " + "the object. To extend the brim ahead of the object instead, use Leading brim " + "length.\n\n" + "Set to 0 to disable."); + def->sidetext = L("mm"); // millimeters, CIS languages need translation + def->min = 0; + def->max = 100; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0.)); + def = this->add("brim_type", coEnum); def->label = L("Brim type"); def->category = L("Support"); @@ -1912,6 +1952,7 @@ void PrintConfigDef::init_fff_params() def->enum_values.emplace_back("inner_only"); def->enum_values.emplace_back("outer_and_inner"); def->enum_values.emplace_back("no_brim"); + def->enum_values.emplace_back("leading_edge_only"); def->enum_labels.emplace_back(L("Auto")); def->enum_labels.emplace_back(L("Mouse ear")); def->enum_labels.emplace_back(L("Painted")); @@ -1919,6 +1960,7 @@ void PrintConfigDef::init_fff_params() def->enum_labels.emplace_back(L("Inner brim only")); def->enum_labels.emplace_back(L("Outer and inner brim")); def->enum_labels.emplace_back(L("No-brim")); + def->enum_labels.emplace_back(L("Leading edge only")); def->mode = comSimple; def->set_default_value(new ConfigOptionEnum(btAutoBrim)); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index acc5f56105..3342a54b6d 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -391,6 +391,10 @@ enum BrimType { btInnerOnly, btOuterAndInner, btNoBrim, + // Belt printers: brim only where the part first touches the belt, nothing after + // that. Appended last so no existing value shifts. On a non-belt printer this + // has no meaning and behaves as btOuterOnly. + btLeadingEdgeOnly, }; enum TimelapseType : int { @@ -1140,6 +1144,8 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionBool, brim_use_efc_outline)) ((ConfigOptionEnum, brim_type)) ((ConfigOptionFloat, brim_width)) + ((ConfigOptionFloat, leading_brim_length)) + ((ConfigOptionFloat, extra_brim_width)) ((ConfigOptionFloat, brim_ears_detection_length)) ((ConfigOptionFloat, brim_ears_max_angle)) ((ConfigOptionFloat, skirt_start_angle)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index e8f7a8e08f..1b8ddf3a9e 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -969,6 +969,13 @@ void PrintObject::generate_support_material() this->_generate_support_material(); m_print->throw_if_canceled(); } + // Belt brim rides here rather than in the brim step because its apron + // prologue introduces print_z values below the object's first layer, and + // 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(); BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] generate_support_material EXIT tid=" << std::this_thread::get_id() << " obj=" << this; this->set_done(posSupportMaterial); } else { @@ -1142,6 +1149,52 @@ void PrintObject::clear_support_layers() l->cantilevers.clear(); } } + // Belt brim is owned by the same step, so it must die with it or an + // invalidate-without-rerun would leave stale bands (and stale prologue Zs) + // behind. Unconditional: unlike support layers it is never shared. + this->clear_belt_brim(); +} + +// Belt brim ------------------------------------------------------------------ +// +// The tilt test is answered from the print CONFIG, not from SlicingParameters: +// invalidating posSupportMaterial clears m_slicing_params.valid, and this is +// queried from Print::process() dispatch, Brim.cpp and the G-code emitter, where +// a stale zero shear factor would silently drop the brim. BeltBrim.cpp itself +// reads the real belt floor through BeltFloorContext, where the parameters are +// guaranteed current. +bool PrintObject::has_belt_brim() const +{ + if (! m_print->has_tilted_belt()) + return false; + // Translating an instance along the belt axis changes its physical belt-floor + // Z, so one set of bands cannot serve several instances sharing a PrintObject. + // belt_force_separate() in PrintApply.cpp gives one instance per object + // whenever a global belt flag is set, which the shipped belt profiles do. + if (m_instances.size() > 1) + return false; + if (m_config.brim_type == btNoBrim) + return false; + if (m_config.brim_width.value <= 0. && m_config.leading_brim_length.value <= 0. + && m_config.extra_brim_width.value <= 0.) + return false; + return ! this->has_raft(); +} + +void PrintObject::clear_belt_brim() +{ + m_belt_brim_by_layer.clear(); + m_belt_brim_areas_by_layer.clear(); + m_belt_brim_prologue.clear(); +} + +void PrintObject::set_belt_brim(std::vector &&by_layer, + std::vector &&areas, + std::vector &&prologue) +{ + m_belt_brim_by_layer = std::move(by_layer); + m_belt_brim_areas_by_layer = std::move(areas); + m_belt_brim_prologue = std::move(prologue); } std::shared_ptr PrintObject::alloc_tree_support_preview_cache() @@ -1184,6 +1237,8 @@ bool PrintObject::invalidate_state_by_config_options( bool invalidated = false; for (const t_config_option_key &opt_key : opt_keys) { if ( opt_key == "brim_width" + || opt_key == "leading_brim_length" + || opt_key == "extra_brim_width" || opt_key == "brim_object_gap" || opt_key == "brim_use_efc_outline" || opt_key == "brim_type" diff --git a/src/libslic3r/Support/BeltFloorContext.cpp b/src/libslic3r/Support/BeltFloorContext.cpp index d2ffcfd397..eb6cf3670c 100644 --- a/src/libslic3r/Support/BeltFloorContext.cpp +++ b/src/libslic3r/Support/BeltFloorContext.cpp @@ -66,7 +66,7 @@ Polygons BeltFloorContext::half_plane(coordf_t print_z, bool belt_surface) const if (!m_active) return {}; - const double cutoff = (print_z - m_z_shift - m_floor_offset) / m_shear_factor; + const double cutoff = this->cutoff_u(print_z); const coord_t cutoff_scaled = scale_(cutoff); const coord_t large_bound = scale_(1e3); diff --git a/src/libslic3r/Support/BeltFloorContext.hpp b/src/libslic3r/Support/BeltFloorContext.hpp index 7b8b704ef6..3a45a6eba8 100644 --- a/src/libslic3r/Support/BeltFloorContext.hpp +++ b/src/libslic3r/Support/BeltFloorContext.hpp @@ -47,6 +47,12 @@ public: // Returns -infinity if not active. double floor_print_z(const Point &pos_slicing) const; + // The from_axis coordinate (unscaled, slicing frame) where the belt surface + // crosses a horizontal plane at print_z. Inverse of floor_print_z() along + // the shear axis. Only meaningful when is_active(). + coordf_t cutoff_u(coordf_t print_z) const + { return (print_z - m_z_shift - m_floor_offset) / m_shear_factor; } + // Pre-compute belt floor polygons for a range of layers. // layer_print_z(i) returns the print_z for layer index i. std::vector compute_per_layer_floors( diff --git a/src/libslic3r/Support/SupportCommon.cpp b/src/libslic3r/Support/SupportCommon.cpp index e6b93f1717..4ab571edd1 100644 --- a/src/libslic3r/Support/SupportCommon.cpp +++ b/src/libslic3r/Support/SupportCommon.cpp @@ -268,7 +268,9 @@ SupportGeneratorLayersPtr generate_raft_base( // The object does not have a raft. // Calculate the area covered by the brim. const BrimType brim_type = object.config().brim_type; - const bool brim_outer = brim_type == btOuterOnly || brim_type == btOuterAndInner; + // btLeadingEdgeOnly only means anything on a belt printer, where this code path + // does not run; elsewhere it degrades to an outer brim (see Brim.cpp). + const bool brim_outer = brim_type == btOuterOnly || brim_type == btOuterAndInner || brim_type == btLeadingEdgeOnly; const bool brim_inner = brim_type == btInnerOnly || brim_type == btOuterAndInner; // BBS: the pattern of raft and brim are the same, thus the brim can be serpated by support raft. const auto brim_object_gap = scaled(object.config().brim_object_gap.value); diff --git a/src/libslic3r/Support/SupportSpotsGenerator.cpp b/src/libslic3r/Support/SupportSpotsGenerator.cpp index 6fb4908e67..aa79fdb9b8 100644 --- a/src/libslic3r/Support/SupportSpotsGenerator.cpp +++ b/src/libslic3r/Support/SupportSpotsGenerator.cpp @@ -761,7 +761,9 @@ std::tuple build_object_part_from_slice(const size_t &slice_i // thus has lower adhesion. For now this effect will be neglected. ExPolygon slice_poly = layer->lslices[slice_idx]; ExPolygons brim; - if (params.brim_type == BrimType::btOuterAndInner || params.brim_type == BrimType::btOuterOnly) { + // btLeadingEdgeOnly degrades to an outer brim off belt printers (see Brim.cpp). + if (params.brim_type == BrimType::btOuterAndInner || params.brim_type == BrimType::btOuterOnly + || params.brim_type == BrimType::btLeadingEdgeOnly) { Polygon brim_hole = slice_poly.contour; brim_hole.reverse(); Polygons c = expand(slice_poly.contour, scale_(params.brim_width)); // For very small polygons, the expand may result in empty vector, even thought the input is correct. diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index f95260e0c2..3fc69d6e4b 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -630,11 +630,25 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in const bool gcf_is_klipper = gcflavor == GCodeFlavor::gcfKlipper; // Belt printer: detect early since it affects multiple toggle decisions below. + // `is_belt_tilted` is the stricter test that mirrors PrintObject::has_belt_brim(): + // only a tilted belt gets the belt-plane brim, while a belt printer with no + // rotation is geometrically a flat bed and keeps the ordinary plate brim. bool is_belt_printer = false; + bool is_belt_tilted = false; { - const auto *belt_opt = preset_bundle->printers.get_edited_preset().config.option("belt_printer"); + const auto &printer_cfg = preset_bundle->printers.get_edited_preset().config; + const auto *belt_opt = printer_cfg.option("belt_printer"); if (belt_opt) is_belt_printer = belt_opt->value; + const auto *axis = printer_cfg.option>("belt_slice_rotation"); + const auto *angle = printer_cfg.option("belt_slice_rotation_angle"); + if (is_belt_printer && axis != nullptr && angle != nullptr) { + // Same window as Print::has_tilted_belt(); shared constants so the GUI and + // the backend cannot drift apart. + const double tilt = std::abs(angle->value); + is_belt_tilted = (axis->value == BeltRotationAxis::X || axis->value == BeltRotationAxis::Y) + && tilt >= BELT_BRIM_MIN_TILT_DEG && tilt <= BELT_BRIM_MAX_TILT_DEG; + } } bool have_volumetric_extrusion_rate_slope = config->option("max_volumetric_extrusion_rate_slope")->value > 0; @@ -808,21 +822,34 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in toggle_field("skirt_height", false); } - bool have_brim = (config->opt_enum("brim_type") != btNoBrim) && !is_belt_printer; - if (is_belt_printer) - toggle_field("brim_type", false); + // Belt printers now get a brim too, laid onto the tilted belt by BeltBrim.cpp, + // so brim type / width / object gap all apply. A belt printer with no tilt is + // geometrically a flat bed and uses the ordinary plate brim, hence the separate + // is_belt_tilted test. + bool have_brim = config->opt_enum("brim_type") != btNoBrim; toggle_field("brim_object_gap", have_brim); - toggle_field("brim_use_efc_outline", have_brim); - toggle_field("combine_brims", have_brim); - bool have_brim_width = (config->opt_enum("brim_type") != btNoBrim) && config->opt_enum("brim_type") != btAutoBrim && + // Both are first-layer-only concepts that the belt path cannot honour. + toggle_field("brim_use_efc_outline", have_brim && !is_belt_tilted); + toggle_field("combine_brims", have_brim && !is_belt_tilted); + bool have_brim_width = have_brim && config->opt_enum("brim_type") != btAutoBrim && config->opt_enum("brim_type") != btPainted; - toggle_field("brim_width", have_brim_width); + // On a tilted belt Auto / Mouse ear / Painted all collapse to outer-only at the + // configured width, so the width field has to stay live for them too. + toggle_field("brim_width", have_brim_width || (have_brim && is_belt_tilted)); toggle_field("brim_flow_ratio", have_brim); + // Paired toggle_line + toggle_field: cb_toggle_line is null in the per-object + // override panel, so the row cannot be hidden there and greying out is the + // fallback. Both extras are belt-only: one extends the brim ahead along the belt, + // the other widens it across the belt. + for (auto el : { "leading_brim_length", "extra_brim_width" }) { + toggle_line(el, is_belt_tilted); + toggle_field(el, is_belt_tilted && have_brim); + } // Wall filament selectors use the same logic as in Print::extruders(). toggle_field("outer_wall_filament_id", have_perimeters || have_brim); toggle_field("inner_wall_filament_id", have_perimeters || have_brim); - bool have_brim_ear = (config->opt_enum("brim_type") == btEar); + bool have_brim_ear = (config->opt_enum("brim_type") == btEar) && !is_belt_tilted; const auto brim_width = config->opt_float("brim_width"); // disable brim_ears_max_angle and brim_ears_detection_length if brim_width is 0 toggle_field("brim_ears_max_angle", brim_width > 0.0f); diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index 6b51167163..19241cbd93 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -86,15 +86,15 @@ std::map> SettingsFactory::OBJECT_C {"make_overhang_printable_angle","", 8},{"make_overhang_printable_hole_size","",9}, {"wall_sequence","",10}, {"precise_z_height", "",10} }}, - { L("Support"), {{"brim_type", "",1},{"brim_width", "",2},{"brim_object_gap", "",3},{"brim_flow_ratio", "",4},{"brim_use_efc_outline", "",5}, - {"enable_support", "",6},{"support_type", "",7},{"support_threshold_angle", "",8}, {"support_threshold_overlap", "",9}, {"support_on_build_plate_only", "",10}, - {"support_filament", "",11},{"support_interface_filament", "",12},{"support_expansion", "",13},{"support_style", "",14}, - {"tree_support_brim_width", "",15}, {"tree_support_branch_angle", "",16},{"tree_support_branch_angle_organic","",17}, {"tree_support_wall_count", "",18},{"tree_support_branch_diameter_angle", "",19},//tree support - {"support_bottom_z_distance", "",20},{"support_top_z_distance", "",21},{"support_base_pattern", "",22},{"support_base_pattern_spacing", "",23}, - {"support_interface_top_layers", "",24},{"support_interface_bottom_layers", "",25},{"support_interface_spacing", "",26},{"support_bottom_interface_spacing", "",27}, - {"support_object_xy_distance", "",28}, {"bridge_no_support", "",29},{"max_bridge_length", "",30},{"support_critical_regions_only", "",31},{"support_remove_small_overhang","",32}, - {"build_plate_tilt_x","",33},{"build_plate_tilt_y","",34}, - {"support_object_first_layer_gap","",35} + { L("Support"), {{"brim_type", "",1},{"brim_width", "",2},{"leading_brim_length", "",3},{"extra_brim_width", "",4},{"brim_object_gap", "",5},{"brim_flow_ratio", "",6},{"brim_use_efc_outline", "",7}, + {"enable_support", "",8},{"support_type", "",9},{"support_threshold_angle", "",10}, {"support_threshold_overlap", "",11}, {"support_on_build_plate_only", "",12}, + {"support_filament", "",13},{"support_interface_filament", "",14},{"support_expansion", "",15},{"support_style", "",16}, + {"tree_support_brim_width", "",17}, {"tree_support_branch_angle", "",18},{"tree_support_branch_angle_organic","",19}, {"tree_support_wall_count", "",20},{"tree_support_branch_diameter_angle", "",21},//tree support + {"support_bottom_z_distance", "",22},{"support_top_z_distance", "",23},{"support_base_pattern", "",24},{"support_base_pattern_spacing", "",25}, + {"support_interface_top_layers", "",26},{"support_interface_bottom_layers", "",27},{"support_interface_spacing", "",28},{"support_bottom_interface_spacing", "",29}, + {"support_object_xy_distance", "",30}, {"bridge_no_support", "",31},{"max_bridge_length", "",32},{"support_critical_regions_only", "",33},{"support_remove_small_overhang","",34}, + {"build_plate_tilt_x","",35},{"build_plate_tilt_y","",36}, + {"support_object_first_layer_gap","",37} }}, { L("Speed"), {{"support_speed", "",12}, {"support_interface_speed", "",13} }} diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 0613467ac8..7a4cefe4fa 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -3071,6 +3071,8 @@ void TabPrint::build() optgroup = page->new_optgroup(L("Brim"), L"param_adhension"); optgroup->append_single_option_line("brim_type", "others_settings_brim#type"); optgroup->append_single_option_line("brim_width", "others_settings_brim#width"); + optgroup->append_single_option_line("leading_brim_length", "others_settings_brim#leading-length"); + optgroup->append_single_option_line("extra_brim_width", "others_settings_brim#extra-width"); optgroup->append_single_option_line("brim_object_gap", "others_settings_brim#brim-object-gap"); optgroup->append_single_option_line("brim_flow_ratio", "others_settings_brim#brim-flow-ratio"); optgroup->append_single_option_line("brim_use_efc_outline", "others_settings_brim#brim-use-efc-outline"); diff --git a/tests/fff_print/test_skirt_brim.cpp b/tests/fff_print/test_skirt_brim.cpp index 3f63d3de5f..755dec06e0 100644 --- a/tests/fff_print/test_skirt_brim.cpp +++ b/tests/fff_print/test_skirt_brim.cpp @@ -1,6 +1,8 @@ #include +#include "libslic3r/ClipperUtils.hpp" #include "libslic3r/GCodeReader.hpp" +#include "libslic3r/Layer.hpp" #include "libslic3r/Config.hpp" #include "libslic3r/Geometry.hpp" #include "libslic3r/Geometry/ConvexHull.hpp" @@ -441,3 +443,193 @@ SCENARIO("Skirt and brim generation", "[SkirtBrim]") { } } } + +// Belt printers --------------------------------------------------------------- +// +// On a tilted belt the brim is laid onto the belt PLANE rather than into the Z=0 +// bed plane, so it is spread across many layers instead of living on the first +// one. The discriminating measurement is the number of contiguous brim runs in +// the G-code: a flat plate brim gives a single run, a belt brim gives one per +// layer that carries a band. Distinct Z values are useless here, because the +// machine-frame transform couples Y into Z so every belt move has its own Z. +static DynamicPrintConfig belt_brim_config() +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "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" }, + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "skirt_loops", 0 }, + { "top_shell_layers", 0 }, + { "bottom_shell_layers", 1 }, + { "machine_start_gcode", "T[initial_tool]\n" }, + }); + return config; +} + +TEST_CASE("Belt brim spans many layers instead of one", "[SkirtBrim][belt]") +{ + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", "outer_only" }, + { "brim_width", 5 }, + }); + const std::string gcode = slice({ cube(20) }, config); + // A plate-brim implementation would score 1 here. + CHECK(role_passes(gcode, "brim") > 10); +} + +TEST_CASE("Belt brim is absent when both widths are zero", "[SkirtBrim][belt]") +{ + // The "no effect when disabled" guard: brim_type Auto is the shipped default and + // reports has_brim() even at width 0, so this also pins the gate that keeps the + // flat plate brim from running on a tilted belt. + const char *brim_type = GENERATE("auto_brim", "outer_only", "no_brim"); + DYNAMIC_SECTION("brim_type " << brim_type) { + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", brim_type }, + { "brim_width", 0 }, + { "leading_brim_length", 0 }, + { "extra_brim_width", 0 }, + }); + const std::string gcode = slice({ cube(20) }, config); + CHECK(role_passes(gcode, "brim") == 0); + } +} + +TEST_CASE("Leading brim length alone produces a belt brim", "[SkirtBrim][belt]") +{ + // Exercises the leading_brim_length-only enablement path and the downhill sweep. + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", "outer_only" }, + { "brim_width", 0 }, + { "leading_brim_length", 5 }, + { "brim_object_gap", 0 }, + }); + const std::string gcode = slice({ cube(20) }, config); + CHECK(role_passes(gcode, "brim") > 0); +} + +TEST_CASE("Leading brim length reaches further ahead of the object", "[SkirtBrim][belt]") +{ + // Compared between two runs rather than against an absolute coordinate, so the + // assertion survives any change of origin or axis remap. + auto brim_extent = [](double extra) { + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", "outer_only" }, + { "brim_width", 3 }, + { "leading_brim_length", extra }, + { "brim_object_gap", 0 }, + }); + const std::string gcode = slice({ cube(20) }, config); + // The apron prints before the object reaches the belt, so it shows up as brim + // extrusion at the lowest machine Z of any brim move. + double min_z = std::numeric_limits::max(); + GCodeReader parser; + parser.parse_buffer(gcode, [&min_z](GCodeReader &self, const GCodeReader::GCodeLine &line) { + if (line.extruding(self) && line.comment().find("brim") != std::string_view::npos) + min_z = std::min(min_z, static_cast(self.z())); + }); + return min_z; + }; + const double without = brim_extent(0.); + const double with = brim_extent(10.); + REQUIRE(without < std::numeric_limits::max()); + REQUIRE(with < std::numeric_limits::max()); + CHECK(with < without); +} + +TEST_CASE("Every brim type slices on a belt printer", "[SkirtBrim][belt]") +{ + // Auto / Mouse ear / Painted collapse to outer-only rather than crashing or + // silently producing nothing. + const char *brim_type = GENERATE("auto_brim", "brim_ears", "painted", "outer_only", + "inner_only", "outer_and_inner", "no_brim"); + DYNAMIC_SECTION("brim_type " << brim_type) { + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", brim_type }, + { "brim_width", 5 }, + }); + const std::string gcode = slice({ cube(20) }, config); + REQUIRE(! gcode.empty()); + if (std::string(brim_type) == "no_brim") + CHECK(role_passes(gcode, "brim") == 0); + else if (std::string(brim_type) != "inner_only") + // A solid cube has no holes, so inner_only legitimately yields nothing. + CHECK(role_passes(gcode, "brim") > 0); + } +} + +TEST_CASE("An untilted belt printer gets no brim", "[SkirtBrim][belt]") +{ + // Belt brim needs a tilt to have a belt plane to lie on, and the flat plate brim + // cannot reach the G-code on any belt printer: it is emitted out of + // skirt_brim_groups(), which _make_skirt() builds, and that returns early for every + // belt printer. So an untilted belt printer gets nothing - unchanged by this + // feature. Making the flat brim work here would mean reopening the belt skirt gate, + // which is a separate change; Print::validate() warns instead. + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "belt_slice_rotation", "none" }, + { "brim_type", "outer_only" }, + { "brim_width", 5 }, + }); + const std::string gcode = slice({ cube(20) }, config); + CHECK(role_passes(gcode, "brim") == 0); +} + +TEST_CASE("Belt brim does not resurrect the skirt", "[SkirtBrim][belt]") +{ + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", "outer_only" }, + { "brim_width", 5 }, + { "skirt_loops", 2 }, + }); + const std::string gcode = slice({ cube(20) }, config); + CHECK(role_passes(gcode, "skirt") == 0); +} + +TEST_CASE("Belt brim lines all have the same width", "[SkirtBrim][belt]") +{ + // Each brim line's extrusion volume comes from its nozzle-to-belt clearance. Anchoring + // every line to a fixed fraction of its own band gives them all the same clearance, so + // they all come out the same width. The nominal-spacing lattice this replaced let each + // line land wherever it fell inside its band, so the clearance - and the width with it - + // varied by 2x, which showed up as visibly ragged brim. + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", "outer_only" }, + { "brim_width", 5 }, + { "brim_object_gap", 0 }, + }); + Print print; + init_and_process_print({ cube(20) }, print, config); + const PrintObject *obj = print.objects().front(); + + std::vector widths; + auto collect = [&widths](const ExtrusionEntityCollection &coll) { + for (const ExtrusionEntity *ee : coll.entities) + if (const auto *path = dynamic_cast(ee)) + widths.push_back(path->width); + }; + for (const ExtrusionEntityCollection &band : obj->belt_brim_by_layer()) + collect(band); + for (const BeltBrimBand &band : obj->belt_brim_prologue()) + collect(band.fills); + + REQUIRE(widths.size() > 10); + const float lo = *std::min_element(widths.begin(), widths.end()); + const float hi = *std::max_element(widths.begin(), widths.end()); + CHECK_THAT(hi, Catch::Matchers::WithinRel(lo, 1e-4)); +} diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index 7524c27479..b564a948d7 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -8,6 +8,7 @@ add_executable(${_TEST_NAME}_tests test_arachne_walls.cpp test_arrange.cpp test_bambu_networking.cpp + test_belt_brim.cpp test_calib.cpp test_clipper_offset.cpp test_clipper_utils.cpp diff --git a/tests/libslic3r/test_belt_brim.cpp b/tests/libslic3r/test_belt_brim.cpp new file mode 100644 index 0000000000..9f645817e9 --- /dev/null +++ b/tests/libslic3r/test_belt_brim.cpp @@ -0,0 +1,341 @@ +#include + +#include "libslic3r/BeltBrim.hpp" +#include "libslic3r/BoundingBox.hpp" +#include "libslic3r/ClipperUtils.hpp" +#include "libslic3r/ExPolygon.hpp" + +using namespace Slic3r; + +// Pure-geometry tests for the belt brim. No Print, no slicing: everything here is +// a property of the tilted-belt mapping and the sweep/lattice helpers, which is +// exactly the part that has to be right before any G-code is worth looking at. + +static ExPolygon make_box(coord_t x0, coord_t y0, coord_t x1, coord_t y1) +{ + ExPolygon out; + out.contour.points = { Point(x0, y0), Point(x1, y0), Point(x1, y1), Point(x0, y1) }; + return out; +} + +static void add_hole(ExPolygon &ex, coord_t x0, coord_t y0, coord_t x1, coord_t y1) +{ + Polygon hole; + // Holes run clockwise, opposite the contour. + hole.points = { Point(x0, y0), Point(x0, y1), Point(x1, y1), Point(x1, y0) }; + ex.holes.emplace_back(std::move(hole)); +} + +SCENARIO("sweep_ex sweeps a box", "[BeltBrim]") { + const coord_t mm = scale_(1.); + GIVEN("a 10x10 mm box") { + const ExPolygons src { make_box(0, 0, 10 * mm, 10 * mm) }; + + WHEN("swept 5 mm along -Y") { + const ExPolygons out = sweep_ex(src, Point(0, -5 * mm)); + THEN("it becomes one 10x15 mm box") { + REQUIRE(out.size() == 1); + REQUIRE(out.front().holes.empty()); + const BoundingBox bb = get_extents(out); + CHECK(bb.min.y() == -5 * mm); + CHECK(bb.max.y() == 10 * mm); + CHECK(bb.min.x() == 0); + CHECK(bb.max.x() == 10 * mm); + CHECK_THAT(unscale(unscale(out.front().area())), + Catch::Matchers::WithinRel(150., 1e-6)); + } + } + + WHEN("swept by a zero vector") { + THEN("it is unchanged") { + const ExPolygons out = sweep_ex(src, Point(0, 0)); + REQUIRE(out.size() == 1); + CHECK(out.front().area() == src.front().area()); + } + } + + WHEN("swept diagonally") { + // For a convex P, area(P + [0,t]) == area(P) + |t| * width of P + // perpendicular to t. For a square swept along (1,1)/sqrt2 the + // perpendicular width is the diagonal, 10*sqrt2. + const ExPolygons out = sweep_ex(src, Point(3 * mm, 3 * mm)); + THEN("area grows by |t| times the perpendicular width") { + const double expected = 100. + std::sqrt(2. * 9.) * (10. * std::sqrt(2.)); + CHECK_THAT(unscale(unscale(out.front().area())), + Catch::Matchers::WithinRel(expected, 1e-6)); + } + } + } +} + +SCENARIO("sweep_ex closes holes narrower than the sweep", "[BeltBrim]") { + const coord_t mm = scale_(1.); + // This is the assertion that catches the two likeliest implementation bugs: + // omitting hole boundaries from the parallelogram set, or using the wrong + // Clipper fill rule. Note hole survival depends on the hole's extent ALONG + // the sweep direction, not on its narrowest dimension. + GIVEN("a 20x20 mm box with a hole 2 mm tall in the sweep direction") { + ExPolygon ex = make_box(0, 0, 20 * mm, 20 * mm); + add_hole(ex, 5 * mm, 9 * mm, 15 * mm, 11 * mm); + WHEN("swept 5 mm along -Y") { + const ExPolygons out = sweep_ex(ExPolygons{ ex }, Point(0, -5 * mm)); + THEN("the hole is filled in") { + REQUIRE(out.size() == 1); + CHECK(out.front().holes.empty()); + } + } + } + GIVEN("a 20x20 mm box with a hole 12 mm tall in the sweep direction") { + ExPolygon ex = make_box(0, 0, 20 * mm, 20 * mm); + add_hole(ex, 5 * mm, 4 * mm, 15 * mm, 16 * mm); + WHEN("swept 5 mm along -Y") { + const ExPolygons out = sweep_ex(ExPolygons{ ex }, Point(0, -5 * mm)); + THEN("the hole survives, shrunk by the sweep") { + REQUIRE(out.size() == 1); + REQUIRE(out.front().holes.size() == 1); + const BoundingBox hb = get_extents(out.front().holes.front()); + CHECK(hb.max.y() - hb.min.y() == 7 * mm); + } + } + } + GIVEN("a box whose edges are parallel to the sweep vector") { + // Degenerate parallelograms; Clipper must simply discard them. + const ExPolygons src { make_box(0, 0, 10 * mm, 10 * mm) }; + WHEN("swept along +X") { + const ExPolygons out = sweep_ex(src, Point(4 * mm, 0)); + THEN("the result is the expected rectangle") { + REQUIRE(out.size() == 1); + const BoundingBox bb = get_extents(out); + CHECK(bb.min.x() == 0); + CHECK(bb.max.x() == 14 * mm); + } + } + } + GIVEN("a reversed (clockwise) contour") { + ExPolygon ex = make_box(0, 0, 10 * mm, 10 * mm); + ex.contour.reverse(); + WHEN("swept") { + const ExPolygons out = sweep_ex(ExPolygons{ ex }, Point(0, -5 * mm)); + THEN("material is still produced") { + REQUIRE(! out.empty()); + CHECK(get_extents(out).min.y() == -5 * mm); + } + } + } +} + +SCENARIO("Belt flattening round-trips and rescales only the shear axis", "[BeltBrim]") { + const coord_t mm = scale_(1.); + const double shear = GENERATE(0.1, 0.5, 1.0, 3.0); + const int from_axis = GENERATE(0, 1); + DYNAMIC_SECTION("shear " << shear << " axis " << from_axis) { + const BeltBrimFrame frame { shear, from_axis }; + + // An L shape with a hole, so contours and holes are both exercised. + ExPolygon ex; + ex.contour.points = { Point(0, 0), Point(20 * mm, 0), Point(20 * mm, 6 * mm), + Point(6 * mm, 6 * mm), Point(6 * mm, 20 * mm), Point(0, 20 * mm) }; + add_hole(ex, 2 * mm, 2 * mm, 4 * mm, 4 * mm); + const ExPolygons src { ex }; + + const ExPolygons round_tripped = belt_unflatten(belt_flatten(src, frame), frame); + REQUIRE(round_tripped.size() == src.size()); + REQUIRE(round_tripped.front().holes.size() == src.front().holes.size()); + for (size_t c = 0; c < src.front().num_contours(); ++ c) { + const Points &a = src.front().contour_or_hole(c).points; + const Points &b = round_tripped.front().contour_or_hole(c).points; + REQUIRE(a.size() == b.size()); + for (size_t i = 0; i < a.size(); ++ i) { + // Rounding, not truncation, so the round trip stays within a + // couple of coordinate units. + CHECK(std::abs(a[i].x() - b[i].x()) <= 2); + CHECK(std::abs(a[i].y() - b[i].y()) <= 2); + // The axis that is not stretched must come back untouched. + if (from_axis == 0) + CHECK(a[i].y() == b[i].y()); + else + CHECK(a[i].x() == b[i].x()); + } + } + } +} + +SCENARIO("Flattening makes shear-axis distances true on-belt distances", "[BeltBrim]") { + // The property the whole design rests on: an in-plane distance w projects to + // dw = w * cos(tilt) along the shear axis, so stretching that axis by + // 1/cos(tilt) makes ordinary Clipper offsets measure real on-belt distance. + const coord_t mm = scale_(1.); + const double shear = GENERATE(0.1, 0.5, 1.0, 3.0); + const int from_axis = GENERATE(0, 1); + DYNAMIC_SECTION("shear " << shear << " axis " << from_axis) { + const BeltBrimFrame frame { shear, from_axis }; + const double stretch = std::sqrt(1. + shear * shear); + CHECK_THAT(frame.u_stretch(), Catch::Matchers::WithinRel(stretch, 1e-12)); + CHECK_THAT(frame.cos_tilt() * frame.u_stretch(), Catch::Matchers::WithinRel(1., 1e-12)); + + // Two points 1 mm apart along the shear axis are stretch mm apart once + // flattened. + ExPolygon seg = make_box(0, 0, 1 * mm, 1 * mm); + const BoundingBox flat = get_extents(belt_flatten(ExPolygons{ seg }, frame)); + const coord_t span_u = from_axis == 0 ? flat.max.x() - flat.min.x() + : flat.max.y() - flat.min.y(); + CHECK_THAT(unscale(span_u), Catch::Matchers::WithinRel(stretch, 1e-5)); + } +} + +SCENARIO("belt_brim_line_positions walks an exact lattice", "[BeltBrim]") { + const coord_t pitch = 420; // arbitrary units; the point is exactness + const coord_t anchor = 1000; + + GIVEN("a band narrower than the pitch containing no lattice point") { + // Between anchor+0 and anchor+pitch, pick a window that misses both. + const std::vector us = belt_brim_line_positions(anchor + 100, anchor + 300, pitch, anchor); + THEN("nothing is emitted") { CHECK(us.empty()); } + } + GIVEN("a band containing exactly one lattice point") { + const std::vector us = belt_brim_line_positions(anchor - 10, anchor + 10, pitch, anchor); + THEN("that point is emitted") { + REQUIRE(us.size() == 1); + CHECK(us.front() == anchor); + } + } + GIVEN("a wide band, as at a shallow belt tilt") { + const std::vector us = belt_brim_line_positions(anchor, anchor + 5 * pitch, pitch, anchor); + THEN("several lines are emitted at exactly the pitch") { + REQUIRE(us.size() == 5); + for (size_t i = 1; i < us.size(); ++ i) + CHECK(us[i] - us[i - 1] == pitch); + } + } + GIVEN("two adjacent bands sharing a boundary") { + // Half-open ownership: a lattice point landing on the shared bound belongs + // to the upper band only, so no line is duplicated or dropped. + const coord_t bound = anchor + 2 * pitch; + const std::vector lower = belt_brim_line_positions(anchor, bound, pitch, anchor); + const std::vector upper = belt_brim_line_positions(bound, bound + 2 * pitch, pitch, anchor); + THEN("the boundary point appears exactly once, in the upper band") { + CHECK(std::count(lower.begin(), lower.end(), bound) == 0); + CHECK(std::count(upper.begin(), upper.end(), bound) == 1); + CHECK(lower.size() == 2); + CHECK(upper.size() == 2); + } + } + GIVEN("a lattice anchored below zero") { + THEN("negative lattice points are handled") { + const std::vector us = belt_brim_line_positions(-3 * pitch, -pitch, pitch, 0); + REQUIRE(us.size() == 2); + CHECK(us.front() == -3 * pitch); + CHECK(us.back() == -2 * pitch); + } + } + GIVEN("a degenerate pitch or band") { + THEN("nothing is emitted rather than looping forever") { + CHECK(belt_brim_line_positions(0, 1000, 0, 0).empty()); + CHECK(belt_brim_line_positions(1000, 1000, pitch, 0).empty()); + CHECK(belt_brim_line_positions(1000, 500, pitch, 0).empty()); + } + } +} + +SCENARIO("belt_brim_region reduces to the plate brim without an apron", "[BeltBrim]") { + const coord_t mm = scale_(1.); + const BeltBrimFrame frame { 1.0, 1 }; + const ExPolygons footprint { make_box(0, 0, 20 * mm, 20 * mm) }; + const coord_t width = 3 * mm; + const coord_t gap = 1 * mm; + + GIVEN("outer brim, no apron") { + const ExPolygons region = belt_brim_region(footprint, true, false, width, gap, 0, 0, frame); + THEN("it matches the plate brim ring built from the same offsets") { + const ExPolygons inner = offset_ex(Polygons{ footprint.front().contour }, float(gap), jtRound, SCALED_RESOLUTION); + const ExPolygons outer = offset_ex(inner, float(width), jtRound, SCALED_RESOLUTION); + const ExPolygons expect = diff_ex(outer, inner); + // Not exact: the region is offset from the CLOSED footprint, and closing a + // single convex island is a geometric no-op but still round-trips every + // vertex through a dilate/erode, which perturbs the area in the 8th + // significant figure. + CHECK_THAT(unscale(unscale(area(region))), + Catch::Matchers::WithinRel(unscale(unscale(area(expect))), 1e-6)); + } + } + GIVEN("no outer and no inner brim") { + THEN("the region is empty") { + CHECK(belt_brim_region(footprint, false, false, width, gap, 5 * mm, 0, frame).empty()); + } + } + GIVEN("an apron but no brim width") { + const ExPolygons region = belt_brim_region(footprint, true, false, 0, 0, 5 * mm, 0, frame); + THEN("brim appears only downhill of the footprint") { + REQUIRE(! region.empty()); + const BoundingBox rb = get_extents(region); + // shear > 0 means downhill is -u, and from_axis 1 means u is Y. + CHECK(rb.min.y() < 0); + CHECK(rb.max.y() <= 0 + 2); // nothing above the footprint's own base + } + } +} + +SCENARIO("The apron follows the sign of the shear", "[BeltBrim]") { + // Guards the one sign convention that is easiest to get backwards: which way + // is downhill, i.e. which way the belt carries the part. + const coord_t mm = scale_(1.); + const ExPolygons footprint { make_box(0, 0, 20 * mm, 20 * mm) }; + + const ExPolygons pos = belt_brim_region(footprint, true, false, 0, 0, 5 * mm, 0, BeltBrimFrame{ 1.0, 1 }); + const ExPolygons neg = belt_brim_region(footprint, true, false, 0, 0, 5 * mm, 0, BeltBrimFrame{ -1.0, 1 }); + REQUIRE(! pos.empty()); + REQUIRE(! neg.empty()); + CHECK(get_extents(pos).min.y() < 0); + CHECK(get_extents(neg).max.y() > 20 * mm); +} + +SCENARIO("Outer belt brim does not fill the space between contact islands", "[BeltBrim]") { + // A belt contact patch is often a narrow, broken-up strip. Offsetting each island + // outwards by brim_width merges the rings of any two islands closer than + // 2 x brim_width and fills the space between them - and on a belt that space is + // UNDERNEATH the part, which is not what "outer brim only" means. Closing the + // footprint before the outward offset is what prevents it. + const coord_t mm = scale_(1.); + const BeltBrimFrame frame { 1.0, 1 }; + const coord_t width = 3 * mm; + + GIVEN("two contact islands 4 mm apart, closer than 2 x brim width") { + const ExPolygons footprint { + make_box(0, 0, 10 * mm, 10 * mm), + make_box(14 * mm, 0, 24 * mm, 10 * mm), + }; + const ExPolygons region = belt_brim_region(footprint, true, false, width, 0, 0, 0, frame); + THEN("no brim is placed in the gap between them") { + REQUIRE(! region.empty()); + // Midpoint of the gap, and a point just inside either edge of it. + for (const coord_t x : { 12 * mm, coord_t(10.5 * mm), coord_t(13.5 * mm) }) { + const Point probe(x, 5 * mm); + bool covered = false; + for (const ExPolygon &ex : region) + if (ex.contains(probe)) { covered = true; break; } + CHECK(! covered); + } + } + THEN("brim is still placed outside the pair") { + bool outside_covered = false; + const Point probe(-1 * mm, 5 * mm); // 1 mm left of the left island + for (const ExPolygon &ex : region) + if (ex.contains(probe)) { outside_covered = true; break; } + CHECK(outside_covered); + } + } + + GIVEN("an apron on a fragmented footprint") { + const ExPolygons footprint { + make_box(0, 0, 10 * mm, 10 * mm), + make_box(14 * mm, 0, 24 * mm, 10 * mm), + }; + // Closing fills concavities only, so an outward protrusion such as the apron must + // survive it untouched. + const ExPolygons region = belt_brim_region(footprint, true, false, width, 0, 5 * mm, 0, frame); + THEN("the apron still reaches downhill") { + REQUIRE(! region.empty()); + CHECK(get_extents(region).min.y() <= -5 * mm); + } + } +} From b1905ebc20e0f065519909dd0fa6670911a0610a Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Thu, 6 Aug 2026 01:08:44 -0500 Subject: [PATCH 074/224] Belt brim: fixes from review Six issues found by reviewing the previous commit against belt-printer, two of them release-blocking. Data race (high). Print::process() runs generate_support_material() for all objects in a tbb::parallel_for, and make_belt_brim() runs at its tail, but belt_brim_obstacles() read every OTHER object's support_layers() - which a concurrent task may be inside clear_support_layers() deleting. That is a use-after-free, and even when it survives, the obstacle set depends on which object finishes first. Only this object's own supports are consulted now; they are complete at that point. Foreign objects still contribute their slices, which are finished and immutable before the support phase. Apron bands dropped (high), two separate causes. An apron band prints below its own object's first layer, but another object can already be printing at that print_z, in which case process_layer() takes the ordinary path and never emitted the band - the emission is now shared by both paths. Separately, a band whose print_z matched a support layer of the SAME object was overwritten in the print-wide merge, which keeps one record per object per z and could not detect the collision because LayerToPrint::layer() is null for a band. The per-object pairing loop is now a three-way merge over object, support and apron streams, so each object contributes at most one record per z. Multi-instance was far too strict (medium). It refused belt brim for every multi-instance object, killing plain brim width and inner brim too, and only warned when a leading length was set. Only movement ALONG the belt changes an instance's belt-floor Z, so copies side by side ACROSS the belt share one set of bands perfectly well; belt_brim_instances_compatible() now tests just that, and the warning fires whenever the brim is actually suppressed. Apron layer bookkeeping (medium). Apron layers count toward m_layer_count and advance m_layer_index, but emitted no Z/height tags, left m_last_layer_z, m_max_layer_z and m_last_height stale - so the first object layer computed its height against a pre-apron Z - and skipped before_layer_change_gcode and layer_change_gcode entirely. All of that now matches the ordinary path. Obstacle cost (low). belt_brim_obstacles() ran a full-plate union per band. A bounding-box pre-filter drops non-overlapping objects before materialising any polygon, and the union is skipped for trivial inputs. Deliberately unchanged: every apron band still reports cooling layer_id 0. CoolingBuffer uses it for the initial_layer_fan_speed override and the close_fan_the_first_x_layers gate, and every band lies on the belt plane itself, so it is all first-layer material by the only definition that means anything on a belt. Numbering the bands would ramp the fan up while still printing on the belt. Now documented at the assignment rather than left implicit. --- src/libslic3r/BeltBrim.cpp | 49 ++++++++--- src/libslic3r/GCode.cpp | 121 +++++++++++++++++++++++----- src/libslic3r/GCode.hpp | 8 ++ src/libslic3r/Print.cpp | 11 ++- src/libslic3r/Print.hpp | 3 + src/libslic3r/PrintObject.cpp | 28 +++++-- tests/fff_print/test_skirt_brim.cpp | 104 ++++++++++++++++++++++++ 7 files changed, 282 insertions(+), 42 deletions(-) diff --git a/src/libslic3r/BeltBrim.cpp b/src/libslic3r/BeltBrim.cpp index 40ecc60f58..fa119c6609 100644 --- a/src/libslic3r/BeltBrim.cpp +++ b/src/libslic3r/BeltBrim.cpp @@ -324,24 +324,46 @@ static void belt_brim_band_paths(const BeltBrimContext &bc, } } -// Union of everything actually extruded at `print_z` by any object, expressed in -// `self`'s local slicing frame. Includes `self` itself: its slice at this Z can -// overhang outside the belt footprint and land in the brim ring, which the -// flattened brim_object_gap - a belt-plane separation - does not cover. -static Polygons belt_brim_obstacles(const Print &print, const PrintObject &self, coordf_t print_z, coordf_t tol) +// Union of everything extruded at `print_z` that the brim must keep clear of, expressed +// in `self`'s local slicing frame. Includes `self` itself: its slice at this Z can +// overhang outside the belt footprint and land in the brim ring, which the flattened +// brim_object_gap - a belt-plane separation - does not cover. +// +// THREADING: this runs inside posSupportMaterial, which Print::process() executes for all +// objects in a tbb::parallel_for (Print.cpp). Object slices are finished by then and safe +// to read across objects, but SUPPORT layers are not: another object's thread may be +// inside clear_support_layers() - which deletes the SupportLayer pointers - right now, so +// touching a foreign object's support_layers() here is a use-after-free. Only this +// object's own supports are consulted; they are complete, because make_belt_brim() runs at +// the tail of this object's own generate_support_material(). The cost is that the brim +// does not dodge a *different* object's support at the same Z, which needs the objects to +// overlap in the belt direction in the first place. +// `region_bbox` bounds the brim; anything outside it cannot clip a brim line, so whole +// objects are skipped without materialising their polygons. On a typical plate the +// objects do not overlap and every foreign object drops out here, which matters because +// this runs once per band - hundreds of times per object. +static Polygons belt_brim_obstacles(const Print &print, const PrintObject &self, + const BoundingBox ®ion_bbox, coordf_t print_z, coordf_t tol) { const Point shift_self = self.instances().empty() ? Point(0, 0) : self.instances().front().shift_without_plate_offset(); Polygons out; - for (const PrintObject *o : print.objects()) + for (const PrintObject *o : print.objects()) { + const bool is_self = (o == &self); for (const PrintInstance &inst : o->instances()) { const Point delta = inst.shift_without_plate_offset() - shift_self; if (const Layer *l = o->get_layer_at_printz(print_z, tol)) { - Polygons ps = to_polygons(l->lslices); - for (Polygon &p : ps) - p.translate(delta); - polygons_append(out, std::move(ps)); + BoundingBox lb = get_extents(l->lslices); + lb.translate(delta.x(), delta.y()); + if (lb.overlap(region_bbox)) { + Polygons ps = to_polygons(l->lslices); + for (Polygon &p : ps) + p.translate(delta); + polygons_append(out, std::move(ps)); + } } + if (! is_self) + continue; if (const SupportLayer *sl = o->get_support_layer_at_printz(print_z, tol)) { Polygons ps = sl->support_fills.polygons_covered_by_spacing(); for (Polygon &p : ps) @@ -349,6 +371,9 @@ static Polygons belt_brim_obstacles(const Print &print, const PrintObject &self, polygons_append(out, std::move(ps)); } } + } + if (out.size() < 2) + return out; // union_() of 0 or 1 polygons is pure overhead return union_(out); } @@ -453,7 +478,7 @@ void make_belt_brim(PrintObject &object) std::vector areas_by_layer(nlayers); for (size_t i = 0; i < nlayers; ++ i) { const Layer &layer = *object.layers()[i]; - const Polygons obstacles = belt_brim_obstacles(print, object, layer.print_z, 0.5 * layer.height); + const Polygons obstacles = belt_brim_obstacles(print, object, bc.region_bbox, layer.print_z, 0.5 * layer.height); belt_brim_band_paths(bc, layer.print_z, layer.height, obstacles, by_layer[i], areas_by_layer[i]); } @@ -473,7 +498,7 @@ void make_belt_brim(PrintObject &object) + bc.ctx.floor_offset() + bc.ctx.z_shift(); if (h > EPSILON) for (coordf_t z = first.print_z - h; z > z_lead - h; z -= h) { - const Polygons obstacles = belt_brim_obstacles(print, object, z, 0.5 * h); + const Polygons obstacles = belt_brim_obstacles(print, object, bc.region_bbox, z, 0.5 * h); BeltBrimBand band; band.print_z = z; band.height = h; diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index f2caca404f..5b76f53ab5 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2125,22 +2125,20 @@ std::vector GCode::collect_layers_to_print(const PrintObjec std::vector> warning_ranges; - // Belt printers: the brim apron is stuck to the belt AHEAD of the part, which - // on a tilted belt means below the object's first layer. Those bands carry no - // object or support layer, so they are emitted first and handled by - // process_layer()'s brim-only branch. Already ordered lowest print_z first. - for (const BeltBrimBand &band : object.belt_brim_prologue()) { - LayerToPrint prologue_layer; - prologue_layer.belt_brim_band = &band; - prologue_layer.original_object = &object; - layers_to_print.push_back(prologue_layer); - } - // Pair the object layers with the support layers by z. + // + // Belt printers add a third stream: brim apron bands, which sit on the belt AHEAD of + // the part and so print below the object's first layer. They are merged here rather + // than pushed as standalone records, because a band's print_z can coincide with a + // support layer of this same object - and the print-wide merge downstream keeps only + // one record per object per z, so a standalone band would be silently overwritten. size_t idx_object_layer = 0; size_t idx_support_layer = 0; + size_t idx_brim_band = 0; + const auto &brim_bands = object.belt_brim_prologue(); // ordered by ascending print_z const LayerToPrint* last_extrusion_layer = nullptr; - while (idx_object_layer < object.layers().size() || idx_support_layer < object.support_layers().size()) { + while (idx_object_layer < object.layers().size() || idx_support_layer < object.support_layers().size() + || idx_brim_band < brim_bands.size()) { LayerToPrint layer_to_print; double print_z_min = std::numeric_limits::max(); if (idx_object_layer < object.layers().size()) { @@ -2153,6 +2151,11 @@ std::vector GCode::collect_layers_to_print(const PrintObjec print_z_min = std::min(print_z_min, layer_to_print.support_layer->print_z); } + if (idx_brim_band < brim_bands.size()) { + layer_to_print.belt_brim_band = &brim_bands[idx_brim_band++]; + print_z_min = std::min(print_z_min, layer_to_print.belt_brim_band->print_z); + } + if (layer_to_print.object_layer && layer_to_print.object_layer->print_z > print_z_min + EPSILON) { layer_to_print.object_layer = nullptr; --idx_object_layer; @@ -2163,11 +2166,17 @@ std::vector GCode::collect_layers_to_print(const PrintObjec --idx_support_layer; } + if (layer_to_print.belt_brim_band && layer_to_print.belt_brim_band->print_z > print_z_min + EPSILON) { + layer_to_print.belt_brim_band = nullptr; + --idx_brim_band; + } + layer_to_print.original_object = &object; layers_to_print.push_back(layer_to_print); bool has_extrusions = (layer_to_print.object_layer && layer_to_print.object_layer->has_extrusions()) - || (layer_to_print.support_layer && layer_to_print.support_layer->has_extrusions()); + || (layer_to_print.support_layer && layer_to_print.support_layer->has_extrusions()) + || (layer_to_print.belt_brim_band && ! layer_to_print.belt_brim_band->fills.empty()); // Check that there are extrusions on the very first layer. The case with empty // first layer may result in skirt/brim in the air and maybe other issues. @@ -5266,20 +5275,26 @@ LayerResult GCode::process_belt_brim_layer( const bool last_layer, const size_t single_object_instance_idx) { - // layer_id 0: the apron precedes every object layer and nothing downstream - // indexes by it. spiral_vase_enable false: spiral vase is refused alongside - // belt brim in Print::validate(). cooling_buffer_flush true: an apron layer is - // a complete layer, and the default (object_layer || raft_layer || last_layer) - // would be false here, so fan and slowdown would never be applied to it. + // layer_id 0 is deliberate, not a placeholder. CoolingBuffer reads it for the + // initial_layer_fan_speed override and the close_fan_the_first_x_layers gate + // (CoolingBuffer.cpp), and every apron band is first-layer material by the only + // definition that means anything on a belt: it lies on the belt plane itself. Numbering + // the bands 1, 2, 3... would ramp the fan up while still printing on the belt. + // spiral_vase_enable false: spiral vase is refused alongside belt brim in + // Print::validate(). cooling_buffer_flush true: an apron layer is a complete layer, and + // the default (object_layer || raft_layer || last_layer) is false here, so fan and + // slowdown would otherwise never be applied to it. LayerResult result { {}, 0, false, true }; if (layer_tools.extruders.empty()) // Nothing to extrude. return result; coordf_t print_z = 0.; + coordf_t height = 0.; for (const LayerToPrint <p : layers) if (ltp.belt_brim_band != nullptr) { print_z = ltp.belt_brim_band->print_z; + height = ltp.belt_brim_band->height; break; } @@ -5298,8 +5313,63 @@ LayerResult GCode::process_belt_brim_layer( const unsigned int extruder_id = layer_tools.extruders.front(); if (m_writer->filament() == nullptr || m_writer->filament()->id() != extruder_id) gcode += this->set_extruder(extruder_id, print_z); + + // An apron band is a real printed layer: it is counted in m_layer_count, it advances + // m_layer_index through change_layer(), and the G-code viewer needs its Z/height tags. + // Keep the same caches and hooks the ordinary path maintains, or the first object layer + // would compute its height against a stale pre-apron Z and layer-change templates would + // skip these layers entirely. + { + char buf[64]; + sprintf(buf, ";%s%g\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Layer_Change).c_str(), print_z); + gcode += buf; + sprintf(buf, ";Z:%g\n", print_z); + gcode += buf; + const float band_height = float(height); + sprintf(buf, ";%s%g\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Height).c_str(), band_height); + gcode += buf; + m_last_layer_z = float(print_z); + m_max_layer_z = std::max(m_max_layer_z, m_last_layer_z); + m_last_height = band_height; + } + + if (! m_config.before_layer_change_gcode.value.empty()) { + DynamicConfig config; + config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index + 1)); + config.set_key_value("layer_z", new ConfigOptionFloat(print_z)); + config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); + gcode += this->placeholder_parser_process("before_layer_change_gcode", + print.config().before_layer_change_gcode.value, m_writer->filament()->id(), &config) + "\n"; + } + gcode += this->change_layer(print_z); + if (! m_config.layer_change_gcode.value.empty()) { + DynamicConfig config; + config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index)); + config.set_key_value("layer_z", new ConfigOptionFloat(print_z)); + config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); + gcode += this->placeholder_parser_process("layer_change_gcode", + print.config().layer_change_gcode.value, m_writer->filament()->id(), &config) + "\n"; + } + + gcode += this->emit_belt_brim_bands(print, layers, single_object_instance_idx); + + result.gcode = std::move(gcode); + return result; +} + +// Emit every apron band carried by this set of layers. +// +// Shared by the brim-only branch above and the ordinary process_layer() path. Both need +// it: an apron band prints below its OWN object's first layer, but on a multi-object belt +// another object can already be printing at that print_z, in which case the layer has an +// object layer, takes the ordinary path, and the band would be silently dropped. +std::string GCode::emit_belt_brim_bands(const Print &print, + const std::vector &layers, + const size_t single_object_instance_idx) +{ + std::string gcode; for (const LayerToPrint <p : layers) { const BeltBrimBand *band = ltp.belt_brim_band; if (band == nullptr || band->fills.empty() || ltp.original_object == nullptr) @@ -5324,9 +5394,7 @@ LayerResult GCode::process_belt_brim_layer( m_avoid_crossing_perimeters.disable_once(); } } - - result.gcode = std::move(gcode); - return result; + return gcode; } // Bedslinger model. The heavier the bed load, the lower the achievable Y acceleration for a given @@ -5821,6 +5889,17 @@ LayerResult GCode::process_layer( //BBS: set layer time fan speed after layer change gcode gcode += ";_SET_FAN_SPEED_CHANGING_LAYER\n"; + // Belt printers: an apron band prints below its own object's first layer, but with + // several objects on the belt another one can already be printing at this print_z. + // The layer then has an object layer and takes this ordinary path instead of the + // brim-only branch, so the band has to be emitted here or it would be dropped. + // Before any object extrusion at this Z, as the brim must go down first. + if (print.has_belt_brim()) { + const Vec2d saved_origin = m_origin; + gcode += this->emit_belt_brim_bands(print, layers, single_object_instance_idx); + this->set_origin(saved_origin); + } + //Calibration Layer-specific GCode // ORCA-Belt: on belt printers the calibration object is counter-rotated to // stand upright in slicing space on top of a support wedge, so its first diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index d4c7bf9bc3..54b7e8abd4 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -418,6 +418,14 @@ protected: const bool last_layer, const size_t single_object_instance_idx); + // Emit the apron bands carried by these layers. Called from both the brim-only + // branch and the ordinary path, since a band's print_z can coincide with another + // object's layer on a multi-object belt. + std::string emit_belt_brim_bands( + const Print &print, + const std::vector &layers, + const size_t single_object_instance_idx); + LayerResult process_layer( const Print &print, // Set of object & print layers of the same PrintObject and with the same print_z. diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 189f9e178e..2df4cde79b 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1401,10 +1401,13 @@ StringObjectException Print::validate(std::vector *warnin "brim length."), "brim_object_gap", object->model_object()); - if (ocfg.leading_brim_length.value > 0. && object->instances().size() > 1) - warn(L("This object has several instances sharing one belt position, so no brim is " - "generated for it. Arrange the copies along the belt instead."), - "leading_brim_length", 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 " diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index 72a12918c7..4b1f4891f2 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -392,6 +392,9 @@ public: // trimming and the spiral vase probe, and widening it would perturb belt // support output. bool has_belt_brim() 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& belt_brim_by_layer() const { return m_belt_brim_by_layer; } const std::vector& belt_brim_areas_by_layer() const { return m_belt_brim_areas_by_layer; } const std::vector& belt_brim_prologue() const { return m_belt_brim_prologue; } diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 1b8ddf3a9e..c5d5409d73 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1167,11 +1167,7 @@ bool PrintObject::has_belt_brim() const { if (! m_print->has_tilted_belt()) return false; - // Translating an instance along the belt axis changes its physical belt-floor - // Z, so one set of bands cannot serve several instances sharing a PrintObject. - // belt_force_separate() in PrintApply.cpp gives one instance per object - // whenever a global belt flag is set, which the shipped belt profiles do. - if (m_instances.size() > 1) + if (! this->belt_brim_instances_compatible()) return false; if (m_config.brim_type == btNoBrim) return false; @@ -1181,6 +1177,28 @@ bool PrintObject::has_belt_brim() const return ! this->has_raft(); } +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; + const int axis = m_slicing_params.belt_floor_from_axis; + 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(); diff --git a/tests/fff_print/test_skirt_brim.cpp b/tests/fff_print/test_skirt_brim.cpp index 755dec06e0..b5d315db2a 100644 --- a/tests/fff_print/test_skirt_brim.cpp +++ b/tests/fff_print/test_skirt_brim.cpp @@ -633,3 +633,107 @@ TEST_CASE("Belt brim lines all have the same width", "[SkirtBrim][belt]") const float hi = *std::max_element(widths.begin(), widths.end()); CHECK_THAT(hi, Catch::Matchers::WithinRel(lo, 1e-4)); } + +TEST_CASE("Belt apron survives another object printing at the same Z", "[SkirtBrim][belt]") +{ + // An apron band prints below its OWN object's first layer, but with two objects on the + // belt the second one is already printing at that print_z. The layer then has an + // object layer and takes the ordinary process_layer() path rather than the brim-only + // branch, so the band must be emitted from both or it is silently dropped. A + // single-object print cannot exercise this. + auto brim_passes = [](int object_count) { + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", "outer_only" }, + { "brim_width", 3 }, + { "leading_brim_length", 8 }, + { "brim_object_gap", 0 }, + }); + std::vector meshes; + for (int i = 0; i < object_count; ++ i) { + TriangleMesh m = cube(20); + // Offset along the belt so the second object starts well after the first. + m.translate(0.f, float(40 * i), 0.f); + meshes.emplace_back(std::move(m)); + } + Print print; + Model model; + init_print(std::move(meshes), print, model, config); + print.process(); + return role_passes(gcode(print), "brim"); + }; + + const int one = brim_passes(1); + const int two = brim_passes(2); + REQUIRE(one > 0); + // Two identical objects should carry twice the brim. Merely asserting `two > one` + // would not be decisive: the FIRST object's apron survives the bug, because nothing + // else is printing that early, so only the second object's apron goes missing. + // Requiring close to 2x is what actually detects the dropped bands. + 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 + // the three-way merge in collect_layers_to_print() exists to handle: a band sharing a + // print_z with a support layer of the SAME object used to overwrite it in the + // print-wide merge. A smoke test - it cannot prove the collision occurred - but it + // does exercise the merge with all three streams populated. + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "leading_brim_length", 6 }, + { "brim_object_gap", 0 }, + { "enable_support", 1 }, + }); + const std::string gc = slice({ TestMesh::overhang }, config); + REQUIRE(! gc.empty()); + CHECK(role_passes(gc, "brim") > 0); +} From 849e6493f81a2961a4ed5588d1ce94f3d7760418 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Thu, 6 Aug 2026 03:54:19 -0500 Subject: [PATCH 075/224] Belt brim: offer "Leading edge only" only on belt printers "Leading edge only" describes where a part meets a moving belt, so it has no meaning on a fixed bed and should not clutter the Brim type dropdown there. Filtered the same way support_style and wipe_tower_wall_type already are a few lines above in TabPrint::toggle_options(): the field holds its own copy of the option definition, and Choice maps the combobox selection straight onto that copy's enum_values, so rewriting the values, the labels and the combobox items together keeps the mapping correct. The entry is kept when it is the current value, so opening a project that uses it on a non-belt printer cannot leave the control displaying an option it does not offer - which would silently rewrite the setting on the next edit. Print::validate() already warns that it prints as an ordinary outer brim there. Matches the scope of the existing precedents: the per-object override panel is not filtered. --- src/slic3r/GUI/Tab.cpp | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 7a4cefe4fa..5508b6e099 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -3227,6 +3227,41 @@ void TabPrint::toggle_options() cb->SetValue(n); } + // "Leading edge only" describes where a part meets a moving belt, so it is offered only + // on belt printers. Same pattern as support_style above: the field owns a copy of the + // option definition, and Choice maps the combobox selection straight onto that copy's + // enum_values, so rewriting both together keeps the mapping correct. + field = m_active_page->get_field("brim_type"); + if (auto choice = dynamic_cast(field)) { + bool is_belt_printer = false; + if (m_preset_bundle) { + const auto *belt_opt = m_preset_bundle->printers.get_edited_preset().config.option("belt_printer"); + if (belt_opt) + is_belt_printer = belt_opt->value; + } + auto def = print_config_def.get("brim_type"); + const auto current = m_config->opt_enum("brim_type"); + auto &opt = const_cast(field->m_opt); + auto cb = dynamic_cast(choice->window); + if (cb != nullptr) { + auto n = cb->GetValue(); + opt.enum_values.clear(); + opt.enum_labels.clear(); + cb->Clear(); + for (size_t i = 0; i < def->enum_values.size(); ++ i) { + // Keep the entry if it is already selected, so switching to a non-belt + // printer cannot leave the control showing a value it does not offer. + if (def->enum_values[i] == "leading_edge_only" && ! is_belt_printer + && current != btLeadingEdgeOnly) + continue; + opt.enum_values.push_back(def->enum_values[i]); + opt.enum_labels.push_back(def->enum_labels[i]); + cb->Append(_(def->enum_labels[i])); + } + cb->SetValue(n); + } + } + // BBL printers do not support cone wipe tower field = m_active_page->get_field("wipe_tower_wall_type"); if (auto choice = dynamic_cast(field)) { From 88d5e9e4425c18089140b4811bfe6a2f5d1d162a Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 5 Aug 2026 03:34:04 -0500 Subject: [PATCH 076/224] Fix: reset BuildVolume belt state when leaving belt mode (R1) Non-belt branch of set_bed_shape reset only the 3DBed renderer, not the BuildVolume; Bed3D::set_shape early-returns on unchanged bed, so a belt->normal switch or in-place belt toggle-off left the BuildVolume with m_is_belt_printer=true and inflated Y bounds -> out-of-bounds objects treated as printable on a normal printer. --- src/slic3r/GUI/Plater.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index a67e424fc8..365384ecf9 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -12387,6 +12387,12 @@ void Plater::priv::set_bed_shape(const Pointfs &shape, // G-code load time (GCodeViewer::compute_belt_back_transform), so no mesh-side // inverse needs to be pushed to the viewer here. } else { + // Reset the BuildVolume belt state too: Bed3D::set_shape early-returns when + // the bed params are unchanged, so a belt->normal switch (or toggling belt off + // on the same printer) would otherwise leave the BuildVolume with + // m_is_belt_printer=true and an inflated Y bbox, wrongly treating out-of-bounds + // objects as printable. Idempotent for a printer that was never belt. + bed.build_volume().set_belt_printer(false, 0., false); bed.set_belt_printer(false, 0.f); if (preview) preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(false, 0.f); From 0d921803255337b688587df114f534f3e96bf3cd Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 5 Aug 2026 03:34:04 -0500 Subject: [PATCH 077/224] Fix: clear belt fields in GCodeProcessorResult::reset() (R2) reset() cleared the sibling machine_frame_transform_active but not belt_tilt_angle/belt_z_origin/preslice_remap_*; a reused result carried stale belt metadata into a subsequent normal print, flipping the store_z branch and corrupting start-gcode preview Z for non-belt prints. --- src/libslic3r/GCode/GCodeProcessor.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 43536c30b9..e872a2f652 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -2531,6 +2531,11 @@ void GCodeProcessorResult::reset() { timelapse_warning_code = 0; printable_height = 0.0f; 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; From 10810908a96169cb364cca96d561804484b4e4b4 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 5 Aug 2026 03:35:09 -0500 Subject: [PATCH 078/224] Fix: restore atomic node invalidation in TreeSupport::drop_nodes + drop unused var (R3, R9) The 2-node merge moved the two valid=false writes outside the mutex that upstream held together with the contact_nodes push_back; restore a single critical section per branch (belt branch also guards to_buildplate). Remove an unused top_interface_layers local in drop_nodes. --- src/libslic3r/Support/TreeSupport.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index faed27164f..412104e0ed 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -2859,7 +2859,6 @@ 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 size_t top_interface_layers = config.support_interface_top_layers.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; @@ -3100,19 +3099,21 @@ void TreeSupport::drop_nodes() // 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)) { + std::scoped_lock lock(m_ts_data->m_mutex); node_parent->to_buildplate = false; + neighbour->valid = false; + p_node->valid = false; } else { const bool to_buildplate = !is_inside_ex(get_collision(0, obj_layer_nr_next), next_position); SupportNode* next_node = m_ts_data->create_node(next_position, node_parent->distance_to_top + 1, obj_layer_nr_next, node_parent->support_roof_layers_below - (node_parent->distance_to_top >= 0 ? 1 : 0), to_buildplate, node_parent, print_z_next, height_next); get_max_move_dist(next_node); - m_ts_data->m_mutex.lock(); + std::scoped_lock lock(m_ts_data->m_mutex); contact_nodes[layer_nr_next].push_back(next_node); - m_ts_data->m_mutex.unlock(); + neighbour->valid = false; + p_node->valid = false; } - neighbour->valid = false; - p_node->valid = false; } else if (neighbours.size() > 1) //Don't merge leaf nodes because we would then incur movement greater than the maximum move distance. { From 049612022a8233c2757e4f8b418518fd3f91d22e Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 5 Aug 2026 03:35:58 -0500 Subject: [PATCH 079/224] Perf: avoid unconditional lower-layer polygon copies in support overhang paths (R4, R5) SupportMaterial::detect_overhangs copied lower_layer_polygons per region even without build-plate tilt; hoist the tilted copy out of the region loop and use the original polygons directly when untilted. TreeSupport3D flattened lslices_extrudable to Polygons unconditionally; restore the upstream ExPolygons offset on the untilted path. --- src/libslic3r/Support/SupportMaterial.cpp | 27 ++++++++++++++--------- src/libslic3r/Support/TreeSupport3D.cpp | 7 ++++-- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/libslic3r/Support/SupportMaterial.cpp b/src/libslic3r/Support/SupportMaterial.cpp index ae5ba5849e..9230807b4b 100644 --- a/src/libslic3r/Support/SupportMaterial.cpp +++ b/src/libslic3r/Support/SupportMaterial.cpp @@ -1475,6 +1475,19 @@ static inline ExPolygons detect_overhangs( } } + // Apply build plate tilt: shift lower layer polygons to simulate tilted gravity. + // This is loop-invariant across regions, so compute it once here. + const Polygons *effective_lower = &lower_layer_polygons; + Polygons tilted_lower; + if (has_tilt) { + tilted_lower = lower_layer_polygons; + const double lh = lower_layer.height; + Point tilt_shift(coord_t(scale_(lh * tan(tilt_y_rad))), + coord_t(scale_(lh * tan(tilt_x_rad)))); + translate(tilted_lower, tilt_shift); + effective_lower = &tilted_lower; + } + for (LayerRegion *layerm : layer.regions()) { // Extrusion width accounts for the roundings of the extrudates. // It is the maximum widh of the extrudate. @@ -1491,17 +1504,9 @@ static inline ExPolygons detect_overhangs( // Overhang polygons for this layer and region. Polygons diff_polygons; Polygons layerm_polygons = to_polygons(layerm->slices.surfaces); - // Apply build plate tilt: shift lower layer polygons to simulate tilted gravity - Polygons effective_lower = lower_layer_polygons; - if (has_tilt) { - const double lh = lower_layer.height; - Point tilt_shift(coord_t(scale_(lh * tan(tilt_y_rad))), - coord_t(scale_(lh * tan(tilt_x_rad)))); - translate(effective_lower, tilt_shift); - } if (lower_layer_offset == 0.f) { // Support everything. - diff_polygons = diff(layerm_polygons, effective_lower); + diff_polygons = diff(layerm_polygons, *effective_lower); if (buildplate_only) { // Don't support overhangs above the top surfaces. // This step is done before the contact surface is calculated by growing the overhang region. @@ -1512,7 +1517,7 @@ static inline ExPolygons detect_overhangs( //FIXME cache the lower layer offset if this layer has multiple regions. diff_polygons = diff(layerm_polygons, - expand(effective_lower, lower_layer_offset, SUPPORT_SURFACES_OFFSET_PARAMETERS)); + expand(*effective_lower, lower_layer_offset, SUPPORT_SURFACES_OFFSET_PARAMETERS)); if (buildplate_only && ! annotations.buildplate_covered[layer_id].empty()) { // Don't support overhangs above the top surfaces. // This step is done before the contact surface is calculated by growing the overhang region. @@ -1522,7 +1527,7 @@ static inline ExPolygons detect_overhangs( // Offset the support regions back to a full overhang, restrict them to the full overhang. // This is done to increase size of the supporting columns below, as they are calculated by // propagating these contact surfaces downwards. - diff_polygons = diff(intersection(expand(diff_polygons, lower_layer_offset, SUPPORT_SURFACES_OFFSET_PARAMETERS), layerm_polygons), effective_lower); + diff_polygons = diff(intersection(expand(diff_polygons, lower_layer_offset, SUPPORT_SURFACES_OFFSET_PARAMETERS), layerm_polygons), *effective_lower); } //FIXME add user defined filtering here based on minimal area or minimum radius or whatever. diff --git a/src/libslic3r/Support/TreeSupport3D.cpp b/src/libslic3r/Support/TreeSupport3D.cpp index 702815e5bb..66b4338d24 100644 --- a/src/libslic3r/Support/TreeSupport3D.cpp +++ b/src/libslic3r/Support/TreeSupport3D.cpp @@ -260,14 +260,17 @@ static std::vector>> group_me } else lower_layer_offset = scaled(lower_layer.height / tan_threshold); // Apply build plate tilt: shift lower layer polygons to simulate tilted gravity - Polygons lower_src = to_polygons(lower_layer.lslices_extrudable); + Polygons lower_layer_offseted; if (has_tilt) { + Polygons lower_src = to_polygons(lower_layer.lslices_extrudable); const double lh = lower_layer.height; Point tilt_shift(coord_t(scale_(lh * tan(tilt_y_rad))), coord_t(scale_(lh * tan(tilt_x_rad)))); translate(lower_src, tilt_shift); + lower_layer_offseted = offset(lower_src, lower_layer_offset); + } else { + lower_layer_offseted = offset(lower_layer.lslices_extrudable, lower_layer_offset); } - Polygons lower_layer_offseted = offset(lower_src, lower_layer_offset); overhangs = diff(current_layer.lslices_extrudable, lower_layer_offseted); if (lower_layer_offset == 0) { raw_overhangs = overhangs; From f8fe5a07cdbd026a08550cf809ed3aaad70495f5 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 5 Aug 2026 03:35:25 -0500 Subject: [PATCH 080/224] Perf: hoist frame-invariant slope up_direction/normal_z out of the per-volume render loop (R6) Belt slope-shading changes recomputed up_direction (with a printer-preset config lookup) and normal_z per volume; both are frame-invariant. Compute once before the to_render loop and reuse the already-hoisted support_normal_z. Uniforms are still set per volume; visuals unchanged. --- src/slic3r/GUI/3DScene.cpp | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index 6f7ca47919..1dd11bccf6 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -1075,6 +1075,20 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type, const float support_normal_z = get_selection_support_normal_z(); + // Compute up direction accounting for build plate tilt. This is frame-invariant + // (config cannot change mid-render), so compute it once before the volume loop. + Vec3f up_direction = Vec3f::UnitZ(); + { + const DynamicPrintConfig& prt_cfg = GUI::wxGetApp().preset_bundle->printers.get_edited_preset().config; + double tilt_x_deg = prt_cfg.opt_float("build_plate_tilt_x"); + double tilt_y_deg = prt_cfg.opt_float("build_plate_tilt_y"); + if (tilt_x_deg != 0. || tilt_y_deg != 0.) { + double tilt_x_rad = Geometry::deg2rad(tilt_x_deg); + double tilt_y_rad = Geometry::deg2rad(tilt_y_deg); + up_direction = Vec3f(float(tan(tilt_y_rad)), float(tan(tilt_x_rad)), 1.f).normalized(); + } + } + for (GLVolumeWithIdAndZ& volume : to_render) { #if ENABLE_MODIFIERS_ALWAYS_TRANSPARENT if (type == ERenderType::Transparent) { @@ -1132,21 +1146,6 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type, shader->set_uniform("print_volume.type", -1); } - const float normal_z = get_selection_support_normal_z(); - - // Compute up direction accounting for build plate tilt - Vec3f up_direction = Vec3f::UnitZ(); - { - const DynamicPrintConfig& prt_cfg = GUI::wxGetApp().preset_bundle->printers.get_edited_preset().config; - double tilt_x_deg = prt_cfg.opt_float("build_plate_tilt_x"); - double tilt_y_deg = prt_cfg.opt_float("build_plate_tilt_y"); - if (tilt_x_deg != 0. || tilt_y_deg != 0.) { - double tilt_x_rad = Geometry::deg2rad(tilt_x_deg); - double tilt_y_rad = Geometry::deg2rad(tilt_y_deg); - up_direction = Vec3f(float(tan(tilt_y_rad)), float(tan(tilt_x_rad)), 1.f).normalized(); - } - } - // Per-extruder printable-height shading. The flag is set to // 2.0 only for multi-extruder printers (two per-extruder heights); otherwise it is forced to 0.0 // on every render so no stale flag survives a multi->single-extruder plate switch, keeping the @@ -1167,7 +1166,7 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type, shader->set_uniform("volume_world_matrix", volume.first->world_matrix()); shader->set_uniform("slope.actived", m_slope.isGlobalActive && !volume.first->is_modifier && !volume.first->is_wipe_tower); shader->set_uniform("slope.volume_world_normal_matrix", static_cast(volume.first->world_matrix().matrix().block(0, 0, 3, 3).inverse().transpose().cast())); - shader->set_uniform("slope.normal_z", normal_z); + shader->set_uniform("slope.normal_z", support_normal_z); shader->set_uniform("slope.up_direction", up_direction); #if ENABLE_ENVIRONMENT_MAP From 6fd2de76e6b9274b4811d8f92cfc8c9acd00416c Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 5 Aug 2026 03:35:12 -0500 Subject: [PATCH 081/224] Fix: preserve upstream select-by-angle behavior when build plate is untilted (R7) select_facets_by_angle replaced upstream's limit.dot(down) threshold with cos(threshold), changing facet selection for non-uniformly-scaled/mirror objects on ALL printers. Restore the exact upstream computation when no build-plate tilt is active; keep the tilted-gravity form only under tilt. --- src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp index 26afdbbe33..c0d791aa00 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp @@ -567,7 +567,12 @@ void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block) auto [tilt_x_deg, tilt_y_deg] = get_build_plate_tilt(); double tilt_x_rad = tilt_x_deg * M_PI / 180.0; double tilt_y_rad = tilt_y_deg * M_PI / 180.0; - Vec3d gravity_dir = Vec3d(-tan(tilt_y_rad), -tan(tilt_x_rad), -1.0).normalized(); + const bool has_tilt = (tilt_x_deg != 0. || tilt_y_deg != 0.); + // NB: use an if, not a ?:, so each branch converts to Vec3d independently + // (the two Eigen expression types don't unify in a ternary). + Vec3d gravity_dir = -Vec3d::UnitZ(); + if (has_tilt) + gravity_dir = Vec3d(-tan(tilt_y_rad), -tan(tilt_x_rad), -1.0).normalized(); int mesh_id = -1; for (const ModelVolume* mv : mo->volumes) { @@ -578,7 +583,16 @@ void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block) const Transform3d trafo_matrix = mi->get_matrix_no_offset() * mv->get_matrix_no_offset(); Vec3f down = (trafo_matrix.inverse() * gravity_dir).cast().normalized(); - float dot_limit = std::cos(threshold); + float dot_limit; + if (!has_tilt) { + // Exact upstream computation: threshold derived from a tilted limit + // vector transformed into mesh space, so non-uniform/mirror/shear + // transforms behave identically to upstream. + Vec3f limit = (trafo_matrix.inverse() * Vec3d(std::sin(threshold), 0, -std::cos(threshold))).cast().normalized(); + dot_limit = limit.dot(down); + } else { + dot_limit = std::cos(threshold); + } // Now calculate dot product of vert_direction and facets' normals. int idx = 0; From 7fc86db5f0c3876d641dc278d13ff4a93da4428b Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 5 Aug 2026 03:37:48 -0500 Subject: [PATCH 082/224] Fix: only clear belt-derived build_plate_tilt on genuine belt->off transition (R8) update_fff() zeroed any build_plate_tilt matching the dormant belt-derived tilt (default X/45) within 0.01, wiping a legitimate manual tilt on a non-belt tilted-bed printer. Track the belt->non-belt transition and the exact values belt-sync wrote, clearing only those on an in-place toggle; reset tracking on preset load so preset switches never wipe tilt. --- src/slic3r/GUI/Tab.cpp | 43 +++++++++++++++++++++++++++++++----------- src/slic3r/GUI/Tab.hpp | 7 +++++++ 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 5508b6e099..8de8bf6e84 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -5875,6 +5875,18 @@ if (is_marlin_flavor) // this gets executed after preset is loaded and before GUI fields are updated void TabPrinter::on_preset_loaded() { + // R8: reset the belt-tilt transition tracking to reflect the freshly loaded preset WITHOUT + // running update_fff()'s reset logic. on_preset_loaded() is called from Tab::load_current_preset() + // on every printer preset load, right before update()->update_fff(). Seeding m_was_belt_printer + // from the loaded preset's belt_printer flag means a preset switch (belt preset -> non-belt preset) + // enters update_fff() with m_was_belt_printer==false, so the belt->off clear branch is skipped and + // the newly loaded preset's manual tilt is preserved. An in-place belt toggle does NOT go through + // here (only through on_value_change->update), so m_was_belt_printer stays true there and the clear + // still fires. Seed m_belt_synced_tilt from the loaded tilt as a safeguard. + m_was_belt_printer = m_config->opt_bool("belt_printer"); + m_belt_synced_tilt_x = m_config->opt_float("build_plate_tilt_x"); + m_belt_synced_tilt_y = m_config->opt_float("build_plate_tilt_y"); + // Orca //update nozzle_volume_type const Preset& current_printer = m_preset_bundle->printers.get_selected_preset(); @@ -6477,9 +6489,15 @@ void TabPrinter::update_fff() // Belt printer: auto-sync build_plate_tilt_{x,y} (which drives support gravity tilt) // from the belt slicing rotation, the single source of truth for the physical tilt. - // Tilt about X drives tilt_x, tilt about Y drives tilt_y. When belt mode is off, - // reset whichever tilt axis matches a leftover belt value so we don't clobber a - // manually-set tilt on a non-belt tilted printer. + // Tilt about X drives tilt_x, tilt about Y drives tilt_y. + // + // R8: value-guessing (zeroing any tilt matching the dormant belt-derived tilt) wiped a + // legitimate manual build_plate_tilt on a non-belt tilted-bed printer, because the belt + // defaults (rotation=X, angle=45) make a manual tilt of 45 look belt-derived. Instead we + // track the belt->non-belt transition and the exact values belt-sync wrote, and clear the + // tilt only on a genuine in-place belt-off toggle, and only if the value is still what + // belt-sync last wrote. Preset switches reset the tracking in on_preset_loaded(), so they + // never trip the reset. if (m_config->opt_bool("belt_printer")) { auto rot_axis = m_config->option>("belt_slice_rotation")->value; const auto tilt = BeltTransformPipeline::physical_tilt( @@ -6488,17 +6506,20 @@ void TabPrinter::update_fff() m_config->set_key_value("build_plate_tilt_x", new ConfigOptionFloat(tilt.tilt_x_deg)); if (m_config->opt_float("build_plate_tilt_y") != tilt.tilt_y_deg) m_config->set_key_value("build_plate_tilt_y", new ConfigOptionFloat(tilt.tilt_y_deg)); - } else { - const auto tilt = BeltTransformPipeline::physical_tilt( - m_config->option>("belt_slice_rotation")->value, - m_config->opt_float("belt_slice_rotation_angle")); - double tx = m_config->opt_float("build_plate_tilt_x"); - double ty = m_config->opt_float("build_plate_tilt_y"); - if (tx != 0. && std::abs(tx - tilt.tilt_x_deg) < 0.01) + // Remember exactly what belt-sync wrote, so an in-place belt-off toggle can distinguish + // a still-belt-derived tilt (safe to clear) from a since-edited manual one (keep). + m_belt_synced_tilt_x = tilt.tilt_x_deg; + m_belt_synced_tilt_y = tilt.tilt_y_deg; + } else if (m_was_belt_printer) { + // Genuine in-place belt->off toggle on the same preset (on_preset_loaded() was not called + // since the last update, so m_was_belt_printer still reflects belt mode). Clear each axis + // only if it still holds the value belt-sync last wrote; a manual override is preserved. + if (m_config->opt_float("build_plate_tilt_x") == m_belt_synced_tilt_x) m_config->set_key_value("build_plate_tilt_x", new ConfigOptionFloat(0.)); - if (ty != 0. && std::abs(ty - tilt.tilt_y_deg) < 0.01) + if (m_config->opt_float("build_plate_tilt_y") == m_belt_synced_tilt_y) m_config->set_key_value("build_plate_tilt_y", new ConfigOptionFloat(0.)); } + m_was_belt_printer = m_config->opt_bool("belt_printer"); toggle_options(); } diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index 9be9bc17f8..996f1b7d97 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -623,6 +623,13 @@ private: bool m_rebuild_kinematics_page = false; void update_input_shaper_menu(GCodeFlavor flavor); + // R8: track the belt->non-belt transition so update_fff() only clears the belt-derived + // build_plate_tilt on a genuine in-place belt-off toggle, never on a manual tilt or a + // preset switch. m_belt_synced_tilt_{x,y} hold the exact values belt-sync last wrote. + bool m_was_belt_printer = false; + double m_belt_synced_tilt_x = 0.; + double m_belt_synced_tilt_y = 0.; + ogStaticText* m_fff_print_host_upload_description_line {nullptr}; ogStaticText* m_sla_print_host_upload_description_line {nullptr}; From c1a90fc451df1c18bd5fef9000e518e9f2c259f3 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 5 Aug 2026 03:39:47 -0500 Subject: [PATCH 083/224] Fix: correct axis-remap G-code emission and belt first-layer travel speed (B2, B3) - BeltGCodeWriter::travel_to_xyz final branch used config.travel_speed instead of the computed first-layer-aware travel_speed. - extrude_to_xyz decided emit_xyz vs emit_xy from pre-remap Z; emit full XYZ whenever an axis remap is active so remapped machine-Z is never dropped. - base travel_to_xyz now applies apply_axis_remap() on all emitted destinations (standalone remap on non-belt printers was unremapped). - spiral/arc travels fall back to normal linear lift under active remap (endpoint-only remap can't preserve arc plane/I-J). - set_axis_remap() is now synced unconditionally each export to avoid a reused writer retaining a stale non-identity mapping. --- src/libslic3r/BeltGCodeWriter.cpp | 4 ++- src/libslic3r/GCode.cpp | 14 +++++----- src/libslic3r/GCodeWriter.cpp | 45 +++++++++++++++++++++++++------ 3 files changed, 48 insertions(+), 15 deletions(-) diff --git a/src/libslic3r/BeltGCodeWriter.cpp b/src/libslic3r/BeltGCodeWriter.cpp index e1dad4177f..4824d76f69 100644 --- a/src/libslic3r/BeltGCodeWriter.cpp +++ b/src/libslic3r/BeltGCodeWriter.cpp @@ -264,7 +264,9 @@ std::string BeltGCodeWriter::travel_to_xyz(const Vec3d &point, const std::string // Belt mode: always emit full XYZ GCodeG1Formatter w; w.emit_xyz(point_on_plate); - w.emit_f(this->config.travel_speed.get_at(m_cached_extruder_idx) * 60.0); + // Use the first-layer-aware travel_speed computed at the top of this function, + // not the raw config travel_speed, so initial-layer travels are correctly slowed. + w.emit_f(travel_speed * 60.0); w.emit_comment(GCodeWriter::full_gcode_comment, comment); m_pos = dest_point; diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 5b76f53ab5..46d8593182 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2952,16 +2952,18 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato this->init_belt_writer(print, is_bbl_printers); // Standalone axis remap (works with or without belt mode). + // Sync the writer's remap state to the current export UNCONDITIONALLY — even at + // the identity mapping (0,1,2) — so a reused writer never retains a stale + // non-identity mapping from a prior export. has_axis_remap() returns false at + // identity, so identity/default output stays unchanged. { int rx = int(print.config().gcode_remap_x.value); int ry = int(print.config().gcode_remap_y.value); int rz = int(print.config().gcode_remap_z.value); - if (rx != 0 || ry != 1 || rz != 2) { - m_writer->set_axis_remap(rx, ry, rz); - BoundingBoxf bbox_bed(print.config().printable_area.values); - m_writer->set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(), - print.config().printable_height.value)); - } + m_writer->set_axis_remap(rx, ry, rz); + BoundingBoxf bbox_bed(print.config().printable_area.values); + m_writer->set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(), + print.config().printable_height.value)); } // Build the FirstLayerPlane evaluator. When inactive (non-belt printers diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index 491317e567..f3566b0a4b 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -936,7 +936,10 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co Vec2d temp = delta_no_z.normalized() * delta(2) / tan(this->filament()->travel_slope()); Vec3d slope_top_point = Vec3d(temp(0), temp(1), delta(2)) + source; GCodeG1Formatter w0; - w0.emit_xyz(slope_top_point); + // A slope lift is a straight (linear) diagonal move, so remapping its + // endpoint is exact. Route the destination through apply_axis_remap() + // when a remap is active (no-op at identity). + w0.emit_xyz(has_axis_remap() ? apply_axis_remap(slope_top_point) : slope_top_point); w0.emit_f(travel_speed * 60.0); //BBS w0.emit_comment(GCodeWriter::full_gcode_comment, comment); @@ -950,7 +953,14 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co std::string xy_z_move; { GCodeG1Formatter w0; - if (this->is_current_position_clear()) { + if (has_axis_remap()) { + // Remap may couple XY with Z; emit full XYZ in machine coordinates. + w0.emit_xyz(apply_axis_remap(target)); + w0.emit_f(travel_speed * 60.0); + w0.emit_comment(GCodeWriter::full_gcode_comment, comment); + xy_z_move = w0.string(); + } + else if (this->is_current_position_clear()) { w0.emit_xyz(target); w0.emit_f(travel_speed * 60.0); w0.emit_comment(GCodeWriter::full_gcode_comment, comment); @@ -988,7 +998,13 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co Vec3d point_on_plate = { dest_point(0) - m_x_offset, dest_point(1) - m_y_offset, dest_point(2) }; std::string out_string; GCodeG1Formatter w; - if (!this->is_current_position_clear()) + if (has_axis_remap()) { + // Remap may couple XY with Z; emit full XYZ in machine coordinates. + w.emit_xyz(apply_axis_remap(point_on_plate)); + w.emit_f(this->config.travel_speed.get_at(m_cached_extruder_idx) * 60.0); + w.emit_comment(GCodeWriter::full_gcode_comment, comment); + out_string = w.string(); + } else if (!this->is_current_position_clear()) { //force to move xy first then z after filament change w.emit_xy(Vec2d(point_on_plate.x(), point_on_plate.y())); @@ -1053,6 +1069,14 @@ std::string GCodeWriter::_travel_to_z(double z, const std::string &comment) std::string GCodeWriter::_spiral_travel_to_z(double z, const Vec2d &ij_offset, const std::string &comment) { + // A circular XY arc / spiral lift cannot be correctly axis-remapped by + // transforming only its endpoint: the arc plane (G17/XY) and the I-J center + // would change under the remap. When an axis remap is active, fall back to a + // plain linear lift instead of emitting a possibly-wrong spiral/arc. This + // single guard covers every spiral call site (lazy/eager lift and travel_to_xyz). + if (has_axis_remap()) + return _travel_to_z(z, comment); + std::string output; double speed = this->config.travel_speed_z.get_at(m_cached_extruder_idx); @@ -1199,14 +1223,19 @@ std::string GCodeWriter::extrude_to_xyz(const Vec3d &point, double dE, const std //BBS: take plate offset into consider Vec3d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset, point(2) }; - if (has_axis_remap()) - point_on_plate = apply_axis_remap(point_on_plate); - GCodeG1Formatter w; - if (z_changed) + if (has_axis_remap()) { + // z_changed was computed from the ORIGINAL slicing Z, but an axis remap can + // make machine-Z depend on slicing X/Y. An X/Y-only move (slicing-Z + // unchanged) would then drop the required machine-Z word, so always emit + // full XYZ whenever a remap is active. + point_on_plate = apply_axis_remap(point_on_plate); w.emit_xyz(point_on_plate); - else + } else if (z_changed) { + w.emit_xyz(point_on_plate); + } else { w.emit_xy(Vec2d(point_on_plate.x(), point_on_plate.y())); + } if (!force_no_extrusion) w.emit_e(filament()->E()); //BBS From 1bd3404c03be5d4dcdbf8f0c245f3fea632b38ef Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Thu, 6 Aug 2026 15:29:40 -0500 Subject: [PATCH 084/224] Fix belt brim emission: dropped first-contact bands, tool selection, inner-only predicate (A,B,C,D) - Emit coincident belt_brim_by_layer bands even when the leading object layer has no InstanceVisit (zero-extrusion lead-in / no coinciding support), so the brim at first belt contact is no longer dropped. - Register each coincident band's brim filament in ToolOrdering and emit each band exactly once, in its brim-filament pass; emit ordinary-layer aprons in the brim pass before object extrusion (correct tool, brim-first) instead of with whatever tool was active. - has_belt_brim(): inner-only brims need brim_width>0 (leading/extra produce no inner geometry), fixing spurious prime-tower/spiral rejection; mirror in wants_brim. Single-extruder/single-object output is unchanged except previously-dropped bands now print. --- src/libslic3r/GCode.cpp | 116 ++++++++++++++++++++++++--- src/libslic3r/GCode/ToolOrdering.cpp | 38 +++++++-- src/libslic3r/Print.cpp | 10 ++- src/libslic3r/Print.hpp | 6 ++ src/libslic3r/PrintObject.cpp | 29 ++++++- 5 files changed, 175 insertions(+), 24 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 46d8593182..976c99fd9a 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -5891,16 +5891,12 @@ LayerResult GCode::process_layer( //BBS: set layer time fan speed after layer change gcode gcode += ";_SET_FAN_SPEED_CHANGING_LAYER\n"; - // Belt printers: an apron band prints below its own object's first layer, but with - // several objects on the belt another one can already be printing at this print_z. - // The layer then has an object layer and takes this ordinary path instead of the - // brim-only branch, so the band has to be emitted here or it would be dropped. - // Before any object extrusion at this Z, as the brim must go down first. - if (print.has_belt_brim()) { - const Vec2d saved_origin = m_origin; - gcode += this->emit_belt_brim_bands(print, layers, single_object_instance_idx); - this->set_origin(saved_origin); - } + // Belt printers: ordinary-layer apron bands (a band whose print_z coincides with an + // object/support layer, so it takes this path rather than the brim-only branch) are + // NOT emitted here anymore. They used to be laid down with whatever tool happened to + // be active; instead they are now emitted inside the extruder loop below, in their + // own brim-filament pass and before that pass's object extrusion, so the brim goes + // down first with the correct tool. See the emit_belt_brim_for_extruder call. //Calibration Layer-specific GCode // ORCA-Belt: on belt printers the calibration object is counter-rotated to @@ -6584,6 +6580,49 @@ LayerResult GCode::process_layer( // Extrude the skirt, brim, support, perimeters, infill ordered by the extruders. m_skirt_group_done.resize(print.skirt_brim_groups().size()); + + // Belt brim bookkeeping. A coincident belt_brim_by_layer band must be emitted + // exactly once, in its object's brim-filament pass; this records which have gone + // down so the in-visit emit and the end-of-layer orphan sweep never double it. + // Key = (LayerToPrint index, instance_id). + std::set> belt_brim_emitted; + + // Emit every ORDINARY-layer apron band (belt_brim_prologue band coinciding with an + // object/support layer) whose brim filament is this pass's extruder. Mirrors + // emit_belt_brim_bands() per band, but filtered to one brim filament so each band + // prints in the correct tool's pass (Finding B). extruder_id is 0-based (the + // reindexed tool domain); belt_brim_filament() is 1-based, so subtract one. + auto emit_belt_brim_for_extruder = [this, &print, &layers, single_object_instance_idx](unsigned int extruder_id) -> std::string { + std::string gc; + for (const LayerToPrint <p : layers) { + const BeltBrimBand *band = ltp.belt_brim_band; + if (band == nullptr || band->fills.empty() || ltp.original_object == nullptr) + continue; + const PrintObject &object = *ltp.original_object; + if (! object.has_belt_brim() || (unsigned int)(object.belt_brim_filament() - 1) != extruder_id) + continue; + // Speeds, flow and retraction all read m_config. + m_config.apply(print.default_region_config()); + m_config.apply(object.config(), true); + const size_t i_begin = single_object_instance_idx == size_t(-1) ? 0 : single_object_instance_idx; + const size_t i_end = single_object_instance_idx == size_t(-1) ? object.instances().size() + : single_object_instance_idx + 1; + for (size_t i = i_begin; i < i_end && i < object.instances().size(); ++ i) { + // Band geometry is object-local, like the object's own extrusions. + const Point &offset = object.instances()[i].shift; + this->set_origin(unscale(offset)); + this->on_set_origin(&object, offset); + m_avoid_crossing_perimeters.use_external_mp(); + for (const ExtrusionEntity *ee : band->fills.entities) + if (ee != nullptr) + gc += this->extrude_entity(*ee, "brim", NOZZLE_CONFIG(support_speed)); + m_avoid_crossing_perimeters.use_external_mp(false); + m_avoid_crossing_perimeters.disable_once(); + } + } + return gc; + }; + for (unsigned int extruder_id : layer_tools.extruders) { if (print.config().skirt_type == stCombined && !print.skirt_brim_groups().empty()) { @@ -6681,6 +6720,16 @@ LayerResult GCode::process_layer( if (layer_tools.has_wipe_tower && m_wipe_tower) m_last_processor_extrusion_role = erWipeTower; + // Belt printers: now that this pass's tool is selected, lay down any ordinary-layer + // apron band whose brim filament is this extruder, before the object extrusion at + // this Z (brim goes down first, with the correct tool). Restore the origin so the + // object-setup code below is unaffected. + if (print.has_belt_brim()) { + const Vec2d saved_origin = m_origin; + gcode += emit_belt_brim_for_extruder(extruder_id); + this->set_origin(saved_origin); + } + auto &filament_plan = filament_to_print_instances[extruder_id]; std::vector &instances_to_print = filament_plan.first; const std::vector &instance_visits = filament_plan.second; @@ -6697,8 +6746,20 @@ LayerResult GCode::process_layer( const LayerToPrint &layer_to_print = layers[instance_to_print.layer_id]; if (visit.first_visit && print_wipe_extrusions == (is_anything_overridden ? 1 : 0)) { gcode += generate_object_skirt_group(print, instance_to_print.print_object, instance_to_print.instance_id, layer_tools, layer, extruder_id); - gcode += generate_object_brim(print, instance_to_print.print_object, instance_to_print.instance_id, first_layer, - layer_to_print.object_layer); + const PrintObject &vobj = instance_to_print.print_object; + if (vobj.has_belt_brim()) { + // Coincident belt brim: emit once, only in this object's brim-filament + // pass (extruder_id and belt_brim_filament()-1 are both 0-based here), + // and dedup on the LayerToPrint index (not Layer::id()) so the orphan + // sweep below never re-emits it. + if (extruder_id == (unsigned int)(vobj.belt_brim_filament() - 1) && + belt_brim_emitted.insert({ instance_to_print.layer_id, instance_to_print.instance_id }).second) + gcode += generate_object_brim(print, vobj, instance_to_print.instance_id, first_layer, + layer_to_print.object_layer); + } else { + gcode += generate_object_brim(print, vobj, instance_to_print.instance_id, first_layer, + layer_to_print.object_layer); + } } // To control print speed of the 1st object layer printed over raft interface. @@ -6886,6 +6947,37 @@ LayerResult GCode::process_layer( } } } + + // Belt brim orphan sweep (Finding C). A coincident belt_brim_by_layer band lives on + // an object layer, but that layer can yield no InstanceVisit above - a zero-extrusion + // lead-in slice with no coinciding support - so the in-visit emit never fired and the + // band would be dropped. Emit any such band exactly once here, keyed the same way as + // the in-visit emit so already-printed bands are skipped. These orphan layers carry + // no object material, so ending on the brim's position is harmless; we still save and + // restore m_origin, and only toolchange when the brim filament differs from the active + // one - a no-op on single-extruder prints, keeping their output unchanged. + if (print.has_belt_brim()) { + const Vec2d saved_origin = m_origin; + for (const LayerToPrint <p : layers) { + const PrintObject *obj = ltp.original_object; + if (obj == nullptr || ! obj->has_belt_brim() || ltp.object_layer == nullptr) + continue; + const size_t ltp_idx = size_t(<p - layers.data()); + const unsigned int brim0 = (unsigned int)(obj->belt_brim_filament() - 1); + const size_t i_begin = single_object_instance_idx == size_t(-1) ? 0 : single_object_instance_idx; + const size_t i_end = single_object_instance_idx == size_t(-1) ? obj->instances().size() + : single_object_instance_idx + 1; + for (size_t instance_id = i_begin; instance_id < i_end && instance_id < obj->instances().size(); ++ instance_id) { + if (! belt_brim_emitted.insert({ ltp_idx, instance_id }).second) + continue; + if (m_writer->filament() == nullptr || m_writer->filament()->id() != brim0) + gcode += this->set_extruder(brim0, print_z); + gcode += generate_object_brim(print, *obj, instance_id, first_layer, ltp.object_layer); + } + } + this->set_origin(saved_origin); + } + if (first_layer) { for (auto iter = by_extruder.begin(); iter != by_extruder.end(); ++iter) { if (!iter->second.empty()) diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index e0520a3694..f96748848f 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -876,14 +876,9 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto // push. Deliberately not layer_tools.has_object, which drives skirt marking // and wiping overrides. if (! object.belt_brim_prologue().empty()) { - unsigned int brim_filament = 0; - for (size_t i = 0; i < object.num_printing_regions(); ++ i) { - const unsigned int f = object.printing_region(i).config().outer_wall_filament_id.value; - if (f > 0 && (brim_filament == 0 || f < brim_filament)) - brim_filament = f; - } - if (brim_filament == 0) - brim_filament = 1; + // 1-based, same domain the object/support pushes above use; reindexed to 0-based + // with the rest of the list later. + const unsigned int brim_filament = object.belt_brim_filament(); for (const BeltBrimBand &band : object.belt_brim_prologue()) { if (band.fills.empty()) continue; @@ -893,6 +888,25 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto } } + // Coincident brim bands (belt_brim_by_layer) print ON an object layer rather than + // below it, but that layer can produce no InstanceVisit in process_layer - a + // zero-extrusion lead-in slice with no coinciding support - and the band would then + // be silently dropped. Register the brim filament on every layer that carries a + // coincident band, in the same 1-based domain as the prologue push above, so a brim + // pass always exists there. + if (object.has_belt_brim()) { + const unsigned int brim_filament = object.belt_brim_filament(); + const auto &by_layer = object.belt_brim_by_layer(); + const size_t n = std::min(by_layer.size(), object.layers().size()); + for (size_t i = 0; i < n; ++ i) { + if (by_layer[i].empty()) + continue; + LayerTools &layer_tools = this->tools_for_layer(object.layers()[i]->print_z); + layer_tools.extruders.push_back(brim_filament); + layer_tools.has_belt_brim = true; + } + } + for (auto& layer : m_layer_tools) { // Sort and remove duplicates sort_remove_duplicates(layer.extruders); @@ -940,6 +954,14 @@ void ToolOrdering::fill_wipe_tower_partitions(const PrintConfig &config, coordf_ // below the object's first layer, and treating those layers as raft would put a // wipe tower at negative Z. Belt brim and the prime tower are mutually // exclusive (rejected in Print::validate()), so simply drop the clause there. + // + // Gate on config.belt_printer, NOT on has_belt_brim: every layer below the + // object bottom on a belt printer is legitimately a sub-object stream - brim + // apron, belt support printed below Z0, or the object's own lead-in - and none of + // them is ever raft, because Print::validate() rejects raft_layers>0 on a belt + // printer outright. Narrowing this to has_belt_brim would reclassify + // belt-support-below-floor layers as raft on brim-less belt prints and reintroduce + // the negative-Z wipe tower, so the broad belt_printer gate is correct. const bool belt_no_raft_gap = config.belt_printer.value; for (LayerTools < : m_layer_tools) lt.has_wipe_tower |= (lt.has_object && (config.timelapse_type == TimelapseType::tlSmooth || lt.wipe_tower_partitions > 0)) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 2df4cde79b..017fa19736 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1374,9 +1374,15 @@ StringObjectException Print::validate(std::vector *warnin for (const PrintObject *object : m_objects) { const PrintObjectConfig &ocfg = object->config(); + // Mirror PrintObject::has_belt_brim(): an inner-only brim needs a positive + // brim_width (leading/extra widen only the outer ring), so keep this + // predicate in step or the belt-brim warnings below would fire for a brim + // that has_belt_brim() rejects. const bool wants_brim = ocfg.brim_type != btNoBrim - && (ocfg.brim_width.value > 0. || ocfg.leading_brim_length.value > 0. - || ocfg.extra_brim_width.value > 0.); + && (ocfg.brim_type == btInnerOnly + ? ocfg.brim_width.value > 0. + : (ocfg.brim_width.value > 0. || ocfg.leading_brim_length.value > 0. + || ocfg.extra_brim_width.value > 0.)); if (! wants_brim) continue; diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index 4b1f4891f2..ff5a505360 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -392,6 +392,12 @@ public: // trimming and the spiral vase probe, and widening it would perturb belt // support output. bool has_belt_brim() const; + // Brim filament for this object's belt brim, returned in the 1-based domain of + // PrintRegion::outer_wall_filament_id and the raw values pushed into + // LayerTools::extruders in ToolOrdering::collect_extruders (ToolOrdering reindexes + // the whole list to 0-based afterwards). Lowest positive outer_wall_filament_id + // over the printing regions, 1 if none is explicitly set. + 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; diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index c5d5409d73..68cedfc534 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1171,12 +1171,37 @@ bool PrintObject::has_belt_brim() const return false; if (m_config.brim_type == btNoBrim) return false; - if (m_config.brim_width.value <= 0. && m_config.leading_brim_length.value <= 0. - && m_config.extra_brim_width.value <= 0.) + // An inner-only brim has no leading/extra geometry: leading_brim_length and + // extra_brim_width both widen the OUTER ring, which btInnerOnly never emits, so it + // produces nothing unless brim_width itself is positive. Every other brim type is + // satisfied by any one of the three widths. Requiring the width here (instead of + // "any width") stops has_belt_brim() - and therefore Print::validate() - from + // rejecting the prime tower / spiral vase for a brim that would never be drawn. + if (m_config.brim_type == btInnerOnly) { + if (m_config.brim_width.value <= 0.) + return false; + } else if (m_config.brim_width.value <= 0. && m_config.leading_brim_length.value <= 0. + && m_config.extra_brim_width.value <= 0.) { return false; + } return ! this->has_raft(); } +unsigned int PrintObject::belt_brim_filament() const +{ + // 1-based, matching PrintRegion::outer_wall_filament_id and the raw values pushed + // into LayerTools::extruders in ToolOrdering::collect_extruders (the whole list is + // reindexed to 0-based later). Lowest positive outer-wall filament over the + // printing regions; 1 when none is explicitly set. + unsigned int brim_filament = 0; + for (size_t i = 0; i < this->num_printing_regions(); ++ i) { + const unsigned int f = this->printing_region(i).config().outer_wall_filament_id.value; + if (f > 0 && (brim_filament == 0 || f < brim_filament)) + brim_filament = f; + } + 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 From a453cb1eba0626f627a8f6136e1c015ec7078482 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Thu, 6 Aug 2026 15:38:31 -0500 Subject: [PATCH 085/224] tests: cover belt-brim first-contact emission, tool selection, inner/leading-edge, and predicate (E) Deterministic tests for: coincident brim at first belt contact not dropped (C), single- and multi-extruder brim tool selection with no doubling (B), multi-object apron ordering, inner-only+leading-only not rejecting prime tower/spiral (D), and inner/holed + leading-edge-only geometry. --- tests/fff_print/test_skirt_brim.cpp | 297 ++++++++++++++++++++++++++++ tests/libslic3r/test_belt_brim.cpp | 74 +++++++ 2 files changed, 371 insertions(+) diff --git a/tests/fff_print/test_skirt_brim.cpp b/tests/fff_print/test_skirt_brim.cpp index b5d315db2a..486be88f40 100644 --- a/tests/fff_print/test_skirt_brim.cpp +++ b/tests/fff_print/test_skirt_brim.cpp @@ -9,7 +9,12 @@ #include +#include #include +#include +#include +#include +#include #include "test_helpers.hpp" // get access to init_print, etc @@ -473,6 +478,298 @@ static DynamicPrintConfig belt_brim_config() return config; } +// Same belt as belt_brim_config(), but with `filaments` distinct filaments so the brim's +// tool selection can be observed. Kept separate from belt_brim_config() so the existing +// single-filament belt tests are untouched. +static DynamicPrintConfig belt_brim_multifilament_config(unsigned int filaments, + std::initializer_list extra = {}) +{ + DynamicPrintConfig config = multifilament_config(filaments); + config.set_deserialize_strict({ + { "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" }, + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "skirt_loops", 0 }, + { "top_shell_layers", 0 }, + { "bottom_shell_layers", 1 }, + { "machine_start_gcode", "T[initial_tool]\n" }, + }); + if (extra.size() > 0) + config.set_deserialize_strict(extra); + return config; +} + +// 0-based tool indices used by extrusions whose role comment contains `role` (needs +// gcode_comments). Mirrors tools_for_role in test_multifilament.cpp; statics do not cross +// translation units, so it is repeated here. +static std::set belt_tools_for_role(const std::string &gcode, const std::string &role) +{ + std::set tools; + int current_tool = 0; + GCodeReader reader; + reader.parse_buffer(gcode, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) { + const std::string cmd(line.cmd()); + if (cmd.size() >= 2 && cmd[0] == 'T' && std::isdigit((unsigned char) cmd[1])) + current_tool = std::stoi(cmd.substr(1)); + else if (line.extruding(self) && std::string(line.comment()).find(role) != std::string::npos) + tools.insert(current_tool); + }); + return tools; +} + +// Machine Z of the first extruding move whose role comment contains `role`, in file order; +// numeric_limits::max() when the role never extrudes. +static double first_role_z(const std::string &gcode, const std::string &role) +{ + double z = std::numeric_limits::max(); + GCodeReader parser; + parser.parse_buffer(gcode, [&z, &role](GCodeReader &self, const GCodeReader::GCodeLine &line) { + if (line.extruding(self) && line.comment().find(role) != std::string_view::npos) { + z = self.z(); + self.quit_parsing(); + } + }); + 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"). +static int nonempty_belt_brim_layers(const PrintObject &object) +{ + int n = 0; + for (const ExtrusionEntityCollection &band : object.belt_brim_by_layer()) + if (! band.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. +static std::map first_move_by_tool(const std::string &gcode, const std::string &role) +{ + std::map first; + int tool = 0; + long idx = 0; + GCodeReader reader; + reader.parse_buffer(gcode, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) { + const std::string cmd(line.cmd()); + if (cmd.size() >= 2 && cmd[0] == 'T' && std::isdigit((unsigned char) cmd[1])) { + tool = std::stoi(cmd.substr(1)); + return; + } + if (! line.extruding(self)) + return; + ++ idx; + if (std::string(line.comment()).find(role) != std::string::npos && ! first.count(tool)) + first[tool] = idx; + }); + return first; +} + +// C - the band coincident with the object's FIRST contact with the belt must not be dropped: +// a belt brim has to appear at or below the object's first perimeter. On the unfixed feature +// the first-contact band is dropped and the first brim then appears only at a later (higher) +// layer. Machine Z is meaningful and shared between roles under the belt remap, so the first +// brim's Z must not exceed the first perimeter's. Both with and without support. +TEST_CASE("Belt brim is laid at the object's first belt contact", "[SkirtBrim][belt]") +{ + const bool support = GENERATE(false, true); + DYNAMIC_SECTION("enable_support=" << support) { + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "leading_brim_length", 0 }, + { "extra_brim_width", 0 }, + { "brim_object_gap", 0 }, + { "enable_support", support ? 1 : 0 }, + }); + const std::string gcode = slice({ cube(20) }, config); + + const double brim_z = first_role_z(gcode, "brim"); + const double peri_z = first_role_z(gcode, "perimeter"); + REQUIRE(brim_z < std::numeric_limits::max()); + REQUIRE(peri_z < std::numeric_limits::max()); + CHECK(brim_z <= peri_z + EPSILON); + } +} + +// C control - when the band's own object layer has extrusion (any interior layer of a solid +// cube), the band takes the ordinary process_layer() path and must be drawn immediately +// before that layer's perimeters, and exactly once: never dropped, never double-emitted. +TEST_CASE("Belt brim on an object layer precedes its perimeters, once", "[SkirtBrim][belt]") +{ + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "brim_object_gap", 0 }, + }); + Print print; + Model model; + init_print({ cube(20) }, print, model, config); + const std::string gc = gcode(print); + + // Ordering: the first thing extruded is brim, then perimeter. + const std::vector seq = role_sequence(gc, { "brim", "perimeter" }); + REQUIRE(seq.size() >= 2); + 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()); + REQUIRE(bands > 0); + CHECK(role_passes(gc, "brim") == bands); +} + +// B - single extruder (filament id 1). Every band must survive the 1-based -> 0-based +// filament-id conversion the apron path performs: a wrong conversion drops all single-extruder +// bands, so the pass count would collapse. The expected count is derived from the sliced +// layers, not a ratio. +TEST_CASE("Belt brim on a single extruder emits every band once", "[SkirtBrim][belt]") +{ + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "brim_object_gap", 0 }, + }); + Print print; + Model model; + init_print({ cube(20) }, print, model, config); + const std::string gc = gcode(print); + + const int expected = nonempty_belt_brim_layers(*print.objects().front()); + REQUIRE(expected > 0); + CHECK(role_passes(gc, "brim") == expected); + CHECK(belt_tools_for_role(gc, "brim") == std::set{ 0 }); // filament 1 -> tool 0 +} + +// B - multi extruder (wall filament id 2). Every belt-brim line must print on the object's +// wall filament (index 2 -> tool 1), and the total number of passes must equal the +// single-extruder baseline: no per-filament doubling. +TEST_CASE("Belt brim on a multi-extruder object uses the wall filament, no doubling", "[SkirtBrim][belt]") +{ + // Single-extruder baseline built the same way (same nozzle/flow), so the band geometry - + // and thus the band count - is identical and only the filament assignment differs. + DynamicPrintConfig base = belt_brim_multifilament_config(1, { + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "brim_object_gap", 0 }, + }); + const int baseline = role_passes(slice({ cube(20) }, base), "brim"); + REQUIRE(baseline > 0); + + DynamicPrintConfig config = belt_brim_multifilament_config(2, { + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "brim_object_gap", 0 }, + { "outer_wall_filament_id", 2 }, + { "inner_wall_filament_id", 2 }, + }); + const std::string gc = slice({ cube(20) }, config); + + CHECK(belt_tools_for_role(gc, "brim") == std::set{ 1 }); // filament 2 -> tool 1 + CHECK(role_passes(gc, "brim") == baseline); +} + +// B - two objects offset ALONG the belt (Y, since the tilt is about X), each with its own +// wall filament. Each object's brim/apron must print on that object's filament AND before +// that object's own perimeters. The object is identified by its unique tool. +TEST_CASE("Belt brim of each object precedes its perimeters on its own filament", "[SkirtBrim][belt]") +{ + DynamicPrintConfig config = belt_brim_multifilament_config(2, { + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "leading_brim_length", 6 }, + { "brim_object_gap", 0 }, + }); + + std::vector meshes; + meshes.emplace_back(cube(20)); + TriangleMesh second = cube(20); + second.translate(0.f, 40.f, 0.f); // offset along the belt so it lands well after the first + meshes.emplace_back(std::move(second)); + + const std::vector> overrides { + { { "outer_wall_filament_id", 1 }, { "inner_wall_filament_id", 1 } }, + { { "outer_wall_filament_id", 2 }, { "inner_wall_filament_id", 2 } }, + }; + Print print; + Model model; + init_print(std::move(meshes), print, model, config, &overrides, /*arrange=*/false); + print.process(); + const std::string gc = gcode(print); + + // Both brims appear, each on its object's wall filament (1 -> T0, 2 -> T1). + CHECK(belt_tools_for_role(gc, "brim") == std::set{ 0, 1 }); + + const std::map brim_first = first_move_by_tool(gc, "brim"); + const std::map peri_first = first_move_by_tool(gc, "perimeter"); + for (int tool : { 0, 1 }) { + REQUIRE(brim_first.count(tool) == 1); + REQUIRE(peri_first.count(tool) == 1); + CHECK(brim_first.at(tool) < peri_first.at(tool)); + } +} + +// D - the belt-brim predicate must not fire on a request that produces no belt brim. +// leading_brim_length / extra_brim_width only feed the OUTER ring, so inner_only with zero +// brim_width yields nothing and must not claim the layers the prime tower / spiral vase need. +TEST_CASE("Belt inner-only leading brim does not reject the prime tower or spiral vase", "[SkirtBrim][belt]") +{ + auto inner_leading = [](std::initializer_list extra) { + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", "inner_only" }, + { "brim_width", 0 }, + { "leading_brim_length", 6 }, + { "brim_object_gap", 0 }, + }); + config.set_deserialize_strict(extra); + return config; + }; + + SECTION("prime tower is left alone") { + Print print; + Model model; + init_print({ cube(20) }, print, model, inner_leading({ { "enable_prime_tower", 1 } })); + CHECK_FALSE(print.objects().front()->has_belt_brim()); + CHECK(print.validate().string.empty()); + } + SECTION("spiral vase is left alone") { + Print print; + Model model; + init_print({ cube(20) }, print, model, inner_leading({ { "spiral_mode", 1 } })); + CHECK_FALSE(print.objects().front()->has_belt_brim()); + CHECK(print.validate().string.empty()); + } + SECTION("a real inner brim still rejects the prime tower") { + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", "inner_only" }, + { "brim_width", 4 }, + { "brim_object_gap", 0 }, + { "enable_prime_tower", 1 }, + }); + Print print; + Model model; + init_print({ cube(20) }, print, model, config); + CHECK(print.objects().front()->has_belt_brim()); + CHECK_FALSE(print.validate().string.empty()); + } +} + TEST_CASE("Belt brim spans many layers instead of one", "[SkirtBrim][belt]") { DynamicPrintConfig config = belt_brim_config(); diff --git a/tests/libslic3r/test_belt_brim.cpp b/tests/libslic3r/test_belt_brim.cpp index 9f645817e9..d5e99dafaa 100644 --- a/tests/libslic3r/test_belt_brim.cpp +++ b/tests/libslic3r/test_belt_brim.cpp @@ -275,6 +275,80 @@ SCENARIO("belt_brim_region reduces to the plate brim without an apron", "[BeltBr } } +SCENARIO("belt_brim_region builds an inner ring inside a hole", "[BeltBrim]") { + // Holed prisms (a washer) are the only footprints an inner brim has anything to grab. + // The inner path offsets the hole boundary inward and keeps the ring between the two + // offsets, clipped back inside the hole - it must be non-empty and live in the hole, + // never spill out onto the plate. No apron is applied to the inner ring. + const coord_t mm = scale_(1.); + const BeltBrimFrame frame { 1.0, 1 }; + const coord_t width = 3 * mm; + const coord_t gap = 1 * mm; + + GIVEN("a 40x40 mm washer with a 20 mm square hole") { + ExPolygon washer = make_box(0, 0, 40 * mm, 40 * mm); + add_hole(washer, 10 * mm, 10 * mm, 30 * mm, 30 * mm); + const ExPolygons footprint { washer }; + const BoundingBox hole_bb = get_extents(washer.holes.front()); + + WHEN("an inner-only brim is requested") { + const ExPolygons region = belt_brim_region(footprint, false, true, width, gap, 0, 0, frame); + THEN("a non-empty ring is produced strictly inside the hole") { + REQUIRE(! region.empty()); + CHECK(area(region) > 0); + const BoundingBox rb = get_extents(region); + CHECK(rb.min.x() >= hole_bb.min.x()); + CHECK(rb.min.y() >= hole_bb.min.y()); + CHECK(rb.max.x() <= hole_bb.max.x()); + CHECK(rb.max.y() <= hole_bb.max.y()); + } + } + WHEN("no inner brim is requested") { + THEN("the hole contributes nothing") { + CHECK(belt_brim_region(footprint, false, false, width, gap, 0, 0, frame).empty()); + } + } + } +} + +SCENARIO("Leading-edge-only retains the downhill half of the brim region", "[BeltBrim]") { + // BeltBrim.cpp ~445-458 clips the region to the object's first-contact band and keeps + // only what lies at or downhill of it. That clip is built with band_box(), which is + // file-static, so the rectangular half-band is reconstructed here with the SAME sign + // rule the code uses (low_side = shear > 0, i.e. downhill is -u) to pin the convention + // for both tilt signs. downhill_sign() is the exported accessor the flag mirrors. + const coord_t mm = scale_(1.); + const double shear = GENERATE(1.0, -1.0); + DYNAMIC_SECTION("shear " << shear) { + const BeltBrimFrame frame { shear, 1 }; // from_axis 1 => u is Y + CHECK((frame.downhill_sign() < 0) == (frame.shear > 0.)); + + const ExPolygons region { make_box(0, 0, 20 * mm, 20 * mm) }; // straddles the cut + const coord_t u_cut = 8 * mm; + const BoundingBox bb = get_extents(region); + + const bool low_side = frame.shear > 0.; + const coord_t lo = low_side ? bb.min.y() : u_cut; + const coord_t hi = low_side ? u_cut : bb.max.y(); + Polygon keep; + keep.points = { Point(bb.min.x(), lo), Point(bb.max.x(), lo), + Point(bb.max.x(), hi), Point(bb.min.x(), hi) }; + const ExPolygons kept = intersection_ex(region, Polygons{ keep }); + + REQUIRE(! kept.empty()); + const BoundingBox kb = get_extents(kept); + if (frame.shear > 0.) { + // downhill is -u: nothing above the cut survives. + CHECK(kb.max.y() <= u_cut + 2); + CHECK(kb.min.y() < u_cut); + } else { + // downhill is +u: nothing below the cut survives. + CHECK(kb.min.y() >= u_cut - 2); + CHECK(kb.max.y() > u_cut); + } + } +} + SCENARIO("The apron follows the sign of the shear", "[BeltBrim]") { // Guards the one sign convention that is easiest to get backwards: which way // is downhill, i.e. which way the belt carries the part. From 8b2e28817d03cf4cad7b5870798904765a6db1a8 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 8 Aug 2026 12:33:41 -0500 Subject: [PATCH 086/224] fix non 45 degree slicing methods after regression created while cleaning up UI --- src/CMakeLists.txt | 8 +++- src/libslic3r/GCode/MachineFrameTransform.cpp | 33 +++++++++-------- src/libslic3r/GCode/MachineFrameTransform.hpp | 2 +- src/libslic3r/PrintConfig.cpp | 6 +-- tests/fff_print/test_gcodewriter.cpp | 37 +++++++++++++++++++ 5 files changed, 66 insertions(+), 20 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 79b49cfd16..1e650df95b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -90,6 +90,12 @@ if (SLIC3R_GUI) # list(REMOVE_ITEM wxWidgets_LIBRARIES oleacc) find_package(wxInspector REQUIRED) + # wxInspector 1.0.0 installs its headers but accidentally declares the + # INSTALL_INTERFACE include directory PRIVATE, so its imported target does + # not expose them to consumers. Restore the package prefix include path until + # the upstream export is fixed. + get_filename_component(WXINSPECTOR_PREFIX "${wxInspector_DIR}/../../.." ABSOLUTE) + target_include_directories(wxInspector::wxInspector INTERFACE "${WXINSPECTOR_PREFIX}/include") list(APPEND wxWidgets_LIBRARIES "wxInspector::wxInspector") message(STATUS "wx libs: ${wxWidgets_LIBRARIES}") @@ -175,7 +181,7 @@ endif () # Add the Slic3r GUI library, libcurl, OpenGL and GLU libraries. if (SLIC3R_GUI) # target_link_libraries(OrcaSlicer ws2_32 uxtheme setupapi libslic3r_gui ${wxWidgets_LIBRARIES}) -target_link_libraries(OrcaSlicer libslic3r_gui) +target_link_libraries(OrcaSlicer libslic3r_gui wxInspector::wxInspector) if (MSVC) # Generate debug symbols even in release mode. target_link_options(OrcaSlicer PUBLIC "$<$:/DEBUG>") diff --git a/src/libslic3r/GCode/MachineFrameTransform.cpp b/src/libslic3r/GCode/MachineFrameTransform.cpp index 3d322b4303..af09ba7353 100644 --- a/src/libslic3r/GCode/MachineFrameTransform.cpp +++ b/src/libslic3r/GCode/MachineFrameTransform.cpp @@ -29,31 +29,34 @@ bool MachineFrameTransform::init_from_config(const PrintConfig &config) return false; const double angle_rad = Geometry::deg2rad(angle_deg); - const double cos_a = std::cos(angle_rad); - if (std::abs(cos_a) <= EPSILON) + const double sin_a = std::sin(angle_rad); + if (std::abs(sin_a) <= EPSILON) return false; - const double tan_a = std::sin(angle_rad) / cos_a; - const double inv_cos = 1.0 / cos_a; + const double cot_a = std::cos(angle_rad) / sin_a; + const double inv_sin = 1.0 / std::abs(sin_a); - // Couple the height axis (Z) to the belt-feed axis and stretch the belt-feed - // axis by 1/cos so a unit slicing move maps to the correct belt travel. The - // shear sign matches the belt-floor slope derived from the same rotation in - // BeltTransformPipeline::compute_belt_height_and_floor: - // tilt about X: feed axis Y, Z += +tan·Y, scale Y *= 1/cos - // tilt about Y: feed axis X, Z += -tan·X, scale X *= 1/cos + // This stage runs after the conventional belt axis swap. For an X-axis + // slicing rotation, remapped Y is model height and remapped Z is travel + // along the belt. Convert those Cartesian coordinates to machine axes with + // the established belt-printer convention: + // machine gantry = model height / sin(a) + // machine belt = model belt + model height * cot(a) + // The Y-rotation case is the same mapping on X/Z, with the rotation sign. + // At 45 degrees tan/cot and sin/cos are equal, which previously hid the + // incorrect complementary-angle formulas used by this unified transform. Matrix3d shear = Matrix3d::Identity(); Matrix3d scale = Matrix3d::Identity(); if (axis == BeltRotationAxis::X) { - shear(2, 1) = tan_a; // Z from Y - scale(1, 1) = inv_cos; // Y + shear(2, 1) = cot_a; // Z from Y + scale(1, 1) = inv_sin; // Y } else { // BeltRotationAxis::Y - shear(2, 0) = -tan_a; // Z from X - scale(0, 0) = inv_cos; // X + shear(2, 0) = -cot_a; // Z from X + scale(0, 0) = inv_sin; // X } // Apply shear first, then scale (the historical default ShearThenScale order: // result = scale * shear * p). For the canonical 45°/X belt this maps - // (x,y,z) -> (x, y/cos, y + z), matching the previous per-axis config. + // (x,y,z) -> (x, y/sin, y + z), matching the previous per-axis config. Transform3d combined = Transform3d::Identity(); combined.linear() = scale * shear; diff --git a/src/libslic3r/GCode/MachineFrameTransform.hpp b/src/libslic3r/GCode/MachineFrameTransform.hpp index 744a7976c5..83519bdbe4 100644 --- a/src/libslic3r/GCode/MachineFrameTransform.hpp +++ b/src/libslic3r/GCode/MachineFrameTransform.hpp @@ -15,7 +15,7 @@ namespace Slic3r { // // Derived entirely from the single belt tilt (belt_slice_rotation axis + // belt_slice_rotation_angle): a shear coupling the height axis to the belt-feed -// axis (factor tan a) plus a 1/cos a scale on the belt-feed axis. The expert +// axis (factor cot a) plus a 1/sin a scale on the gantry-height axis. The expert // belt_frame_tilt_decouple flag lets the machine-frame angle differ from the // pre-slice rotation angle via belt_frame_tilt_angle. class MachineFrameTransform diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index f641699d91..d0ce14ec35 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -7216,9 +7216,9 @@ void PrintConfigDef::init_fff_params() def = this->add("gcode_back_transform", coBool); def->label = L("G-code back-transform"); def->category = L("Printable space"); - def->tooltip = L("Reverse the shear/scale transform applied during slicing so G-code " - "coordinates are in the machine's physical coordinate space. " - "Requires at least one shear axis with global mode enabled."); + 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)); diff --git a/tests/fff_print/test_gcodewriter.cpp b/tests/fff_print/test_gcodewriter.cpp index 6af11d8d23..22ca387bbb 100644 --- a/tests/fff_print/test_gcodewriter.cpp +++ b/tests/fff_print/test_gcodewriter.cpp @@ -20,12 +20,49 @@ #include #include #include "libslic3r/BeltGCodeWriter.hpp" +#include "libslic3r/BeltTransform.hpp" #include "libslic3r/GCodeReader.hpp" #include "libslic3r/PrintConfig.hpp" using namespace Slic3r; using namespace Slic3r::Test; +TEST_CASE("Belt machine coordinates retain a non-45-degree slicing angle", "[GCodeWriter][belt]") +{ + PrintConfig config; + 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; + + BeltGCodeWriter writer; + writer.set_belt_back_transform(config); + writer.set_machine_frame_transform(config); + writer.set_axis_remap(int(config.gcode_remap_x.value), + int(config.gcode_remap_y.value), + int(config.gcode_remap_z.value)); + + // Start with a point in the unrotated model frame, then feed the writer the + // same rotated coordinate produced by the pre-slice mesh transform. The + // back-transform must recover the model point before the axis swap and + // machine-frame shear/scale are applied. + const Vec3d model(4., 10., 3.); + Transform3d forward = BeltTransformPipeline::build_forward_transform(config); + const Vec3d machine = writer.to_machine_coords(forward * model); + + // The conventional X-tilt remap produces (x, z, y). At 30 degrees the + // gantry coordinate is z/sin(30) and belt travel is y + z*cot(30). + // The complementary tan/inv-cos formulas accidentally used by the unified + // transform are indistinguishable at 45 degrees, but fail this case. + REQUIRE_THAT(machine.x(), Catch::Matchers::WithinAbs(4., 1e-9)); + REQUIRE_THAT(machine.y(), Catch::Matchers::WithinAbs(3. / std::sin(Geometry::deg2rad(30.)), 1e-9)); + REQUIRE_THAT(machine.z(), Catch::Matchers::WithinAbs(10. + 3. / std::tan(Geometry::deg2rad(30.)), 1e-9)); +} + // Arrange on a finite bed, not an unbounded InfiniteBed: the latter places items // near INT64_MIN/4 (~2.3e18), which reaches ClipperLib's coordinate limit and throws // "Coordinate outside allowed range" on Windows/arm64. A 500x500 bed keeps coordinates From 88726d76e84547ff9613b847323ffd53836399e3 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 13 Jun 2026 04:24:41 -0500 Subject: [PATCH 087/224] Purge tower part 1 --- result.json | 10 ++ src/libslic3r/GCode/ToolOrdering.cpp | 4 + src/libslic3r/Preset.cpp | 2 + src/libslic3r/Print.cpp | 223 ++++++++++++++++++++++++- src/libslic3r/Print.hpp | 12 ++ src/libslic3r/PrintConfig.cpp | 24 +++ src/libslic3r/PrintConfig.hpp | 5 + src/libslic3r/PrintObject.cpp | 3 +- src/libslic3r/PrintObjectSlice.cpp | 21 +++ src/slic3r/GUI/ConfigManipulation.cpp | 17 +- src/slic3r/GUI/GLCanvas3D.cpp | 8 +- src/slic3r/GUI/Jobs/ArrangeJob.cpp | 6 + src/slic3r/GUI/Plater.cpp | 227 ++++++++++++++++++++++++++ src/slic3r/GUI/Tab.cpp | 1 + 14 files changed, 548 insertions(+), 15 deletions(-) create mode 100644 result.json diff --git a/result.json b/result.json new file mode 100644 index 0000000000..517f6bffa5 --- /dev/null +++ b/result.json @@ -0,0 +1,10 @@ +{ + "error_string": "Success.", + "export_time": 0, + "layer_height": 0.0, + "plate_index": 0, + "prepare_time": 0, + "return_code": 0, + "sparse_infill_density": 0.0, + "wall_loops": 0 +} diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index f96748848f..85a8436c3e 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -381,6 +381,10 @@ bool ToolOrdering::insert_wipe_tower_extruder() { if (!m_print_config_ptr || !m_print_config_ptr->enable_prime_tower) return false; + // Belt mode has no classic wipe tower; the dedicated wipe tower filament + // must not inject extra toolchanges into the purge prism planning. + if (m_print_config_ptr->belt_printer) + return false; if (m_print_config_ptr->wipe_tower_filament == 0) return false; diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 3e6e2b9aaf..092923e336 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1160,6 +1160,8 @@ static std::vector s_Preset_print_options{ "prime_volume", "prime_tower_infill_gap", "prime_tower_flat_ironing", + "belt_purge_tower_width", + "belt_purge_tower_object", "enable_tower_interface_features", "enable_tower_interface_cooldown_during_tower", "wipe_tower_no_sparse_layers", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 017fa19736..390bba1ff2 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -22,6 +22,7 @@ #include "MaterialType.hpp" #include "Model.hpp" #include "format.hpp" +#include "LocalesUtils.hpp" #include #include @@ -389,6 +390,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "prime_volume" || opt_key == "flush_into_infill" || opt_key == "flush_into_support" + || opt_key == "belt_purge_tower_width" || opt_key == "initial_layer_infill_speed" || opt_key == "travel_speed" || opt_key == "travel_speed_z" @@ -1473,6 +1475,15 @@ StringObjectException Print::validate(std::vector *warnin } if (m_config.enable_prime_tower) { + if (m_config.belt_printer.value && m_config.print_sequence == PrintSequence::ByObject + && extruders.size() > 1 && warning != nullptr) { + StringObjectException warningtemp; + warningtemp.string = L("The belt purge tower is not generated in \"By object\" print sequence; " + "filament changes will not be purged."); + warningtemp.opt_key = "enable_prime_tower"; + warningtemp.is_warning = true; + *warning = warningtemp; + } for (const PrintObject* object : m_objects) { if (object->config().precise_z_height.value) { warn(L("Enabling both precise Z height and the prime tower may cause slicing errors."), "precise_z_height"); @@ -1596,7 +1607,7 @@ StringObjectException Print::validate(std::vector *warnin return {_u8L("Variable layer height is not supported with Organic supports.") }; } - if (this->has_wipe_tower() && ! m_objects.empty()) { + if ((this->has_wipe_tower() || this->has_belt_purge_tower()) && ! m_objects.empty()) { // Make sure all extruders use same diameter filament and have the same nozzle diameter // EPSILON comparison is used for nozzles and 10 % tolerance is used for filaments double first_nozzle_diam = m_config.nozzle_diameter.get_at(extruders.front()); @@ -1612,12 +1623,17 @@ StringObjectException Print::validate(std::vector *warnin } } - if (! m_config.use_relative_e_distances) - return { L("The Wipe Tower is currently only supported with the relative extruder addressing (use_relative_e_distances=1).") }; + // The following two constraints come from the classic wipe tower G-code + // generator; purging into the belt purge prism uses normal object + // extrusions and does not need them. + if (this->has_wipe_tower()) { + if (! m_config.use_relative_e_distances) + return { L("The Wipe Tower is currently only supported with the relative extruder addressing (use_relative_e_distances=1).") }; + + if (m_config.ooze_prevention && m_config.single_extruder_multi_material) + return {L("Ooze prevention is only supported with the wipe tower when 'single_extruder_multi_material' is off.")}; + } - if (m_config.ooze_prevention && m_config.single_extruder_multi_material) - return {L("Ooze prevention is only supported with the wipe tower when 'single_extruder_multi_material' is off.")}; - #if 0 if (m_config.gcode_flavor != gcfRepRapSprinter && m_config.gcode_flavor != gcfRepRapFirmware && m_config.gcode_flavor != gcfRepetier && m_config.gcode_flavor != gcfMarlinLegacy && m_config.gcode_flavor != gcfMarlinFirmware) @@ -2708,7 +2724,10 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) m_wipe_tower_data.clear(); m_tool_ordering.clear(); - if (this->has_wipe_tower()) { + if (this->has_belt_purge_tower() && this->config().print_sequence != PrintSequence::ByObject) { + this->_plan_belt_purge(); + } + else if (this->has_wipe_tower()) { this->_make_wipe_tower(); } else if (this->config().print_sequence != PrintSequence::ByObject) { @@ -4085,6 +4104,13 @@ int Print::get_config_index(int filament_id, int layer_id, const std::vector 2) return true; @@ -4097,6 +4123,16 @@ bool Print::has_wipe_tower() const return false; } +// Belt purge prism: purging after filament changes is routed into a sliced +// prism object via the flush-into-objects machinery instead of a wipe tower. +bool Print::has_belt_purge_tower() const +{ + return m_config.belt_printer.value + && m_config.enable_prime_tower.value + && !m_config.spiral_mode.value + && m_config.filament_diameter.values.size() > 1; +} + const WipeTowerData &Print::wipe_tower_data(size_t filaments_cnt) const { // If the wipe tower wasn't created yet, make sure the depth and brim_width members are set to default. @@ -4183,6 +4219,179 @@ bool Print::enable_timelapse_print() const return m_config.timelapse_type.value == TimelapseType::tlSmooth; } +// Belt mode: snap the purge prism's layer grid onto the printed objects' grid. +// After belt slicing every object's layer print_z carries a per-object global +// z offset (mesh-vertex-scan belt_z_shift + instance-Y-dependent terms), so +// objects at different belt-Y positions do not share a layer grid. Purge +// marking looks absorbers up with get_layer_at_printz(lt.print_z, EPSILON), +// so the prism only absorbs purge at toolchange print_z values that coincide +// with one of its own layers. Shifting the prism by at most half a layer +// (a sub-layer-height displacement along the belt) makes its grid residue +// match the reference object's; both grids step by the same layer height +// (enforced by validate()), so matching residues means exact layer matches. +void Print::_align_belt_purge_layers() +{ + PrintObject *prism = nullptr; + for (PrintObject *po : m_objects) + if (po->config().belt_purge_tower_object.value) { + prism = po; + break; + } + if (prism == nullptr || prism->layers().empty()) + return; + + const double h = prism->config().layer_height.value; + if (h <= EPSILON) + return; + + // Grid residue of an object's layer grid: identical for all of an object's + // layers above the first since they step by h. + auto grid_offset = [h](const PrintObject *po) { + const double z = po->layers().front()->print_z; + return z - std::floor(z / h) * h; // in [0, h) + }; + + // Reference grid: the tallest non-prism object (proxy for the object with + // the most toolchange layers). + const PrintObject *ref = nullptr; + double ref_top = -std::numeric_limits::max(); + for (const PrintObject *po : m_objects) { + if (po->config().belt_purge_tower_object.value || po->layers().empty()) + continue; + const double top = po->layers().back()->print_z; + if (top > ref_top) { + ref_top = top; + ref = po; + } + } + if (ref == nullptr) + return; + + const double ref_offset = grid_offset(ref); + bool grids_mismatch = false; + for (const PrintObject *po : m_objects) { + if (po == ref || po->config().belt_purge_tower_object.value || po->layers().empty()) + continue; + double d = std::abs(grid_offset(po) - ref_offset); + d = std::min(d, h - d); + if (d > 5. * EPSILON) { + grids_mismatch = true; + break; + } + } + + // Shift normalized to (-h/2, h/2]. + double delta = ref_offset - grid_offset(prism); + if (delta > 0.5 * h) + delta -= h; + else if (delta <= -0.5 * h) + delta += h; + + BOOST_LOG_TRIVIAL(debug) << "[BELT-DEBUG] purge prism grid snap" + << " ref=" << ref->model_object()->name + << " ref_offset=" << ref_offset + << " prism_offset=" << grid_offset(prism) + << " delta=" << delta + << " grids_mismatch=" << grids_mismatch; + + prism->belt_shift_layer_grid(delta); + + if (grids_mismatch) + this->active_step_add_warning( + PrintStateBase::WarningLevel::NON_CRITICAL, + _u8L("Objects on the plate are sliced on different layer grids; the belt purge tower can only follow " + "one of them. Filament changes on layers of the other objects may not be fully purged.")); +} + +// Belt mode replacement for _make_wipe_tower(): plan filament-change purging +// into the belt purge prism (and any other flush_into_* object) using the +// flush-into-objects machinery, without generating classic wipe tower G-code. +// The toolchange itself is emitted by GCode::set_extruder() via the +// change_filament_gcode macro; the overrides marked here make the new +// filament's first extrusions land in the purge prism. +void Print::_plan_belt_purge() +{ + m_wipe_tower_data.clear(); + + // Must run before ToolOrdering is built: LayerTools merge per-object layer + // print_z values, and the prism only absorbs purge where its (snapped) + // layers coincide with the toolchange layers. + this->_align_belt_purge_layers(); + + const unsigned int number_of_extruders = (unsigned int) m_config.filament_colour.values.size(); + + // No initial priming extrusions: there is no tower to prime on. + m_wipe_tower_data.tool_ordering = ToolOrdering(*this, (unsigned int) -1, false); + m_wipe_tower_data.tool_ordering.sort_and_build_data(*this, (unsigned int) -1, false); + + if (m_wipe_tower_data.tool_ordering.empty() || m_wipe_tower_data.tool_ordering.last_extruder() == unsigned(-1)) + throw Slic3r::SlicingError("The print is empty. The model is not printable with current print settings."); + + if (!m_wipe_tower_data.tool_ordering.has_wipe_tower()) + // No toolchanges anywhere, nothing to purge. + return; + + this->throw_if_canceled(); + + // Flush volumes per filament pair, mirroring the generic wipe tower path: + // full flush matrix for single extruder multi material with purging enabled, + // plain prime volume otherwise. + std::vector flush_matrix(cast( + get_flush_volumes_matrix(m_config.flush_volumes_matrix.values, 0, m_config.nozzle_diameter.values.size()))); + std::vector> wipe_volumes; + for (unsigned int i = 0; i < number_of_extruders; ++i) + wipe_volumes.push_back(std::vector(flush_matrix.begin() + i * number_of_extruders, + flush_matrix.begin() + (i + 1) * number_of_extruders)); + const bool use_flush_matrix = m_config.purge_in_prime_tower && m_config.single_extruder_multi_material; + const float flush_multiplier = (float) m_config.flush_multiplier.get_at(0); + + float total_leftover = 0.f; + float worst_layer_leftover = 0.f; + double worst_layer_z = 0.; + + unsigned int current_extruder_id = m_wipe_tower_data.tool_ordering.first_extruder(); + for (auto &layer_tools : m_wipe_tower_data.tool_ordering.layer_tools()) { + float layer_leftover = 0.f; + for (const unsigned int extruder_id : layer_tools.extruders) { + if (extruder_id == current_extruder_id) + continue; + float volume_to_wipe = use_flush_matrix ? + wipe_volumes[current_extruder_id][extruder_id] * flush_multiplier : + (float) m_config.prime_volume; + float leftover = layer_tools.wiping_extrusions().mark_wiping_extrusions(*this, current_extruder_id, extruder_id, + volume_to_wipe); + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] purge toolchange print_z=" << layer_tools.print_z + << " filament " << current_extruder_id << "->" << extruder_id + << " requested=" << volume_to_wipe + << " absorbed=" << volume_to_wipe - leftover + << " leftover=" << leftover; + layer_leftover += leftover; + current_extruder_id = extruder_id; + } + layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this); + if (layer_leftover > 0.f) { + total_leftover += layer_leftover; + if (layer_leftover > worst_layer_leftover) { + worst_layer_leftover = layer_leftover; + worst_layer_z = layer_tools.print_z; + } + } + this->throw_if_canceled(); + } + + if (total_leftover > 1.f) { + this->active_step_add_warning( + PrintStateBase::WarningLevel::CRITICAL, + Slic3r::format(_u8L("The belt purge tower cannot absorb the full purge volume: %1% mm³ in total could not " + "be purged (worst layer: %2% mm³ at height %3%). The print may show color bleeding. " + "Increase the belt purge tower width, or reduce flushing volumes."), + int(std::ceil(total_leftover)), int(std::ceil(worst_layer_leftover)), + Slic3r::float_to_string_decimal_point(worst_layer_z, 2))); + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge planning leftover total=" << total_leftover + << " worst_layer=" << worst_layer_leftover << " at print_z=" << worst_layer_z; + } +} + void Print::_make_wipe_tower() { m_wipe_tower_data.clear(); diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index ff5a505360..fc78c5efef 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -558,6 +558,10 @@ private: std::vector> detect_extruder_geometric_unprintables() const; void slice_volumes(); + // Belt mode: shift the sliced layer grid (layer print_z and the belt floor + // clipping shift) by delta, used by Print::_align_belt_purge_layers() to + // snap the purge prism's layers onto the printed objects' layer grid. + void belt_shift_layer_grid(double delta); //BBS ExPolygons _shrink_contour_holes(double contour_delta, double hole_delta, const ExPolygons& polys) const; // BBS @@ -1085,6 +1089,8 @@ public: // Wipe tower support. bool has_wipe_tower() const; + // Belt purge prism (belt-mode replacement for the wipe tower). + bool has_belt_purge_tower() const; const WipeTowerData& wipe_tower_data(size_t filaments_cnt = 0) const; const ToolOrdering& tool_ordering() const { return m_tool_ordering; } @@ -1330,6 +1336,12 @@ private: void _make_skirt(); void _make_wipe_tower(); + // Belt mode: route filament-change purging into the belt purge prism + // (flush-into-objects) without generating a classic wipe tower. + void _plan_belt_purge(); + // Belt mode: shift the purge prism's layer grid to match the printed + // objects' grid so toolchange layers find prism layers to purge into. + void _align_belt_purge_layers(); void finalize_first_layer_convex_hull(); void update_filament_self_index_cache(); // Deduplicates, per filament, the (extruder type x volume type) variants the grouping diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index d0ce14ec35..cef9a840a8 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -7321,6 +7321,20 @@ void PrintConfigDef::init_fff_params() def->set_default_value(new ConfigOptionEnum(BeltSupportZOffsetMode::Unconditional)); } + def = this->add("belt_purge_tower_width", coFloat); + def->label = L("Belt purge tower width"); + def->category = L("Printable space"); + def->tooltip = L("Width (machine X, across the belt) of the purge prism that is automatically " + "generated on belt printers when the prime tower is enabled and multiple " + "filaments are used. Filament-change purging is routed into this prism's " + "extrusions instead of a classic wipe tower. Its height is computed " + "automatically from the worst-case purge volume per layer: a wider prism " + "results in a shorter one."); + def->sidetext = L("mm"); + def->min = 1.; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(35.)); + def = this->add("tree_support_branch_angle", coFloat); def->label = L("Tree support branch angle"); def->category = L("Support"); @@ -7966,6 +7980,16 @@ void PrintConfigDef::init_fff_params() "It will not take effect unless the prime tower is enabled."); def->set_default_value(new ConfigOptionBool(false)); + // Internal marker (not shown in any settings tab): identifies the auto-generated + // belt purge prism so it can be updated/removed by the auto-manager and aligned + // to the object layer grid by the backend. Persisted to 3mf like any per-object key. + def = this->add("belt_purge_tower_object", coBool); + def->category = L("Flush options"); + def->label = L("Belt purge tower object"); + def->tooltip = L("Marks the auto-generated belt purge prism. Managed automatically; do not set manually."); + def->mode = comDevelop; + def->set_default_value(new ConfigOptionBool(false)); + def = this->add("wipe_tower_bridging", coFloat); def->label = L("Maximal bridging distance"); def->tooltip = L("Maximal distance between supports on sparse infill sections."); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 3342a54b6d..ffd0320149 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1228,6 +1228,9 @@ PRINT_CONFIG_CLASS_DEFINE( // BBS ((ConfigOptionBool, flush_into_infill)) ((ConfigOptionBool, flush_into_support)) + // Marker for the auto-generated belt purge prism; identifies the object to + // the auto-manager (GUI) and the layer-grid alignment step (backend). + ((ConfigOptionBool, belt_purge_tower_object)) // BBS ((ConfigOptionFloat, tree_support_branch_distance)) ((ConfigOptionFloat, tree_support_tip_diameter)) @@ -1802,6 +1805,8 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionFloat, belt_support_floor_offset)) ((ConfigOptionEnum, belt_support_floor_mode)) ((ConfigOptionEnum, belt_support_z_offset_mode)) + // Width (machine X, across the belt) of the auto-generated belt purge prism. + ((ConfigOptionFloat, belt_purge_tower_width)) //BBS ((ConfigOptionInts, additional_cooling_fan_speed)) ((ConfigOptionInts, close_additional_fan_first_x_layers)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 68cedfc534..cd0bc34a8e 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1663,7 +1663,8 @@ bool PrintObject::invalidate_state_by_config_options( } else if ( opt_key == "flush_into_infill" || opt_key == "flush_into_objects" - || opt_key == "flush_into_support") { + || opt_key == "flush_into_support" + || opt_key == "belt_purge_tower_object") { invalidated |= m_print->invalidate_step(psWipeTower); invalidated |= m_print->invalidate_step(psGCodeExport); } else { diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 1212100eac..b95bc4903f 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -1392,6 +1392,27 @@ void apply_fuzzy_skin_segmentation(PrintObject &print_object, ThrowOnCancel thro }); // end of parallel_for } +// Belt mode: shift the sliced layer grid by delta. Mirrors the global_z_offset +// application in slice() — layer print_z and belt_floor_z_shift move together +// so belt floor clipping stays consistent with the shifted grid. Used by +// Print::_align_belt_purge_layers() to snap the purge prism onto the printed +// objects' layer grid; |delta| <= half a layer height, i.e. a sub-layer shift +// of the prism along the belt. +void PrintObject::belt_shift_layer_grid(double delta) +{ + if (std::abs(delta) < EPSILON) + return; + for (Layer *layer : m_layers) + layer->print_z += delta; + for (SupportLayer *layer : m_support_layers) + layer->print_z += delta; + m_slicing_params.belt_floor_z_shift += delta; + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] belt_shift_layer_grid" + << " obj=" << this->model_object()->name + << " delta=" << delta + << " first_layer.print_z=" << (m_layers.empty() ? 0. : m_layers.front()->print_z); +} + // 1) Decides Z positions of the layers, // 2) Initializes layers and their regions // 3) Slices the object meshes diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 3fc69d6e4b..ffd4d43a71 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -971,11 +971,16 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in toggle_line("preheat_steps", have_ooze_prevention && (preheat_steps > 0)); bool have_prime_tower = config->opt_bool("enable_prime_tower"); + // ORCA-Belt: belt printers replace the classic wipe tower with the + // auto-generated belt purge prism — only its width is tunable; the + // classic tower geometry options do not apply. (is_belt_printer is + // computed at the top of this function.) + toggle_line("belt_purge_tower_width", have_prime_tower && is_belt_printer); for (auto el : {"prime_tower_width", "prime_tower_brim_width", "prime_tower_skip_points", "wipe_tower_wall_type", "prime_tower_infill_gap","prime_tower_enable_framework", "enable_tower_interface_features"}) - toggle_line(el, have_prime_tower); + toggle_line(el, have_prime_tower && !is_belt_printer); toggle_line("enable_tower_interface_cooldown_during_tower", - have_prime_tower && config->opt_bool("enable_tower_interface_features")); + have_prime_tower && !is_belt_printer && config->opt_bool("enable_tower_interface_features")); bool purge_in_primetower = preset_bundle->printers.get_edited_preset().config.opt_bool("purge_in_prime_tower"); @@ -983,17 +988,17 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in "wipe_tower_extra_spacing", "wipe_tower_max_purge_speed", "wipe_tower_bridging", "wipe_tower_extra_flow", "wipe_tower_no_sparse_layers"}) - toggle_line(el, have_prime_tower && supports_wipe_tower_2); + toggle_line(el, have_prime_tower && supports_wipe_tower_2 && !is_belt_printer); WipeTowerWallType wipe_tower_wall_type = config->opt_enum("wipe_tower_wall_type"); - bool have_rib_wall = (wipe_tower_wall_type == WipeTowerWallType::wtwRib)&&have_prime_tower; - toggle_line("wipe_tower_cone_angle", have_prime_tower && supports_wipe_tower_2 && wipe_tower_wall_type == WipeTowerWallType::wtwCone); + bool have_rib_wall = (wipe_tower_wall_type == WipeTowerWallType::wtwRib)&&have_prime_tower&&!is_belt_printer; + toggle_line("wipe_tower_cone_angle", have_prime_tower && supports_wipe_tower_2 && !is_belt_printer && wipe_tower_wall_type == WipeTowerWallType::wtwCone); toggle_line("wipe_tower_extra_rib_length", have_rib_wall); toggle_line("wipe_tower_rib_width", have_rib_wall); toggle_line("wipe_tower_fillet_wall", have_rib_wall); toggle_field("prime_tower_width", have_prime_tower && !have_rib_wall); - toggle_line("single_extruder_multi_material_priming", !bSEMM && have_prime_tower && supports_wipe_tower_2); + toggle_line("single_extruder_multi_material_priming", !bSEMM && have_prime_tower && supports_wipe_tower_2 && !is_belt_printer); toggle_line("prime_volume",have_prime_tower && (!purge_in_primetower || !bSEMM)); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index e3e84bc1c7..8ff05862a8 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -2866,7 +2866,13 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re need_wipe_tower |= dynamic_cast(dconfig.option("enable_wrapping_detection"))->value; } - if (wt && (need_wipe_tower || filaments_count > 1) && !wxGetApp().plater()->only_gcode_mode() && !wxGetApp().plater()->is_gcode_3mf()) { + // Belt printers replace the classic wipe tower with the auto-generated + // belt purge prism (a real model object), so never draw the tower widget. + bool is_belt_printer = false; + if (const auto *belt_opt = wxGetApp().preset_bundle->printers.get_edited_preset().config.option("belt_printer")) + is_belt_printer = belt_opt->value; + + if (wt && !is_belt_printer && (need_wipe_tower || filaments_count > 1) && !wxGetApp().plater()->only_gcode_mode() && !wxGetApp().plater()->is_gcode_3mf()) { for (int plate_id = 0; plate_id < n_plates; plate_id++) { // If print ByObject and there is only one object in the plate, the wipe tower is allowed to be generated. PartPlate* part_plate = ppl.get_plate(plate_id); diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.cpp b/src/slic3r/GUI/Jobs/ArrangeJob.cpp index 6b74b71af1..4e143de7d8 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.cpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.cpp @@ -291,6 +291,12 @@ void ArrangeJob::prepare_wipe_tower() bool enable_prime_tower = op && op->getBool(); if (!enable_prime_tower || params.is_seq_print) return; + // Belt printers have no classic wipe tower; purging goes into the belt + // purge prism, which is a real model object and arranges like any other. + if (const auto *belt_opt = wxGetApp().preset_bundle->printers.get_edited_preset().config.option("belt_printer"); + belt_opt && belt_opt->value) + return; + bool smooth_timelapse = false; auto sop = current_config.option("timelapse_type"); if (sop) { smooth_timelapse = sop->getInt() == TimelapseType::tlSmooth; } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 365384ecf9..d03c389dc7 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -5475,6 +5475,10 @@ struct Plater::priv void exit_gizmo(); void remove(size_t obj_idx); bool delete_object_from_model(size_t obj_idx, bool refresh_immediately = true); //BBS + // ORCA-Belt: keep the auto-generated belt purge prism in sync with the + // config and plate contents. Returns true when the model was mutated + // (caller should refresh the scene). + bool ensure_belt_purge_tower(); void delete_all_objects_from_model(); void reset(bool apply_presets_change = false); void center_selection(); @@ -8879,6 +8883,225 @@ void Plater::priv::process_validation_warnings(const std::vectorconfig.option("belt_purge_tower_object"); + return opt != nullptr && opt->getBool(); + }; + + std::vector prism_idxs; + for (int i = 0; i < (int) model.objects.size(); ++i) + if (is_prism(model.objects[i])) + prism_idxs.push_back(i); + + // Deletes prism objects, keeping the sidebar and part plates in sync + // (same primitives as Plater::priv::remove(), minus scene update — the + // caller refreshes the scene). + auto remove_prisms = [this](const std::vector &idxs) { + for (auto it = idxs.rbegin(); it != idxs.rend(); ++it) { + model.delete_object(size_t(*it)); + partplate_list.notify_instance_removed(*it, -1); + sidebar->obj_list()->delete_object_from_list(size_t(*it)); + } + }; + + const auto &printer_config = wxGetApp().preset_bundle->printers.get_edited_preset().config; + const auto &print_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto &project_config = wxGetApp().preset_bundle->project_config; + + const auto *belt_opt = printer_config.option("belt_printer"); + const bool belt = belt_opt != nullptr && belt_opt->value; + const bool prime_tower_enabled = print_config.has("enable_prime_tower") && print_config.opt_bool("enable_prime_tower"); + const auto *seq_opt = print_config.option>("print_sequence"); + const bool by_object = seq_opt != nullptr && seq_opt->value == PrintSequence::ByObject; + + // Filaments used and bounding extent of the non-prism objects on the + // current plate (1-based filament ids; volume extruder 0 = object default). + PartPlate *plate = partplate_list.get_curr_plate(); + std::set filaments; + double y_min = std::numeric_limits::max(); + double y_max = -std::numeric_limits::max(); + double z_max = 0.; + bool have_objects = false; + if (belt && plate != nullptr) { + for (int obj_idx = 0; obj_idx < (int) model.objects.size(); ++obj_idx) { + const ModelObject *mo = model.objects[obj_idx]; + if (is_prism(mo)) + continue; + int obj_extruder = 1; + if (const ConfigOption *opt = mo->config.option("extruder"); opt != nullptr && opt->getInt() > 0) + obj_extruder = opt->getInt(); + bool any_instance_on_plate = false; + for (int inst_idx = 0; inst_idx < (int) mo->instances.size(); ++inst_idx) { + if (!plate->contain_instance_totally(obj_idx, inst_idx)) + continue; + any_instance_on_plate = true; + const BoundingBoxf3 bb = mo->instance_bounding_box(inst_idx); + y_min = std::min(y_min, bb.min.y()); + y_max = std::max(y_max, bb.max.y()); + z_max = std::max(z_max, bb.max.z()); + } + if (!any_instance_on_plate) + continue; + have_objects = true; + for (const ModelVolume *mv : mo->volumes) + for (int e : mv->get_extruders()) + filaments.insert(e > 0 ? e : obj_extruder); + } + } + + const bool wanted = belt && prime_tower_enabled && !by_object && have_objects && filaments.size() > 1; + if (!wanted) { + if (prism_idxs.empty()) + return false; + remove_prisms(prism_idxs); + BOOST_LOG_TRIVIAL(info) << "[BELT-DEBUG] belt purge tower removed (conditions not met)"; + return true; + } + + // --- Sizing ----------------------------------------------------------- + const double width = print_config.has("belt_purge_tower_width") ? std::max(1., print_config.opt_float("belt_purge_tower_width")) : 35.; + const double layer_h = print_config.has("layer_height") ? print_config.opt_float("layer_height") : 0.2; + + // Belt tilt: standard belt printers rotate about X. For other axes (or no + // rotation) fall back to vertical slicing geometry (theta = 90 deg); the + // backend leftover warning catches any resulting under-absorption. + double theta = M_PI / 2.; + const auto *axis_opt = printer_config.option>("belt_slice_rotation"); + const auto *angle_opt = printer_config.option("belt_slice_rotation_angle"); + if (axis_opt != nullptr && axis_opt->value == BeltRotationAxis::X && angle_opt != nullptr && std::abs(angle_opt->value) > EPSILON) + theta = std::clamp(Geometry::deg2rad(std::abs(angle_opt->value)), Geometry::deg2rad(5.), M_PI / 2.); + const double sin_t = std::sin(theta); + const double cot_t = std::cos(theta) / sin_t; + + // Worst-case purge volume of one layer: up to (filament count - 1) + // toolchanges, each needing the worst flush matrix entry (mirrors the + // volume selection in Print::_plan_belt_purge()). + const bool use_matrix = (print_config.has("purge_in_prime_tower") && print_config.opt_bool("purge_in_prime_tower")) + && (printer_config.has("single_extruder_multi_material") && printer_config.opt_bool("single_extruder_multi_material")); + double max_flush = print_config.has("prime_volume") ? print_config.opt_float("prime_volume") : 45.; + if (use_matrix) { + const size_t extruder_nums = wxGetApp().preset_bundle->get_printer_extruder_count(); + const std::vector matrix = get_flush_volumes_matrix( + project_config.option("flush_volumes_matrix")->values, 0, extruder_nums); + const auto * multi_opt = project_config.option("flush_multiplier"); + const double multiplier = multi_opt != nullptr && !multi_opt->values.empty() ? multi_opt->get_at(0) : 1.; + const int n_total = (int) (std::sqrt(double(matrix.size())) + 0.5); + double m = 0.; + for (int i : filaments) + for (int j : filaments) + if (i != j && i <= n_total && j <= n_total) + m = std::max(m, matrix[size_t(i - 1) * n_total + size_t(j - 1)]); + if (m > 0.) + max_flush = m * multiplier; + } + const double v_layer = double(filaments.size() - 1) * max_flush; + + // One tilted slicing plane cuts a width x (height/sin) rectangle out of + // the prism interior; one layer slab absorbs that area x layer height. + const double eta = 0.85; // perimeters/infill packing safety factor + double height = v_layer * sin_t / (width * layer_h * eta); + const double printable_height = printer_config.has("printable_height") ? printer_config.opt_float("printable_height") : 250.; + height = std::clamp(height, 2. * layer_h, std::max(2. * layer_h, printable_height)); + + // Length along the belt: cover the objects' Y extent; a slicing plane + // spans height*cot(theta) of belt travel, so pad both ends with the full + // lead-in (tilt sign is padded symmetrically) plus the objects' top + // overhang along the belt. + const double margin = 10.; + const double lead = height * cot_t; + double y_start = y_min - lead - margin; + double y_end = y_max + (z_max + height) * cot_t + margin; + + const Vec3d plate_origin = plate->get_origin(); + const auto *bed_opt = printer_config.option("printable_area"); + const bool infinite_y = printer_config.has("belt_printer_infinite_y") && printer_config.opt_bool("belt_printer_infinite_y"); + double lane_x = plate_origin.x() + 5.; // fallback left lane + if (bed_opt != nullptr && !bed_opt->values.empty()) { + const BoundingBoxf bed_ext = get_extents(bed_opt->values); + lane_x = plate_origin.x() + bed_ext.min.x() + 5.; + if (!infinite_y) { + y_start = std::max(y_start, plate_origin.y() + bed_ext.min.y() + 1.); + y_end = std::min(y_end, plate_origin.y() + bed_ext.max.y() - 1.); + } + } + const double length = std::max(y_end - y_start, 10.); + + const Vec3d desired_size(width, length, height); + const Vec3d desired_center(lane_x + 0.5 * width, y_start + 0.5 * length, 0.5 * height); + + // --- Idempotence check ------------------------------------------------- + // Coarse tolerances so object nudges and sizing jitter do not regenerate + // the prism on every background-process tick. + if (prism_idxs.size() == 1) { + const ModelObject *prism = model.objects[prism_idxs.front()]; + const BoundingBoxf3 bb = prism->bounding_box_exact(); + if ((bb.size() - desired_size).cwiseAbs().maxCoeff() < 5. && (bb.center() - desired_center).cwiseAbs().maxCoeff() < 5.) + return false; + } + + // --- (Re)create --------------------------------------------------------- + if (!prism_idxs.empty()) + remove_prisms(prism_idxs); + + ModelObject *new_object = model.add_object(); + new_object->name = _u8L("Belt Purge Tower"); + new_object->add_instance(); + ModelVolume *new_volume = new_object->add_volume(make_cube(width, length, height)); + new_volume->name = new_object->name; + + auto &cfg = new_object->config; + cfg.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true)); + cfg.set_key_value("flush_into_objects", new ConfigOptionBool(true)); + cfg.set_key_value("extruder", new ConfigOptionInt(1)); + // Sacrificial solid prism: one wall, no shells, dense rectilinear infill — + // every extrusion is overriddable, so the absorbed volume matches the + // cross-section x layer-height estimate used for the height above. + cfg.set_key_value("wall_loops", new ConfigOptionInt(1)); + cfg.set_key_value("top_shell_layers", new ConfigOptionInt(0)); + cfg.set_key_value("bottom_shell_layers", new ConfigOptionInt(0)); + cfg.set_key_value("sparse_infill_density", new ConfigOptionPercent(100)); + cfg.set_key_value("sparse_infill_pattern", new ConfigOptionEnum(ipRectilinear)); + cfg.set_key_value("enable_support", new ConfigOptionBool(false)); + cfg.set_key_value("brim_type", new ConfigOptionEnum(btNoBrim)); + cfg.set_key_value("seam_slope_type", new ConfigOptionEnum(SeamScarfType::None)); + cfg.set_key_value("precise_z_height", new ConfigOptionBool(false)); + + new_object->invalidate_bounding_box(); + const BoundingBoxf3 mesh_bb = new_volume->mesh().bounding_box(); + new_object->translate(-mesh_bb.center()); + new_object->instances.front()->set_offset(desired_center); + new_object->ensure_on_bed(); + new_object->instances.front()->set_assemble_transformation(new_object->instances.front()->get_transformation()); + + const size_t obj_idx = model.objects.size() - 1; + // Registers the object in the sidebar and notifies the part plates; + // selection is left untouched (auto-managed object). + sidebar->obj_list()->add_object_to_list(obj_idx, /*call_selection_changed=*/false); + + BOOST_LOG_TRIVIAL(info) << "[BELT-DEBUG] belt purge tower generated" + << " W=" << width << " L=" << length << " H=" << height + << " v_layer=" << v_layer << " max_flush=" << max_flush + << " filaments=" << filaments.size() + << " theta_deg=" << Geometry::rad2deg(theta) + << " center=(" << desired_center.x() << "," << desired_center.y() << ")"; + return true; +} + // Update background processing thread from the current config and Model. // Returns a bitmask of UpdateBackgroundProcessReturnState. unsigned int Plater::priv::update_background_process(bool force_validation, bool postpone_error_messages, bool switch_print) @@ -8889,6 +9112,10 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool // If the update_background_process() was not called by the timer, kill the timer, // so the update_restart_background_process() will not be called again in vain. background_process_timer.Stop(); + // ORCA-Belt: sync the auto-managed belt purge prism before the model is + // applied to the Print below, so the prism change rides this same apply. + if (printer_technology == ptFFF && this->ensure_belt_purge_tower()) + return_state |= UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE; // Update the "out of print bed" state of ModelInstances. update_print_volume_state(); // Apply new config to the possibly running background task. diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 8de8bf6e84..25287c79c3 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -3009,6 +3009,7 @@ void TabPrint::build() optgroup->append_single_option_line("enable_tower_interface_cooldown_during_tower", "multimaterial_settings_prime_tower"); optgroup->append_single_option_line("prime_tower_enable_framework", "multimaterial_settings_prime_tower"); optgroup->append_single_option_line("prime_tower_width", "multimaterial_settings_prime_tower#width"); + optgroup->append_single_option_line("belt_purge_tower_width", "multimaterial_settings_prime_tower"); optgroup->append_single_option_line("prime_volume", "multimaterial_settings_prime_tower"); optgroup->append_single_option_line("prime_tower_brim_width", "multimaterial_settings_prime_tower#brim-width"); optgroup->append_single_option_line("prime_tower_infill_gap", "multimaterial_settings_prime_tower"); From ec4e9717d3bdc17a0cbf1db3312497fe0e16131a Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 13 Jun 2026 05:30:25 -0500 Subject: [PATCH 088/224] Part Two: Functional Results --- src/slic3r/GUI/Plater.cpp | 134 ++++++++++++++++++++++++++------------ 1 file changed, 92 insertions(+), 42 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index d03c389dc7..cda0a1557e 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -5479,6 +5479,21 @@ struct Plater::priv // config and plate contents. Returns true when the model was mutated // (caller should refresh the scene). bool ensure_belt_purge_tower(); + // Inputs of the last belt purge prism generation. Idempotence is keyed on + // these (not the prism's resulting bbox): the prism gets nudged by plate + // assignment after creation, so comparing its bbox would falsely detect a + // change and regenerate it on every background-process tick — which would + // re-invalidate a freshly sliced result and make the preview unreachable. + struct BeltPurgeSignature { + bool valid = false; + int filament_count = 0; + long key[7] = {0}; // rounded geometry inputs (0.1 mm units) + bool operator==(const BeltPurgeSignature &o) const { + if (valid != o.valid || filament_count != o.filament_count) return false; + for (int i = 0; i < 7; ++i) if (key[i] != o.key[i]) return false; + return true; + } + } m_belt_purge_sig; void delete_all_objects_from_model(); void reset(bool apply_presets_change = false); void center_selection(); @@ -8933,6 +8948,8 @@ bool Plater::priv::ensure_belt_purge_tower() // current plate (1-based filament ids; volume extruder 0 = object default). PartPlate *plate = partplate_list.get_curr_plate(); std::set filaments; + double x_min = std::numeric_limits::max(); + double x_max = -std::numeric_limits::max(); double y_min = std::numeric_limits::max(); double y_max = -std::numeric_limits::max(); double z_max = 0.; @@ -8951,6 +8968,8 @@ bool Plater::priv::ensure_belt_purge_tower() continue; any_instance_on_plate = true; const BoundingBoxf3 bb = mo->instance_bounding_box(inst_idx); + x_min = std::min(x_min, bb.min.x()); + x_max = std::max(x_max, bb.max.x()); y_min = std::min(y_min, bb.min.y()); y_max = std::max(y_max, bb.max.y()); z_max = std::max(z_max, bb.max.z()); @@ -8966,10 +8985,11 @@ bool Plater::priv::ensure_belt_purge_tower() const bool wanted = belt && prime_tower_enabled && !by_object && have_objects && filaments.size() > 1; if (!wanted) { + m_belt_purge_sig = BeltPurgeSignature{}; if (prism_idxs.empty()) return false; remove_prisms(prism_idxs); - BOOST_LOG_TRIVIAL(info) << "[BELT-DEBUG] belt purge tower removed (conditions not met)"; + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] belt purge tower removed (conditions not met)"; return true; } @@ -8977,17 +8997,27 @@ bool Plater::priv::ensure_belt_purge_tower() const double width = print_config.has("belt_purge_tower_width") ? std::max(1., print_config.opt_float("belt_purge_tower_width")) : 35.; const double layer_h = print_config.has("layer_height") ? print_config.opt_float("layer_height") : 0.2; - // Belt tilt: standard belt printers rotate about X. For other axes (or no - // rotation) fall back to vertical slicing geometry (theta = 90 deg); the - // backend leftover warning catches any resulting under-absorption. - double theta = M_PI / 2.; - const auto *axis_opt = printer_config.option>("belt_slice_rotation"); + // Belt geometry. The rotation axis is the gantry tilt axis; the belt + // travels along the *other* horizontal axis (X-rotation -> belt along Y, + // the CR-30 default). The purge prism is a long bar laid along the belt + // travel direction, beside the parts. For no/Z rotation we fall back to + // vertical slicing geometry (theta = 90 deg). + const auto *axis_opt = printer_config.option>("belt_slice_rotation"); const auto *angle_opt = printer_config.option("belt_slice_rotation_angle"); - if (axis_opt != nullptr && axis_opt->value == BeltRotationAxis::X && angle_opt != nullptr && std::abs(angle_opt->value) > EPSILON) + const BeltRotationAxis rot = axis_opt != nullptr ? axis_opt->value : BeltRotationAxis::X; + const bool belt_is_y = (rot != BeltRotationAxis::Y); // X / None / Z -> belt along Y + double theta = M_PI / 2.; + if ((rot == BeltRotationAxis::X || rot == BeltRotationAxis::Y) && angle_opt != nullptr && std::abs(angle_opt->value) > EPSILON) theta = std::clamp(Geometry::deg2rad(std::abs(angle_opt->value)), Geometry::deg2rad(5.), M_PI / 2.); const double sin_t = std::sin(theta); const double cot_t = std::cos(theta) / sin_t; + // Parts' extent along the belt-travel axis and the lateral (across-belt) axis. + const double belt_min = belt_is_y ? y_min : x_min; + const double belt_max = belt_is_y ? y_max : x_max; + const double lat_min = belt_is_y ? x_min : y_min; + const double lat_max = belt_is_y ? x_max : y_max; + // Worst-case purge volume of one layer: up to (filament count - 1) // toolchanges, each needing the worst flush matrix entry (mirrors the // volume selection in Print::_plan_belt_purge()). @@ -9018,41 +9048,56 @@ bool Plater::priv::ensure_belt_purge_tower() const double printable_height = printer_config.has("printable_height") ? printer_config.opt_float("printable_height") : 250.; height = std::clamp(height, 2. * layer_h, std::max(2. * layer_h, printable_height)); - // Length along the belt: cover the objects' Y extent; a slicing plane - // spans height*cot(theta) of belt travel, so pad both ends with the full - // lead-in (tilt sign is padded symmetrically) plus the objects' top - // overhang along the belt. - const double margin = 10.; - const double lead = height * cot_t; - double y_start = y_min - lead - margin; - double y_end = y_max + (z_max + height) * cot_t + margin; + // --- Idempotence (input-keyed) ---------------------------------------- + // Key on the generation inputs, NOT the prism's resulting bbox: plate + // assignment nudges the prism after creation, so a bbox comparison sees a + // "change" every tick and regenerates the prism, which re-invalidates any + // freshly sliced result and makes the G-code preview unreachable. + auto q = [](double v) { return std::lround(v * 10.0); }; // 0.1 mm quantization + BeltPurgeSignature sig; + sig.valid = true; + sig.filament_count = (int) filaments.size(); + sig.key[0] = q(width); + sig.key[1] = q(layer_h); + sig.key[2] = q(height); + sig.key[3] = q(belt_min); + sig.key[4] = q(belt_max); + sig.key[5] = q(lat_min); + sig.key[6] = q(z_max); + if (prism_idxs.size() == 1 && sig == m_belt_purge_sig) + return false; // already up to date — do not touch the model - const Vec3d plate_origin = plate->get_origin(); - const auto *bed_opt = printer_config.option("printable_area"); - const bool infinite_y = printer_config.has("belt_printer_infinite_y") && printer_config.opt_bool("belt_printer_infinite_y"); - double lane_x = plate_origin.x() + 5.; // fallback left lane - if (bed_opt != nullptr && !bed_opt->values.empty()) { - const BoundingBoxf bed_ext = get_extents(bed_opt->values); - lane_x = plate_origin.x() + bed_ext.min.x() + 5.; - if (!infinite_y) { - y_start = std::max(y_start, plate_origin.y() + bed_ext.min.y() + 1.); - y_end = std::min(y_end, plate_origin.y() + bed_ext.max.y() - 1.); - } + // --- Position ---------------------------------------------------------- + // Lay the bar along the belt, anchored at the parts' belt-axis minimum so + // it leads the print at the belt entry. A tilted slicing plane through a + // part of height z reaches belt position y + z*cot(theta), so extend the + // trailing end to cover the tallest part's upper toolchanges plus the + // prism's own tilt span. + const double margin = 5.; + const double belt_start = belt_min - margin; // anchored at parts' belt minimum + const double belt_end = belt_max + (z_max + height) * cot_t + margin; // trailing tilt allowance + const double length = std::max(belt_end - belt_start, 10.); + const double belt_center = 0.5 * (belt_start + belt_end); + + // Lateral placement: just beside the parts (the -lateral side by default, + // flipping to the +lateral side if that would fall off the bed). + const Vec3d plate_origin = plate->get_origin(); + const double gap = 5.; + double lat_center = lat_min - gap - 0.5 * width; + if (const auto *bed_opt = printer_config.option("printable_area"); + bed_opt != nullptr && !bed_opt->values.empty()) { + const BoundingBoxf bed_ext = get_extents(bed_opt->values); + const double bed_lat_min = plate_origin[belt_is_y ? 0 : 1] + (belt_is_y ? bed_ext.min.x() : bed_ext.min.y()); + if (lat_center - 0.5 * width < bed_lat_min) + lat_center = lat_max + gap + 0.5 * width; // not enough room on the -lateral side } - const double length = std::max(y_end - y_start, 10.); - const Vec3d desired_size(width, length, height); - const Vec3d desired_center(lane_x + 0.5 * width, y_start + 0.5 * length, 0.5 * height); - - // --- Idempotence check ------------------------------------------------- - // Coarse tolerances so object nudges and sizing jitter do not regenerate - // the prism on every background-process tick. - if (prism_idxs.size() == 1) { - const ModelObject *prism = model.objects[prism_idxs.front()]; - const BoundingBoxf3 bb = prism->bounding_box_exact(); - if ((bb.size() - desired_size).cwiseAbs().maxCoeff() < 5. && (bb.center() - desired_center).cwiseAbs().maxCoeff() < 5.) - return false; - } + // Orient the bar: long axis = belt travel, width = lateral, height = Z. + const double size_x = belt_is_y ? width : length; + const double size_y = belt_is_y ? length : width; + const Vec3d desired_center(belt_is_y ? lat_center : belt_center, + belt_is_y ? belt_center : lat_center, + 0.5 * height); // --- (Re)create --------------------------------------------------------- if (!prism_idxs.empty()) @@ -9061,7 +9106,7 @@ bool Plater::priv::ensure_belt_purge_tower() ModelObject *new_object = model.add_object(); new_object->name = _u8L("Belt Purge Tower"); new_object->add_instance(); - ModelVolume *new_volume = new_object->add_volume(make_cube(width, length, height)); + ModelVolume *new_volume = new_object->add_volume(make_cube(size_x, size_y, height)); new_volume->name = new_object->name; auto &cfg = new_object->config; @@ -9093,12 +9138,17 @@ bool Plater::priv::ensure_belt_purge_tower() // selection is left untouched (auto-managed object). sidebar->obj_list()->add_object_to_list(obj_idx, /*call_selection_changed=*/false); - BOOST_LOG_TRIVIAL(info) << "[BELT-DEBUG] belt purge tower generated" + // Record the inputs that produced this prism so subsequent ticks are no-ops + // until the parts/config actually change. + m_belt_purge_sig = sig; + + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] belt purge tower generated" + << " belt_is_y=" << belt_is_y << " W=" << width << " L=" << length << " H=" << height << " v_layer=" << v_layer << " max_flush=" << max_flush << " filaments=" << filaments.size() << " theta_deg=" << Geometry::rad2deg(theta) - << " center=(" << desired_center.x() << "," << desired_center.y() << ")"; + << " center=(" << desired_center.x() << "," << desired_center.y() << "," << desired_center.z() << ")"; return true; } From 854dae8dd24ccd5e815b5aea1837b346bcb31215 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 13 Jun 2026 06:04:10 -0500 Subject: [PATCH 089/224] strategy incremental --- src/libslic3r/Print.cpp | 27 ++++++++++++++++++++++ src/slic3r/GUI/Plater.cpp | 48 +++++++++++++++++++++++---------------- 2 files changed, 56 insertions(+), 19 deletions(-) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 390bba1ff2..b5d3701e41 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -4345,6 +4345,21 @@ void Print::_plan_belt_purge() const bool use_flush_matrix = m_config.purge_in_prime_tower && m_config.single_extruder_multi_material; const float flush_multiplier = (float) m_config.flush_multiplier.get_at(0); + // Diagnostic: the prism only absorbs purge at toolchange layers whose + // print_z coincides with one of its own layers. Compare the prism's layer + // print_z range to the toolchange print_z range and count how many + // toolchange layers actually land on a prism layer. This distinguishes a + // range/grid-alignment failure (no coverage) from a capacity shortfall + // (covered but not enough cross-section). + const PrintObject *diag_prism = nullptr; + for (const PrintObject *po : m_objects) + if (po->config().belt_purge_tower_object.value && !po->layers().empty()) { diag_prism = po; break; } + if (diag_prism != nullptr) + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge prism layer range print_z=[" + << diag_prism->layers().front()->print_z << ", " << diag_prism->layers().back()->print_z + << "] nlayers=" << diag_prism->layers().size(); + int tc_layers = 0, tc_layers_covered = 0; + float total_leftover = 0.f; float worst_layer_leftover = 0.f; double worst_layer_z = 0.; @@ -4352,9 +4367,16 @@ void Print::_plan_belt_purge() unsigned int current_extruder_id = m_wipe_tower_data.tool_ordering.first_extruder(); for (auto &layer_tools : m_wipe_tower_data.tool_ordering.layer_tools()) { float layer_leftover = 0.f; + bool layer_has_tc = false; for (const unsigned int extruder_id : layer_tools.extruders) { if (extruder_id == current_extruder_id) continue; + if (!layer_has_tc) { + layer_has_tc = true; + ++tc_layers; + if (diag_prism != nullptr && diag_prism->get_layer_at_printz(layer_tools.print_z, EPSILON) != nullptr) + ++tc_layers_covered; + } float volume_to_wipe = use_flush_matrix ? wipe_volumes[current_extruder_id][extruder_id] * flush_multiplier : (float) m_config.prime_volume; @@ -4379,6 +4401,11 @@ void Print::_plan_belt_purge() this->throw_if_canceled(); } + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge coverage: " << tc_layers_covered << "/" << tc_layers + << " toolchange layers land on a prism layer" + << (tc_layers > 0 && tc_layers_covered == 0 ? " (RANGE/GRID MISALIGNMENT — prism absorbs nothing)" : + tc_layers_covered < tc_layers ? " (partial coverage)" : " (full coverage)"); + if (total_leftover > 1.f) { this->active_step_add_warning( PrintStateBase::WarningLevel::CRITICAL, diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index cda0a1557e..6e3ccd6da3 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -9041,12 +9041,19 @@ bool Plater::priv::ensure_belt_purge_tower() } const double v_layer = double(filaments.size() - 1) * max_flush; - // One tilted slicing plane cuts a width x (height/sin) rectangle out of - // the prism interior; one layer slab absorbs that area x layer height. - const double eta = 0.85; // perimeters/infill packing safety factor - double height = v_layer * sin_t / (width * layer_h * eta); + // Height from the per-layer purge demand. A tilted slicing plane cuts a + // width x (height/sin) rectangle out of the bar, so one layer slab absorbs + // width * (height/sin) * layer_height of purge. Solve for the height that + // holds the worst-case per-layer purge, with: + // eta - infill/perimeter packing (not all of the cross-section is solid) + // safety - margin for the tilt ramp at the bar ends and layer-grid + // alignment slop, where a layer cuts less than the full section + // and a minimum so the tower is a real printable body rather than a sliver. + const double eta = 0.85; + const double safety = 1.6; const double printable_height = printer_config.has("printable_height") ? printer_config.opt_float("printable_height") : 250.; - height = std::clamp(height, 2. * layer_h, std::max(2. * layer_h, printable_height)); + double height = safety * v_layer * sin_t / (width * layer_h * eta); + height = std::clamp(height, 8.0, std::max(8.0, printable_height)); // --- Idempotence (input-keyed) ---------------------------------------- // Key on the generation inputs, NOT the prism's resulting bbox: plate @@ -9068,28 +9075,31 @@ bool Plater::priv::ensure_belt_purge_tower() return false; // already up to date — do not touch the model // --- Position ---------------------------------------------------------- - // Lay the bar along the belt, anchored at the parts' belt-axis minimum so - // it leads the print at the belt entry. A tilted slicing plane through a - // part of height z reaches belt position y + z*cot(theta), so extend the - // trailing end to cover the tallest part's upper toolchanges plus the - // prism's own tilt span. + // Default belt-printer placement (per user request): + // - belt-travel axis: anchored at the parts' belt-axis MINIMUM (the belt + // entry / parts' min Y for an X-rotation belt) so the tower leads in. + // - across-belt axis: parked right up against the bed's MAXIMUM edge + // (max X for an X-rotation belt), out of the way of the parts. + // The bar extends from the parts' belt minimum past the parts; a tilted + // slicing plane through a part of height z reaches belt position + // y + z*cot(theta), so the trailing end covers the tallest part's upper + // toolchanges plus the bar's own tilt span. const double margin = 5.; - const double belt_start = belt_min - margin; // anchored at parts' belt minimum + const double belt_start = belt_min; // parts' belt-axis minimum const double belt_end = belt_max + (z_max + height) * cot_t + margin; // trailing tilt allowance const double length = std::max(belt_end - belt_start, 10.); const double belt_center = 0.5 * (belt_start + belt_end); - // Lateral placement: just beside the parts (the -lateral side by default, - // flipping to the +lateral side if that would fall off the bed). + // Across-belt: flush against the bed's maximum edge (small inset), falling + // back to just past the parts if the bed shape is unavailable. const Vec3d plate_origin = plate->get_origin(); - const double gap = 5.; - double lat_center = lat_min - gap - 0.5 * width; + const double inset = 1.; + double lat_center = lat_max + 5. + 0.5 * width; if (const auto *bed_opt = printer_config.option("printable_area"); bed_opt != nullptr && !bed_opt->values.empty()) { - const BoundingBoxf bed_ext = get_extents(bed_opt->values); - const double bed_lat_min = plate_origin[belt_is_y ? 0 : 1] + (belt_is_y ? bed_ext.min.x() : bed_ext.min.y()); - if (lat_center - 0.5 * width < bed_lat_min) - lat_center = lat_max + gap + 0.5 * width; // not enough room on the -lateral side + const BoundingBoxf bed_ext = get_extents(bed_opt->values); + const double bed_lat_max = plate_origin[belt_is_y ? 0 : 1] + (belt_is_y ? bed_ext.max.x() : bed_ext.max.y()); + lat_center = bed_lat_max - inset - 0.5 * width; } // Orient the bar: long axis = belt travel, width = lateral, height = Z. From 60e9ee26c9f8701abb25b0fa69c46419e45f9f50 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 13 Jun 2026 06:27:42 -0500 Subject: [PATCH 090/224] strategy incremental 2 --- src/slic3r/GUI/Plater.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 6e3ccd6da3..0dd0210ba4 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -9090,15 +9090,18 @@ bool Plater::priv::ensure_belt_purge_tower() const double length = std::max(belt_end - belt_start, 10.); const double belt_center = 0.5 * (belt_start + belt_end); - // Across-belt: flush against the bed's maximum edge (small inset), falling - // back to just past the parts if the bed shape is unavailable. - const Vec3d plate_origin = plate->get_origin(); + // Across-belt: flush against the bed's maximum edge, inset by half the bar + // width so the bar's far edge sits on the boundary and the whole bar stays + // on the bed (otherwise the center lands on the edge and half the bar hangs + // off → "objects laid over the boundary" → no slicing). Coordinates are in + // the printable_area / instance frame (do NOT add plate origin — the belt + // calibration objects are positioned the same way, see _calib_temp_belt_*). const double inset = 1.; - double lat_center = lat_max + 5. + 0.5 * width; + double lat_center = lat_max + 5. + 0.5 * width; // fallback: just past the parts if (const auto *bed_opt = printer_config.option("printable_area"); bed_opt != nullptr && !bed_opt->values.empty()) { const BoundingBoxf bed_ext = get_extents(bed_opt->values); - const double bed_lat_max = plate_origin[belt_is_y ? 0 : 1] + (belt_is_y ? bed_ext.max.x() : bed_ext.max.y()); + const double bed_lat_max = belt_is_y ? bed_ext.max.x() : bed_ext.max.y(); lat_center = bed_lat_max - inset - 0.5 * width; } From 62d8f22f52d4fc04d2ed157f8911694d6cf961c6 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 13 Jun 2026 07:02:14 -0500 Subject: [PATCH 091/224] purge tower still centered on X max --- src/slic3r/GUI/Plater.cpp | 47 +++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 0dd0210ba4..d00396d97e 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -9010,7 +9010,6 @@ bool Plater::priv::ensure_belt_purge_tower() if ((rot == BeltRotationAxis::X || rot == BeltRotationAxis::Y) && angle_opt != nullptr && std::abs(angle_opt->value) > EPSILON) theta = std::clamp(Geometry::deg2rad(std::abs(angle_opt->value)), Geometry::deg2rad(5.), M_PI / 2.); const double sin_t = std::sin(theta); - const double cot_t = std::cos(theta) / sin_t; // Parts' extent along the belt-travel axis and the lateral (across-belt) axis. const double belt_min = belt_is_y ? y_min : x_min; @@ -9075,36 +9074,46 @@ bool Plater::priv::ensure_belt_purge_tower() return false; // already up to date — do not touch the model // --- Position ---------------------------------------------------------- - // Default belt-printer placement (per user request): - // - belt-travel axis: anchored at the parts' belt-axis MINIMUM (the belt - // entry / parts' min Y for an X-rotation belt) so the tower leads in. - // - across-belt axis: parked right up against the bed's MAXIMUM edge - // (max X for an X-rotation belt), out of the way of the parts. - // The bar extends from the parts' belt minimum past the parts; a tilted - // slicing plane through a part of height z reaches belt position - // y + z*cot(theta), so the trailing end covers the tallest part's upper - // toolchanges plus the bar's own tilt span. + // Belt-travel axis: anchor the bar at the parts' belt-axis MINIMUM and span + // just the parts' belt extent (+ small margin). Filament changes happen + // over the parts' belt band, so the bar must sit there to absorb them; + // extending it far up the belt moves its body away from the toolchange + // layers and the purge goes unabsorbed (empirically, moving the tower + // toward the parts' Y-min clears the "cannot absorb" warning). const double margin = 5.; - const double belt_start = belt_min; // parts' belt-axis minimum - const double belt_end = belt_max + (z_max + height) * cot_t + margin; // trailing tilt allowance + const double belt_start = belt_min; // parts' belt-axis minimum + const double belt_end = belt_max + margin; // just past the parts — stay low / near the parts const double length = std::max(belt_end - belt_start, 10.); const double belt_center = 0.5 * (belt_start + belt_end); // Across-belt: flush against the bed's maximum edge, inset by half the bar // width so the bar's far edge sits on the boundary and the whole bar stays - // on the bed (otherwise the center lands on the edge and half the bar hangs - // off → "objects laid over the boundary" → no slicing). Coordinates are in - // the printable_area / instance frame (do NOT add plate origin — the belt - // calibration objects are positioned the same way, see _calib_temp_belt_*). - const double inset = 1.; - double lat_center = lat_max + 5. + 0.5 * width; // fallback: just past the parts + // on the bed. The bed (printable_area) is plate-local but model instances + // live in the plate's world frame, so add the plate origin's lateral + // component (plate-origin compensation is needed throughout the belt + // pipeline). lat_min/lat_max come from instance_bounding_box (world frame). + const Vec3d plate_origin = plate->get_origin(); + const double lat_origin = plate_origin[belt_is_y ? 0 : 1]; + const double inset = 1.; + double lat_center = lat_max + 5. + 0.5 * width; // fallback: just past the parts + BoundingBoxf bed_ext_dbg; if (const auto *bed_opt = printer_config.option("printable_area"); bed_opt != nullptr && !bed_opt->values.empty()) { const BoundingBoxf bed_ext = get_extents(bed_opt->values); + bed_ext_dbg = bed_ext; const double bed_lat_max = belt_is_y ? bed_ext.max.x() : bed_ext.max.y(); - lat_center = bed_lat_max - inset - 0.5 * width; + lat_center = lat_origin + bed_lat_max - inset - 0.5 * width; } + // Ground-truth diagnostic for placement frames (warning level so it lands + // in the GUI debug log): plate origin, bed extents, parts bbox, result. + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge place" + << " plate_origin=(" << plate_origin.x() << "," << plate_origin.y() << ")" + << " parts_x=[" << x_min << "," << x_max << "] parts_y=[" << y_min << "," << y_max << "] z_max=" << z_max + << " bed_ext=[" << bed_ext_dbg.min.x() << "," << bed_ext_dbg.min.y() + << " -> " << bed_ext_dbg.max.x() << "," << bed_ext_dbg.max.y() << "]" + << " lat_center=" << lat_center << " belt=[" << belt_start << "," << belt_end << "]"; + // Orient the bar: long axis = belt travel, width = lateral, height = Z. const double size_x = belt_is_y ? width : length; const double size_y = belt_is_y ? length : width; From 7cc50d750c2928629e443e7501004915f710e51c Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 13 Jun 2026 07:20:12 -0500 Subject: [PATCH 092/224] automated placement works --- src/slic3r/GUI/Plater.cpp | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index d00396d97e..b634a65440 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -9148,11 +9148,18 @@ bool Plater::priv::ensure_belt_purge_tower() cfg.set_key_value("seam_slope_type", new ConfigOptionEnum(SeamScarfType::None)); cfg.set_key_value("precise_z_height", new ConfigOptionBool(false)); + // Position by the belt-calibration pattern: drop to the bed, then translate + // the instance by the delta between the object's ACTUAL bbox center and the + // target. Setting the instance offset directly is unreliable here — the + // freshly added cube's local frame is not centered, so set_offset() lands + // the min corner (not the center) on the target, leaving the bar centered + // on the bed edge with half of it hanging off. new_object->invalidate_bounding_box(); - const BoundingBoxf3 mesh_bb = new_volume->mesh().bounding_box(); - new_object->translate(-mesh_bb.center()); - new_object->instances.front()->set_offset(desired_center); new_object->ensure_on_bed(); + const BoundingBoxf3 cur = new_object->bounding_box_exact(); + new_object->translate_instances(Vec3d(desired_center.x() - cur.center().x(), + desired_center.y() - cur.center().y(), + 0.0)); new_object->instances.front()->set_assemble_transformation(new_object->instances.front()->get_transformation()); const size_t obj_idx = model.objects.size() - 1; @@ -9170,7 +9177,9 @@ bool Plater::priv::ensure_belt_purge_tower() << " v_layer=" << v_layer << " max_flush=" << max_flush << " filaments=" << filaments.size() << " theta_deg=" << Geometry::rad2deg(theta) - << " center=(" << desired_center.x() << "," << desired_center.y() << "," << desired_center.z() << ")"; + << " desired_center=(" << desired_center.x() << "," << desired_center.y() << "," << desired_center.z() << ")" + << " achieved_center=(" << new_object->bounding_box_exact().center().x() << "," + << new_object->bounding_box_exact().center().y() << ")"; return true; } From 79c93733d33c327d7a1cc30ed1ae6d66865950dd Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 13 Jun 2026 08:00:16 -0500 Subject: [PATCH 093/224] purge tower additional compensation --- src/slic3r/GUI/Plater.cpp | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index b634a65440..f20a9ec60f 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -9074,17 +9074,22 @@ bool Plater::priv::ensure_belt_purge_tower() return false; // already up to date — do not touch the model // --- Position ---------------------------------------------------------- - // Belt-travel axis: anchor the bar at the parts' belt-axis MINIMUM and span - // just the parts' belt extent (+ small margin). Filament changes happen - // over the parts' belt band, so the bar must sit there to absorb them; - // extending it far up the belt moves its body away from the toolchange - // layers and the purge goes unabsorbed (empirically, moving the tower - // toward the parts' Y-min clears the "cannot absorb" warning). - const double margin = 5.; - const double belt_start = belt_min; // parts' belt-axis minimum - const double belt_end = belt_max + margin; // just past the parts — stay low / near the parts - const double length = std::max(belt_end - belt_start, 10.); - const double belt_center = 0.5 * (belt_start + belt_end); + // Belt-travel axis: span the parts' belt band, extended at BOTH tilted ends + // by the ramp allowance. The bar's tilted ends are triangular ramps where a + // slicing plane cuts less than the full height, so those layers can't hold a + // full purge. Pull the leading edge toward Y=0 below the parts' minimum and + // push the trailing edge past the parts' maximum, each by height/sin(theta) + // (= sqrt(2)*height at 45 deg), so the full-cross-section region covers the + // whole parts' band and every toolchange layer gets a full slice of the bar. + // (Filament changes happen over the parts' belt band — empirically the tower + // must sit there, anchored near the parts' Y-min, to clear the "cannot + // absorb" warning.) + const double margin = 5.; + const double ramp_compensation = height / sin_t; + const double belt_start = std::max(0.0, belt_min - ramp_compensation); // leading ramp, toward Y=0 + const double belt_end = belt_max + margin + ramp_compensation; // trailing ramp, past the parts + const double length = std::max(belt_end - belt_start, 10.); + const double belt_center = 0.5 * (belt_start + belt_end); // Across-belt: flush against the bed's maximum edge, inset by half the bar // width so the bar's far edge sits on the boundary and the whole bar stays From d367bcef923d22d7ecf297ab2e49f3b9d17fbc70 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 13 Jun 2026 08:13:45 -0500 Subject: [PATCH 094/224] extra height compensation --- src/slic3r/GUI/Plater.cpp | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index f20a9ec60f..33fd309f0e 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -9010,6 +9010,7 @@ bool Plater::priv::ensure_belt_purge_tower() if ((rot == BeltRotationAxis::X || rot == BeltRotationAxis::Y) && angle_opt != nullptr && std::abs(angle_opt->value) > EPSILON) theta = std::clamp(Geometry::deg2rad(std::abs(angle_opt->value)), Geometry::deg2rad(5.), M_PI / 2.); const double sin_t = std::sin(theta); + const double cot_t = std::cos(theta) / sin_t; // Parts' extent along the belt-travel axis and the lateral (across-belt) axis. const double belt_min = belt_is_y ? y_min : x_min; @@ -9074,20 +9075,22 @@ bool Plater::priv::ensure_belt_purge_tower() return false; // already up to date — do not touch the model // --- Position ---------------------------------------------------------- - // Belt-travel axis: span the parts' belt band, extended at BOTH tilted ends - // by the ramp allowance. The bar's tilted ends are triangular ramps where a - // slicing plane cuts less than the full height, so those layers can't hold a - // full purge. Pull the leading edge toward Y=0 below the parts' minimum and - // push the trailing edge past the parts' maximum, each by height/sin(theta) - // (= sqrt(2)*height at 45 deg), so the full-cross-section region covers the - // whole parts' band and every toolchange layer gets a full slice of the bar. - // (Filament changes happen over the parts' belt band — empirically the tower - // must sit there, anchored near the parts' Y-min, to clear the "cannot - // absorb" warning.) + // Belt-travel axis. With the mesh rotated by theta before slicing, a machine + // point (y,z) maps to slicing-Z = y*sin(theta) + z*cos(theta). The parts + // occupy slicing-Z in [y_min*sin, y_max*sin + z_max*cos], and the bar's + // FULL-cross-section region (the part not in a triangular end ramp) spans + // slicing-Z [belt_start*sin + H*cos, belt_end*sin]. Covering the parts' + // whole band therefore needs: + // - leading edge belt_start <= y_min - H*cot(theta) (bar's own ramp) + // - trailing edge belt_end >= y_max + z_max*cot(theta) (parts' top + // features print further up the belt, so the bar must reach there). + // The leading side uses the bar-ramp allowance height/sin (>= H*cot, slight + // over-cover); the trailing side adds the parts' height projection + // z_max*cot, which dominates the bar's own ramp. const double margin = 5.; const double ramp_compensation = height / sin_t; - const double belt_start = std::max(0.0, belt_min - ramp_compensation); // leading ramp, toward Y=0 - const double belt_end = belt_max + margin + ramp_compensation; // trailing ramp, past the parts + const double belt_start = std::max(0.0, belt_min - ramp_compensation); // leading ramp, toward Y=0 + const double belt_end = belt_max + margin + ramp_compensation + z_max * cot_t; // + parts' top-feature belt reach const double length = std::max(belt_end - belt_start, 10.); const double belt_center = 0.5 * (belt_start + belt_end); From 131b61b726c30550f833d0c4757bfc6414603909 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 13 Jun 2026 08:45:33 -0500 Subject: [PATCH 095/224] auto purge tower height calculation works --- src/slic3r/GUI/Plater.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 33fd309f0e..656a7d31e2 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -9021,7 +9021,13 @@ bool Plater::priv::ensure_belt_purge_tower() // Worst-case purge volume of one layer: up to (filament count - 1) // toolchanges, each needing the worst flush matrix entry (mirrors the // volume selection in Print::_plan_belt_purge()). - const bool use_matrix = (print_config.has("purge_in_prime_tower") && print_config.opt_bool("purge_in_prime_tower")) + // NOTE: both purge_in_prime_tower and single_extruder_multi_material are + // PRINTER options (Preset.cpp s_Preset_printer_options) — read them from the + // printer preset. Reading purge_in_prime_tower from the print preset returns + // has()==false, collapsing use_matrix to false and sizing the tower for the + // small prime_volume instead of the real color-change flush. This must match + // the backend Print::_plan_belt_purge() which reads both from the merged config. + const bool use_matrix = (printer_config.has("purge_in_prime_tower") && printer_config.opt_bool("purge_in_prime_tower")) && (printer_config.has("single_extruder_multi_material") && printer_config.opt_bool("single_extruder_multi_material")); double max_flush = print_config.has("prime_volume") ? print_config.opt_float("prime_volume") : 45.; if (use_matrix) { From c80f1ab312285d7884a021abd9815d4bf230ccde Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 13 Jun 2026 09:51:48 -0500 Subject: [PATCH 096/224] cancel top of purge tower early if no extra parts to print --- src/libslic3r/Print.cpp | 21 +++++++++++++++++++++ src/libslic3r/Print.hpp | 3 +++ src/libslic3r/PrintObjectSlice.cpp | 25 +++++++++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index b5d3701e41..5a0a6f8d8b 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -4345,6 +4345,27 @@ void Print::_plan_belt_purge() const bool use_flush_matrix = m_config.purge_in_prime_tower && m_config.single_extruder_multi_material; const float flush_multiplier = (float) m_config.flush_multiplier.get_at(0); + // Cancel the purge prism early: pre-scan the tool ordering for the highest + // print_z that actually has a toolchange, then drop the prism's layers above + // it so the tower stops at the last color swap (saves filament/time). This + // MUST happen before the marking loop below: ensure_perimeters_infills_order + // force-overrides the prism's extrusions on every layer (it is a dedicated + // flush object), so truncating afterwards would leave dangling overrides + // pointing into deleted layers. + { + double last_tc_z = -1.; + unsigned int cur_ext = m_wipe_tower_data.tool_ordering.first_extruder(); + for (const auto < : m_wipe_tower_data.tool_ordering.layer_tools()) + for (const unsigned int e : lt.extruders) + if (e != cur_ext) { last_tc_z = lt.print_z; cur_ext = e; } + if (last_tc_z >= 0.) + for (PrintObject *po : m_objects) + if (po->config().belt_purge_tower_object.value) { + po->belt_truncate_layers_above(last_tc_z); + break; + } + } + // Diagnostic: the prism only absorbs purge at toolchange layers whose // print_z coincides with one of its own layers. Compare the prism's layer // print_z range to the toolchange print_z range and count how many diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index fc78c5efef..2591f6cbd1 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -562,6 +562,9 @@ private: // clipping shift) by delta, used by Print::_align_belt_purge_layers() to // snap the purge prism's layers onto the printed objects' layer grid. void belt_shift_layer_grid(double delta); + // Belt mode: remove layers above z (cancel the purge prism past the last + // toolchange). Returns how many layers were dropped. + size_t belt_truncate_layers_above(coordf_t z); //BBS ExPolygons _shrink_contour_holes(double contour_delta, double hole_delta, const ExPolygons& polys) const; // BBS diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index b95bc4903f..f13b208a3a 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -1413,6 +1413,31 @@ void PrintObject::belt_shift_layer_grid(double delta) << " first_layer.print_z=" << (m_layers.empty() ? 0. : m_layers.front()->print_z); } +// Belt mode: drop layers strictly above z (used to cancel the purge prism early +// once there are no more toolchanges above z, so the tower stops at the last +// color swap instead of wasting filament up the rest of the belt). Each layer's +// cross-section is already sliced, so removing upper layers does not affect the +// last toolchange's coverage. Deletes the Layer objects and clears the new top +// layer's upper-layer link. Returns the number of layers removed. +size_t PrintObject::belt_truncate_layers_above(coordf_t z) +{ + size_t keep = m_layers.size(); + while (keep > 0 && m_layers[keep - 1]->print_z > z + EPSILON) + --keep; + if (keep >= m_layers.size()) + return 0; + const size_t removed = m_layers.size() - keep; + for (size_t i = keep; i < m_layers.size(); ++i) + delete m_layers[i]; + m_layers.resize(keep); + if (!m_layers.empty()) + m_layers.back()->upper_layer = nullptr; + BOOST_LOG_TRIVIAL(debug) << "[BELT-DEBUG] truncate purge prism above print_z=" << z + << " kept=" << keep << " removed=" << removed + << " new_top=" << (m_layers.empty() ? 0. : m_layers.back()->print_z); + return removed; +} + // 1) Decides Z positions of the layers, // 2) Initializes layers and their regions // 3) Slices the object meshes From bcfb09481c4fdfdb10ca63e4a419518c5ce6b608 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 13 Jun 2026 11:30:37 -0500 Subject: [PATCH 097/224] pull purge tower into its own files, make purge tower semi-transparent like other purge towers --- src/libslic3r/Preset.cpp | 1 + src/libslic3r/Print.cpp | 25 +- src/libslic3r/PrintConfig.cpp | 11 + src/libslic3r/PrintConfig.hpp | 3 + src/slic3r/CMakeLists.txt | 2 + src/slic3r/GUI/BeltPurgeTower.cpp | 311 +++++++++++++++++++++++++ src/slic3r/GUI/BeltPurgeTower.hpp | 48 ++++ src/slic3r/GUI/ConfigManipulation.cpp | 11 +- src/slic3r/GUI/GLCanvas3D.cpp | 22 ++ src/slic3r/GUI/Plater.cpp | 322 +------------------------- src/slic3r/GUI/Tab.cpp | 14 +- 11 files changed, 443 insertions(+), 327 deletions(-) create mode 100644 src/slic3r/GUI/BeltPurgeTower.cpp create mode 100644 src/slic3r/GUI/BeltPurgeTower.hpp diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 092923e336..1d99f19608 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1412,6 +1412,7 @@ static std::vector s_Preset_printer_options { "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", + "enable_belt_purge_tower", "gcode_flavor", "fan_kickstart", "part_cooling_fan_min_pwm", "fan_speedup_time", "fan_speedup_overhangs", "single_extruder_multi_material", "manual_filament_change", "file_start_gcode", "machine_start_gcode", "machine_end_gcode", "before_layer_change_gcode", "printing_by_object_gcode", "layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode", "change_filament_gcode", "change_extrusion_role_gcode", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 5a0a6f8d8b..4004cd81de 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -359,6 +359,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "hot_plate_temp" || opt_key == "textured_plate_temp" || opt_key == "enable_prime_tower" + || opt_key == "enable_belt_purge_tower" || opt_key == "enable_wrapping_detection" || opt_key == "prime_tower_enable_framework" || opt_key == "prime_tower_width" @@ -1474,16 +1475,18 @@ StringObjectException Print::validate(std::vector *warnin add_warning(layer_warning); } + if (m_config.belt_printer.value && m_config.enable_belt_purge_tower.value + && m_config.print_sequence == PrintSequence::ByObject + && extruders.size() > 1 && warning != nullptr) { + StringObjectException warningtemp; + warningtemp.string = L("The belt purge tower is not generated in \"By object\" print sequence; " + "filament changes will not be purged."); + warningtemp.opt_key = "enable_belt_purge_tower"; + warningtemp.is_warning = true; + *warning = warningtemp; + } + if (m_config.enable_prime_tower) { - if (m_config.belt_printer.value && m_config.print_sequence == PrintSequence::ByObject - && extruders.size() > 1 && warning != nullptr) { - StringObjectException warningtemp; - warningtemp.string = L("The belt purge tower is not generated in \"By object\" print sequence; " - "filament changes will not be purged."); - warningtemp.opt_key = "enable_prime_tower"; - warningtemp.is_warning = true; - *warning = warningtemp; - } for (const PrintObject* object : m_objects) { if (object->config().precise_z_height.value) { warn(L("Enabling both precise Z height and the prime tower may cause slicing errors."), "precise_z_height"); @@ -4127,8 +4130,10 @@ bool Print::has_wipe_tower() const // prism object via the flush-into-objects machinery instead of a wipe tower. bool Print::has_belt_purge_tower() const { + // Its own purge-tower "type", gated by the belt-only enable_belt_purge_tower + // option (not the classic enable_prime_tower). return m_config.belt_printer.value - && m_config.enable_prime_tower.value + && m_config.enable_belt_purge_tower.value && !m_config.spiral_mode.value && m_config.filament_diameter.values.size() > 1; } diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index cef9a840a8..3be885a49e 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -7321,6 +7321,17 @@ void PrintConfigDef::init_fff_params() def->set_default_value(new ConfigOptionEnum(BeltSupportZOffsetMode::Unconditional)); } + def = this->add("enable_belt_purge_tower", coBool); + def->label = L("Enable belt purge tower"); + def->category = L("Multimaterial"); + def->tooltip = L("Belt-printer replacement for the wipe/prime tower. When enabled on a belt " + "printer, a purge prism is automatically generated next to the printed parts " + "and filament-change purging is routed into it (the classic wipe tower cannot " + "be used on belt printers because its G-code bypasses the belt transform). " + "Only available on belt printers."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + def = this->add("belt_purge_tower_width", coFloat); def->label = L("Belt purge tower width"); def->category = L("Printable space"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index ffd0320149..07d8f3e3af 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1807,6 +1807,9 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionEnum, 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 + // purge prism (the belt replacement for the classic wipe/prime tower). + ((ConfigOptionBool, enable_belt_purge_tower)) //BBS ((ConfigOptionInts, additional_cooling_fan_speed)) ((ConfigOptionInts, close_additional_fan_first_x_layers)) diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index b98396f943..bdef8b587f 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -397,6 +397,8 @@ set(SLIC3R_GUI_SOURCES GUI/ParamsPanel.hpp GUI/PartPlate.cpp GUI/PartPlate.hpp + GUI/BeltPurgeTower.cpp + GUI/BeltPurgeTower.hpp GUI/FilamentGroupPopup.hpp GUI/FilamentGroupPopup.cpp GUI/PhysicalPrinterDialog.cpp diff --git a/src/slic3r/GUI/BeltPurgeTower.cpp b/src/slic3r/GUI/BeltPurgeTower.cpp new file mode 100644 index 0000000000..47ff6b6bd4 --- /dev/null +++ b/src/slic3r/GUI/BeltPurgeTower.cpp @@ -0,0 +1,311 @@ +#include "BeltPurgeTower.hpp" + +#include "GUI_App.hpp" +#include "GUI_ObjectList.hpp" +#include "PartPlate.hpp" +#include "I18N.hpp" + +#include "libslic3r/Model.hpp" +#include "libslic3r/Preset.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/PrintConfig.hpp" +#include "libslic3r/TriangleMesh.hpp" +#include "libslic3r/Geometry.hpp" +#include "libslic3r/BoundingBox.hpp" + +#include +#include +#include +#include +#include + +#include + +namespace Slic3r { +namespace GUI { + +// The classic wipe tower is disabled in belt mode (its G-code bypasses the belt +// transform), so filament-change purging is routed into this prism via +// flush_into_objects (see Print::_plan_belt_purge()). The prism is a real model +// object so it is sliced through the normal pipeline and picks up the belt +// rotation. Width across the belt is user-set (belt_purge_tower_width); height +// is sized so each tilted slicing plane's cross-section through the prism can +// absorb the worst-case purge volume of one layer; length follows the printed +// objects along the belt (plus ramp/height compensation at both tilted ends). +bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, ObjectList *obj_list, BeltPurgeSignature &sig) +{ + auto is_prism = [](const ModelObject *mo) { + const ConfigOption *opt = mo->config.option("belt_purge_tower_object"); + return opt != nullptr && opt->getBool(); + }; + + std::vector prism_idxs; + for (int i = 0; i < (int) model.objects.size(); ++i) + if (is_prism(model.objects[i])) + prism_idxs.push_back(i); + + // Deletes prism objects, keeping the sidebar and part plates in sync + // (same primitives as Plater::priv::remove(), minus scene update — the + // caller refreshes the scene). + auto remove_prisms = [&](const std::vector &idxs) { + for (auto it = idxs.rbegin(); it != idxs.rend(); ++it) { + model.delete_object(size_t(*it)); + partplate_list.notify_instance_removed(*it, -1); + obj_list->delete_object_from_list(size_t(*it)); + } + }; + + const auto &printer_config = wxGetApp().preset_bundle->printers.get_edited_preset().config; + const auto &print_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto &project_config = wxGetApp().preset_bundle->project_config; + + const auto *belt_opt = printer_config.option("belt_printer"); + const bool belt = belt_opt != nullptr && belt_opt->value; + // Belt purge tower is its own type, gated by the belt-only printer option + // enable_belt_purge_tower (not the classic process enable_prime_tower). + const bool prime_tower_enabled = printer_config.has("enable_belt_purge_tower") && printer_config.opt_bool("enable_belt_purge_tower"); + const auto *seq_opt = print_config.option>("print_sequence"); + const bool by_object = seq_opt != nullptr && seq_opt->value == PrintSequence::ByObject; + + // Filaments used and bounding extent of the non-prism objects on the + // current plate (1-based filament ids; volume extruder 0 = object default). + PartPlate *plate = partplate_list.get_curr_plate(); + std::set filaments; + double x_min = std::numeric_limits::max(); + double x_max = -std::numeric_limits::max(); + double y_min = std::numeric_limits::max(); + double y_max = -std::numeric_limits::max(); + double z_max = 0.; + bool have_objects = false; + if (belt && plate != nullptr) { + for (int obj_idx = 0; obj_idx < (int) model.objects.size(); ++obj_idx) { + const ModelObject *mo = model.objects[obj_idx]; + if (is_prism(mo)) + continue; + int obj_extruder = 1; + if (const ConfigOption *opt = mo->config.option("extruder"); opt != nullptr && opt->getInt() > 0) + obj_extruder = opt->getInt(); + bool any_instance_on_plate = false; + for (int inst_idx = 0; inst_idx < (int) mo->instances.size(); ++inst_idx) { + if (!plate->contain_instance_totally(obj_idx, inst_idx)) + continue; + any_instance_on_plate = true; + const BoundingBoxf3 bb = mo->instance_bounding_box(inst_idx); + x_min = std::min(x_min, bb.min.x()); + x_max = std::max(x_max, bb.max.x()); + y_min = std::min(y_min, bb.min.y()); + y_max = std::max(y_max, bb.max.y()); + z_max = std::max(z_max, bb.max.z()); + } + if (!any_instance_on_plate) + continue; + have_objects = true; + for (const ModelVolume *mv : mo->volumes) + for (int e : mv->get_extruders()) + filaments.insert(e > 0 ? e : obj_extruder); + } + } + + const bool wanted = belt && prime_tower_enabled && !by_object && have_objects && filaments.size() > 1; + if (!wanted) { + sig = BeltPurgeSignature{}; + if (prism_idxs.empty()) + return false; + remove_prisms(prism_idxs); + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] belt purge tower removed (conditions not met)"; + return true; + } + + // --- Sizing ----------------------------------------------------------- + const double width = print_config.has("belt_purge_tower_width") ? std::max(1., print_config.opt_float("belt_purge_tower_width")) : 35.; + const double layer_h = print_config.has("layer_height") ? print_config.opt_float("layer_height") : 0.2; + + // Belt geometry. The rotation axis is the gantry tilt axis; the belt + // travels along the *other* horizontal axis (X-rotation -> belt along Y, + // the CR-30 default). The purge prism is a long bar laid along the belt + // travel direction, beside the parts. For no/Z rotation we fall back to + // vertical slicing geometry (theta = 90 deg). + const auto *axis_opt = printer_config.option>("belt_slice_rotation"); + const auto *angle_opt = printer_config.option("belt_slice_rotation_angle"); + const BeltRotationAxis rot = axis_opt != nullptr ? axis_opt->value : BeltRotationAxis::X; + const bool belt_is_y = (rot != BeltRotationAxis::Y); // X / None / Z -> belt along Y + double theta = M_PI / 2.; + if ((rot == BeltRotationAxis::X || rot == BeltRotationAxis::Y) && angle_opt != nullptr && std::abs(angle_opt->value) > EPSILON) + theta = std::clamp(Geometry::deg2rad(std::abs(angle_opt->value)), Geometry::deg2rad(5.), M_PI / 2.); + const double sin_t = std::sin(theta); + const double cot_t = std::cos(theta) / sin_t; + + // Parts' extent along the belt-travel axis and the lateral (across-belt) axis. + const double belt_min = belt_is_y ? y_min : x_min; + const double belt_max = belt_is_y ? y_max : x_max; + const double lat_min = belt_is_y ? x_min : y_min; + const double lat_max = belt_is_y ? x_max : y_max; + + // Worst-case purge volume of one layer: up to (filament count - 1) + // toolchanges, each needing the worst flush matrix entry (mirrors the + // volume selection in Print::_plan_belt_purge()). + // NOTE: both purge_in_prime_tower and single_extruder_multi_material are + // PRINTER options (Preset.cpp s_Preset_printer_options) — read them from the + // printer preset. Reading purge_in_prime_tower from the print preset returns + // has()==false, collapsing use_matrix to false and sizing the tower for the + // small prime_volume instead of the real color-change flush. This must match + // the backend Print::_plan_belt_purge() which reads both from the merged config. + const bool use_matrix = (printer_config.has("purge_in_prime_tower") && printer_config.opt_bool("purge_in_prime_tower")) + && (printer_config.has("single_extruder_multi_material") && printer_config.opt_bool("single_extruder_multi_material")); + double max_flush = print_config.has("prime_volume") ? print_config.opt_float("prime_volume") : 45.; + if (use_matrix) { + const size_t extruder_nums = wxGetApp().preset_bundle->get_printer_extruder_count(); + const std::vector matrix = get_flush_volumes_matrix( + project_config.option("flush_volumes_matrix")->values, 0, extruder_nums); + const auto * multi_opt = project_config.option("flush_multiplier"); + const double multiplier = multi_opt != nullptr && !multi_opt->values.empty() ? multi_opt->get_at(0) : 1.; + const int n_total = (int) (std::sqrt(double(matrix.size())) + 0.5); + double m = 0.; + for (int i : filaments) + for (int j : filaments) + if (i != j && i <= n_total && j <= n_total) + m = std::max(m, matrix[size_t(i - 1) * n_total + size_t(j - 1)]); + if (m > 0.) + max_flush = m * multiplier; + } + const double v_layer = double(filaments.size() - 1) * max_flush; + + // Height from the per-layer purge demand. A tilted slicing plane cuts a + // width x (height/sin) rectangle out of the bar, so one layer slab absorbs + // width * (height/sin) * layer_height of purge. Solve for the height that + // holds the worst-case per-layer purge, with eta (infill/perimeter packing) + // and a safety margin for the tilt ramps / grid-alignment slop, plus a + // minimum so the tower is a real printable body rather than a sliver. + const double eta = 0.85; + const double safety = 1.6; + const double printable_height = printer_config.has("printable_height") ? printer_config.opt_float("printable_height") : 250.; + double height = safety * v_layer * sin_t / (width * layer_h * eta); + height = std::clamp(height, 8.0, std::max(8.0, printable_height)); + + // --- Idempotence (input-keyed) ---------------------------------------- + auto q = [](double v) { return std::lround(v * 10.0); }; // 0.1 mm quantization + BeltPurgeSignature new_sig; + new_sig.valid = true; + new_sig.filament_count = (int) filaments.size(); + new_sig.key[0] = q(width); + new_sig.key[1] = q(layer_h); + new_sig.key[2] = q(height); + new_sig.key[3] = q(belt_min); + new_sig.key[4] = q(belt_max); + new_sig.key[5] = q(lat_min); + new_sig.key[6] = q(z_max); + if (prism_idxs.size() == 1 && new_sig == sig) + return false; // already up to date — do not touch the model + + // --- Position ---------------------------------------------------------- + // Belt-travel axis. With the mesh rotated by theta before slicing, a machine + // point (y,z) maps to slicing-Z = y*sin(theta) + z*cos(theta). The parts + // occupy slicing-Z in [y_min*sin, y_max*sin + z_max*cos], and the bar's + // FULL-cross-section region (the part not in a triangular end ramp) spans + // slicing-Z [belt_start*sin + H*cos, belt_end*sin]. Covering the parts' + // whole band needs belt_start <= y_min - H*cot (bar's own leading ramp) and + // belt_end >= y_max + z_max*cot (parts' top features print further up the + // belt). The trailing z_max*cot term dominates the bar's own ramp. + const double margin = 5.; + const double ramp_compensation = height / sin_t; + const double belt_start = std::max(0.0, belt_min - ramp_compensation); // leading ramp, toward Y=0 + const double belt_end = belt_max + margin + ramp_compensation + z_max * cot_t; // + parts' top-feature belt reach + const double length = std::max(belt_end - belt_start, 10.); + const double belt_center = 0.5 * (belt_start + belt_end); + + // Across-belt: flush against the bed's maximum edge, inset by half the bar + // width so the bar's far edge sits on the boundary and the whole bar stays + // on the bed. The bed (printable_area) is plate-local but model instances + // live in the plate's world frame, so add the plate origin's lateral + // component. lat_min/lat_max come from instance_bounding_box (world frame). + const Vec3d plate_origin = plate->get_origin(); + const double lat_origin = plate_origin[belt_is_y ? 0 : 1]; + const double inset = 1.; + double lat_center = lat_max + 5. + 0.5 * width; // fallback: just past the parts + BoundingBoxf bed_ext_dbg; + if (const auto *bed_opt = printer_config.option("printable_area"); + bed_opt != nullptr && !bed_opt->values.empty()) { + const BoundingBoxf bed_ext = get_extents(bed_opt->values); + bed_ext_dbg = bed_ext; + const double bed_lat_max = belt_is_y ? bed_ext.max.x() : bed_ext.max.y(); + lat_center = lat_origin + bed_lat_max - inset - 0.5 * width; + } + + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge place" + << " plate_origin=(" << plate_origin.x() << "," << plate_origin.y() << ")" + << " parts_x=[" << x_min << "," << x_max << "] parts_y=[" << y_min << "," << y_max << "] z_max=" << z_max + << " bed_ext=[" << bed_ext_dbg.min.x() << "," << bed_ext_dbg.min.y() + << " -> " << bed_ext_dbg.max.x() << "," << bed_ext_dbg.max.y() << "]" + << " lat_center=" << lat_center << " belt=[" << belt_start << "," << belt_end << "]"; + + // Orient the bar: long axis = belt travel, width = lateral, height = Z. + const double size_x = belt_is_y ? width : length; + const double size_y = belt_is_y ? length : width; + const Vec3d desired_center(belt_is_y ? lat_center : belt_center, + belt_is_y ? belt_center : lat_center, + 0.5 * height); + + // --- (Re)create --------------------------------------------------------- + if (!prism_idxs.empty()) + remove_prisms(prism_idxs); + + ModelObject *new_object = model.add_object(); + new_object->name = _u8L("Belt Purge Tower"); + new_object->add_instance(); + ModelVolume *new_volume = new_object->add_volume(make_cube(size_x, size_y, height)); + new_volume->name = new_object->name; + + auto &cfg = new_object->config; + cfg.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true)); + cfg.set_key_value("flush_into_objects", new ConfigOptionBool(true)); + cfg.set_key_value("extruder", new ConfigOptionInt(1)); + // Sacrificial solid prism: one wall, no shells, dense rectilinear infill — + // every extrusion is overriddable, so the absorbed volume matches the + // cross-section x layer-height estimate used for the height above. + cfg.set_key_value("wall_loops", new ConfigOptionInt(1)); + cfg.set_key_value("top_shell_layers", new ConfigOptionInt(0)); + cfg.set_key_value("bottom_shell_layers", new ConfigOptionInt(0)); + cfg.set_key_value("sparse_infill_density", new ConfigOptionPercent(100)); + cfg.set_key_value("sparse_infill_pattern", new ConfigOptionEnum(ipRectilinear)); + cfg.set_key_value("enable_support", new ConfigOptionBool(false)); + cfg.set_key_value("brim_type", new ConfigOptionEnum(btNoBrim)); + cfg.set_key_value("seam_slope_type", new ConfigOptionEnum(SeamScarfType::None)); + cfg.set_key_value("precise_z_height", new ConfigOptionBool(false)); + + // Position by the belt-calibration pattern: drop to the bed, then translate + // the instance by the delta between the object's ACTUAL bbox center and the + // target. Setting the instance offset directly is unreliable here — the + // freshly added cube's local frame is not centered, so set_offset() lands + // the min corner (not the center) on the target, leaving the bar centered + // on the bed edge with half of it hanging off. + new_object->invalidate_bounding_box(); + new_object->ensure_on_bed(); + const BoundingBoxf3 cur = new_object->bounding_box_exact(); + new_object->translate_instances(Vec3d(desired_center.x() - cur.center().x(), + desired_center.y() - cur.center().y(), + 0.0)); + new_object->instances.front()->set_assemble_transformation(new_object->instances.front()->get_transformation()); + + const size_t obj_idx = model.objects.size() - 1; + // Registers the object in the sidebar and notifies the part plates; + // selection is left untouched (auto-managed object). + obj_list->add_object_to_list(obj_idx, /*call_selection_changed=*/false); + + // Record the inputs that produced this prism so subsequent ticks are no-ops + // until the parts/config actually change. + sig = new_sig; + + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] belt purge tower generated" + << " belt_is_y=" << belt_is_y + << " W=" << width << " L=" << length << " H=" << height + << " v_layer=" << v_layer << " max_flush=" << max_flush + << " filaments=" << filaments.size() + << " theta_deg=" << Geometry::rad2deg(theta) + << " desired_center=(" << desired_center.x() << "," << desired_center.y() << "," << desired_center.z() << ")" + << " achieved_center=(" << new_object->bounding_box_exact().center().x() << "," + << new_object->bounding_box_exact().center().y() << ")"; + return true; +} + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/BeltPurgeTower.hpp b/src/slic3r/GUI/BeltPurgeTower.hpp new file mode 100644 index 0000000000..b1735303c5 --- /dev/null +++ b/src/slic3r/GUI/BeltPurgeTower.hpp @@ -0,0 +1,48 @@ +#ifndef slic3r_GUI_BeltPurgeTower_hpp_ +#define slic3r_GUI_BeltPurgeTower_hpp_ + +// ORCA-Belt: auto-managed purge prism for belt printers. +// +// Kept in its own translation unit (not buried in Plater.cpp) so it stays out +// of the way of unrelated upstream changes and carries no regression risk for +// normal printers: nothing here runs unless the printer is a belt printer with +// the belt purge tower enabled. See Slic3r::GUI::ensure_belt_purge_tower(). + +namespace Slic3r { +class Model; +class PartPlateList; +namespace GUI { +class ObjectList; + +// Inputs of the last belt purge prism generation. Idempotence is keyed on these +// (not the prism's resulting bbox): the prism gets nudged by plate assignment +// after creation, so comparing its bbox would falsely detect a change and +// regenerate it on every background-process tick — which would re-invalidate a +// freshly sliced result and make the G-code preview unreachable. +struct BeltPurgeSignature +{ + bool valid = false; + int filament_count = 0; + long key[7] = {0}; // rounded geometry inputs (0.1 mm units) + bool operator==(const BeltPurgeSignature &o) const + { + if (valid != o.valid || filament_count != o.filament_count) + return false; + for (int i = 0; i < 7; ++i) + if (key[i] != o.key[i]) + return false; + return true; + } +}; + +// Keep the auto-generated belt purge prism in sync with the current config and +// plate contents. Creates / updates / removes the marked prism ModelObject. +// Returns true when the model was mutated (caller should refresh the scene). +// Runs on every background-process update, so it is idempotent: it only mutates +// the model when the desired prism differs from the cached signature in `sig`. +bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, ObjectList *obj_list, BeltPurgeSignature &sig); + +} // namespace GUI +} // namespace Slic3r + +#endif // slic3r_GUI_BeltPurgeTower_hpp_ diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index ffd4d43a71..71a111ef29 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -972,10 +972,13 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in bool have_prime_tower = config->opt_bool("enable_prime_tower"); // ORCA-Belt: belt printers replace the classic wipe tower with the - // auto-generated belt purge prism — only its width is tunable; the - // classic tower geometry options do not apply. (is_belt_printer is - // computed at the top of this function.) - toggle_line("belt_purge_tower_width", have_prime_tower && is_belt_printer); + // auto-generated belt purge prism, enabled by the belt-only printer option + // enable_belt_purge_tower. Its width (a process option) is only relevant + // then. (is_belt_printer is computed at the top of this function.) + const bool have_belt_purge_tower = is_belt_printer + && preset_bundle->printers.get_edited_preset().config.has("enable_belt_purge_tower") + && preset_bundle->printers.get_edited_preset().config.opt_bool("enable_belt_purge_tower"); + toggle_line("belt_purge_tower_width", have_belt_purge_tower); for (auto el : {"prime_tower_width", "prime_tower_brim_width", "prime_tower_skip_points", "wipe_tower_wall_type", "prime_tower_infill_gap","prime_tower_enable_framework", "enable_tower_interface_features"}) toggle_line(el, have_prime_tower && !is_belt_printer); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 8ff05862a8..ca517c4303 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -2948,6 +2948,28 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re } update_volumes_colors_by_extruder(); + + // ORCA-Belt: render the auto-generated belt purge prism like a wipe tower — + // semi-transparent, in its (filament) color. It is a real sliced object, so + // the G-code preview already shows the actual per-layer purge colors; here in + // the editor we just make the block translucent so it reads as a purge tower + // rather than a solid part. update_colors_by_extruder() preserves alpha when + // not updating alpha, so lowering it once sticks across recolors. + if (m_model != nullptr) { + for (GLVolume *volume : m_volumes.volumes) { + if (volume == nullptr || volume->volume_idx() < 0) + continue; + const int obj_idx = volume->object_idx(); + if (obj_idx < 0 || obj_idx >= (int) m_model->objects.size()) + continue; + const ConfigOption *opt = m_model->objects[obj_idx]->config.option("belt_purge_tower_object"); + if (opt != nullptr && opt->getBool()) { + volume->color.a(0.66f); + volume->force_transparent = true; + } + } + } + // Update selection indices based on the old/new GLVolumeCollection. if (m_selection.get_mode() == Selection::Instance) m_selection.instances_changed(instance_ids_selected); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 656a7d31e2..c2ddb897bf 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -105,6 +105,7 @@ #include "GUI_Preview.hpp" #include "3DBed.hpp" #include "PartPlate.hpp" +#include "BeltPurgeTower.hpp" #include "Camera.hpp" #include "Mouse3DController.hpp" #include "Tab.hpp" @@ -5476,24 +5477,10 @@ struct Plater::priv void remove(size_t obj_idx); bool delete_object_from_model(size_t obj_idx, bool refresh_immediately = true); //BBS // ORCA-Belt: keep the auto-generated belt purge prism in sync with the - // config and plate contents. Returns true when the model was mutated - // (caller should refresh the scene). + // config and plate contents (thin wrapper over GUI::ensure_belt_purge_tower + // in BeltPurgeTower.cpp). Returns true when the model was mutated. bool ensure_belt_purge_tower(); - // Inputs of the last belt purge prism generation. Idempotence is keyed on - // these (not the prism's resulting bbox): the prism gets nudged by plate - // assignment after creation, so comparing its bbox would falsely detect a - // change and regenerate it on every background-process tick — which would - // re-invalidate a freshly sliced result and make the preview unreachable. - struct BeltPurgeSignature { - bool valid = false; - int filament_count = 0; - long key[7] = {0}; // rounded geometry inputs (0.1 mm units) - bool operator==(const BeltPurgeSignature &o) const { - if (valid != o.valid || filament_count != o.filament_count) return false; - for (int i = 0; i < 7; ++i) if (key[i] != o.key[i]) return false; - return true; - } - } m_belt_purge_sig; + BeltPurgeSignature m_belt_purge_sig; void delete_all_objects_from_model(); void reset(bool apply_presets_change = false); void center_selection(); @@ -8898,305 +8885,16 @@ void Plater::priv::process_validation_warnings(const std::vectorconfig.option("belt_purge_tower_object"); - return opt != nullptr && opt->getBool(); - }; - - std::vector prism_idxs; - for (int i = 0; i < (int) model.objects.size(); ++i) - if (is_prism(model.objects[i])) - prism_idxs.push_back(i); - - // Deletes prism objects, keeping the sidebar and part plates in sync - // (same primitives as Plater::priv::remove(), minus scene update — the - // caller refreshes the scene). - auto remove_prisms = [this](const std::vector &idxs) { - for (auto it = idxs.rbegin(); it != idxs.rend(); ++it) { - model.delete_object(size_t(*it)); - partplate_list.notify_instance_removed(*it, -1); - sidebar->obj_list()->delete_object_from_list(size_t(*it)); - } - }; - - const auto &printer_config = wxGetApp().preset_bundle->printers.get_edited_preset().config; - const auto &print_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; - auto &project_config = wxGetApp().preset_bundle->project_config; - - const auto *belt_opt = printer_config.option("belt_printer"); - const bool belt = belt_opt != nullptr && belt_opt->value; - const bool prime_tower_enabled = print_config.has("enable_prime_tower") && print_config.opt_bool("enable_prime_tower"); - const auto *seq_opt = print_config.option>("print_sequence"); - const bool by_object = seq_opt != nullptr && seq_opt->value == PrintSequence::ByObject; - - // Filaments used and bounding extent of the non-prism objects on the - // current plate (1-based filament ids; volume extruder 0 = object default). - PartPlate *plate = partplate_list.get_curr_plate(); - std::set filaments; - double x_min = std::numeric_limits::max(); - double x_max = -std::numeric_limits::max(); - double y_min = std::numeric_limits::max(); - double y_max = -std::numeric_limits::max(); - double z_max = 0.; - bool have_objects = false; - if (belt && plate != nullptr) { - for (int obj_idx = 0; obj_idx < (int) model.objects.size(); ++obj_idx) { - const ModelObject *mo = model.objects[obj_idx]; - if (is_prism(mo)) - continue; - int obj_extruder = 1; - if (const ConfigOption *opt = mo->config.option("extruder"); opt != nullptr && opt->getInt() > 0) - obj_extruder = opt->getInt(); - bool any_instance_on_plate = false; - for (int inst_idx = 0; inst_idx < (int) mo->instances.size(); ++inst_idx) { - if (!plate->contain_instance_totally(obj_idx, inst_idx)) - continue; - any_instance_on_plate = true; - const BoundingBoxf3 bb = mo->instance_bounding_box(inst_idx); - x_min = std::min(x_min, bb.min.x()); - x_max = std::max(x_max, bb.max.x()); - y_min = std::min(y_min, bb.min.y()); - y_max = std::max(y_max, bb.max.y()); - z_max = std::max(z_max, bb.max.z()); - } - if (!any_instance_on_plate) - continue; - have_objects = true; - for (const ModelVolume *mv : mo->volumes) - for (int e : mv->get_extruders()) - filaments.insert(e > 0 ? e : obj_extruder); - } - } - - const bool wanted = belt && prime_tower_enabled && !by_object && have_objects && filaments.size() > 1; - if (!wanted) { - m_belt_purge_sig = BeltPurgeSignature{}; - if (prism_idxs.empty()) - return false; - remove_prisms(prism_idxs); - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] belt purge tower removed (conditions not met)"; - return true; - } - - // --- Sizing ----------------------------------------------------------- - const double width = print_config.has("belt_purge_tower_width") ? std::max(1., print_config.opt_float("belt_purge_tower_width")) : 35.; - const double layer_h = print_config.has("layer_height") ? print_config.opt_float("layer_height") : 0.2; - - // Belt geometry. The rotation axis is the gantry tilt axis; the belt - // travels along the *other* horizontal axis (X-rotation -> belt along Y, - // the CR-30 default). The purge prism is a long bar laid along the belt - // travel direction, beside the parts. For no/Z rotation we fall back to - // vertical slicing geometry (theta = 90 deg). - const auto *axis_opt = printer_config.option>("belt_slice_rotation"); - const auto *angle_opt = printer_config.option("belt_slice_rotation_angle"); - const BeltRotationAxis rot = axis_opt != nullptr ? axis_opt->value : BeltRotationAxis::X; - const bool belt_is_y = (rot != BeltRotationAxis::Y); // X / None / Z -> belt along Y - double theta = M_PI / 2.; - if ((rot == BeltRotationAxis::X || rot == BeltRotationAxis::Y) && angle_opt != nullptr && std::abs(angle_opt->value) > EPSILON) - theta = std::clamp(Geometry::deg2rad(std::abs(angle_opt->value)), Geometry::deg2rad(5.), M_PI / 2.); - const double sin_t = std::sin(theta); - const double cot_t = std::cos(theta) / sin_t; - - // Parts' extent along the belt-travel axis and the lateral (across-belt) axis. - const double belt_min = belt_is_y ? y_min : x_min; - const double belt_max = belt_is_y ? y_max : x_max; - const double lat_min = belt_is_y ? x_min : y_min; - const double lat_max = belt_is_y ? x_max : y_max; - - // Worst-case purge volume of one layer: up to (filament count - 1) - // toolchanges, each needing the worst flush matrix entry (mirrors the - // volume selection in Print::_plan_belt_purge()). - // NOTE: both purge_in_prime_tower and single_extruder_multi_material are - // PRINTER options (Preset.cpp s_Preset_printer_options) — read them from the - // printer preset. Reading purge_in_prime_tower from the print preset returns - // has()==false, collapsing use_matrix to false and sizing the tower for the - // small prime_volume instead of the real color-change flush. This must match - // the backend Print::_plan_belt_purge() which reads both from the merged config. - const bool use_matrix = (printer_config.has("purge_in_prime_tower") && printer_config.opt_bool("purge_in_prime_tower")) - && (printer_config.has("single_extruder_multi_material") && printer_config.opt_bool("single_extruder_multi_material")); - double max_flush = print_config.has("prime_volume") ? print_config.opt_float("prime_volume") : 45.; - if (use_matrix) { - const size_t extruder_nums = wxGetApp().preset_bundle->get_printer_extruder_count(); - const std::vector matrix = get_flush_volumes_matrix( - project_config.option("flush_volumes_matrix")->values, 0, extruder_nums); - const auto * multi_opt = project_config.option("flush_multiplier"); - const double multiplier = multi_opt != nullptr && !multi_opt->values.empty() ? multi_opt->get_at(0) : 1.; - const int n_total = (int) (std::sqrt(double(matrix.size())) + 0.5); - double m = 0.; - for (int i : filaments) - for (int j : filaments) - if (i != j && i <= n_total && j <= n_total) - m = std::max(m, matrix[size_t(i - 1) * n_total + size_t(j - 1)]); - if (m > 0.) - max_flush = m * multiplier; - } - const double v_layer = double(filaments.size() - 1) * max_flush; - - // Height from the per-layer purge demand. A tilted slicing plane cuts a - // width x (height/sin) rectangle out of the bar, so one layer slab absorbs - // width * (height/sin) * layer_height of purge. Solve for the height that - // holds the worst-case per-layer purge, with: - // eta - infill/perimeter packing (not all of the cross-section is solid) - // safety - margin for the tilt ramp at the bar ends and layer-grid - // alignment slop, where a layer cuts less than the full section - // and a minimum so the tower is a real printable body rather than a sliver. - const double eta = 0.85; - const double safety = 1.6; - const double printable_height = printer_config.has("printable_height") ? printer_config.opt_float("printable_height") : 250.; - double height = safety * v_layer * sin_t / (width * layer_h * eta); - height = std::clamp(height, 8.0, std::max(8.0, printable_height)); - - // --- Idempotence (input-keyed) ---------------------------------------- - // Key on the generation inputs, NOT the prism's resulting bbox: plate - // assignment nudges the prism after creation, so a bbox comparison sees a - // "change" every tick and regenerates the prism, which re-invalidates any - // freshly sliced result and makes the G-code preview unreachable. - auto q = [](double v) { return std::lround(v * 10.0); }; // 0.1 mm quantization - BeltPurgeSignature sig; - sig.valid = true; - sig.filament_count = (int) filaments.size(); - sig.key[0] = q(width); - sig.key[1] = q(layer_h); - sig.key[2] = q(height); - sig.key[3] = q(belt_min); - sig.key[4] = q(belt_max); - sig.key[5] = q(lat_min); - sig.key[6] = q(z_max); - if (prism_idxs.size() == 1 && sig == m_belt_purge_sig) - return false; // already up to date — do not touch the model - - // --- Position ---------------------------------------------------------- - // Belt-travel axis. With the mesh rotated by theta before slicing, a machine - // point (y,z) maps to slicing-Z = y*sin(theta) + z*cos(theta). The parts - // occupy slicing-Z in [y_min*sin, y_max*sin + z_max*cos], and the bar's - // FULL-cross-section region (the part not in a triangular end ramp) spans - // slicing-Z [belt_start*sin + H*cos, belt_end*sin]. Covering the parts' - // whole band therefore needs: - // - leading edge belt_start <= y_min - H*cot(theta) (bar's own ramp) - // - trailing edge belt_end >= y_max + z_max*cot(theta) (parts' top - // features print further up the belt, so the bar must reach there). - // The leading side uses the bar-ramp allowance height/sin (>= H*cot, slight - // over-cover); the trailing side adds the parts' height projection - // z_max*cot, which dominates the bar's own ramp. - const double margin = 5.; - const double ramp_compensation = height / sin_t; - const double belt_start = std::max(0.0, belt_min - ramp_compensation); // leading ramp, toward Y=0 - const double belt_end = belt_max + margin + ramp_compensation + z_max * cot_t; // + parts' top-feature belt reach - const double length = std::max(belt_end - belt_start, 10.); - const double belt_center = 0.5 * (belt_start + belt_end); - - // Across-belt: flush against the bed's maximum edge, inset by half the bar - // width so the bar's far edge sits on the boundary and the whole bar stays - // on the bed. The bed (printable_area) is plate-local but model instances - // live in the plate's world frame, so add the plate origin's lateral - // component (plate-origin compensation is needed throughout the belt - // pipeline). lat_min/lat_max come from instance_bounding_box (world frame). - const Vec3d plate_origin = plate->get_origin(); - const double lat_origin = plate_origin[belt_is_y ? 0 : 1]; - const double inset = 1.; - double lat_center = lat_max + 5. + 0.5 * width; // fallback: just past the parts - BoundingBoxf bed_ext_dbg; - if (const auto *bed_opt = printer_config.option("printable_area"); - bed_opt != nullptr && !bed_opt->values.empty()) { - const BoundingBoxf bed_ext = get_extents(bed_opt->values); - bed_ext_dbg = bed_ext; - const double bed_lat_max = belt_is_y ? bed_ext.max.x() : bed_ext.max.y(); - lat_center = lat_origin + bed_lat_max - inset - 0.5 * width; - } - - // Ground-truth diagnostic for placement frames (warning level so it lands - // in the GUI debug log): plate origin, bed extents, parts bbox, result. - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge place" - << " plate_origin=(" << plate_origin.x() << "," << plate_origin.y() << ")" - << " parts_x=[" << x_min << "," << x_max << "] parts_y=[" << y_min << "," << y_max << "] z_max=" << z_max - << " bed_ext=[" << bed_ext_dbg.min.x() << "," << bed_ext_dbg.min.y() - << " -> " << bed_ext_dbg.max.x() << "," << bed_ext_dbg.max.y() << "]" - << " lat_center=" << lat_center << " belt=[" << belt_start << "," << belt_end << "]"; - - // Orient the bar: long axis = belt travel, width = lateral, height = Z. - const double size_x = belt_is_y ? width : length; - const double size_y = belt_is_y ? length : width; - const Vec3d desired_center(belt_is_y ? lat_center : belt_center, - belt_is_y ? belt_center : lat_center, - 0.5 * height); - - // --- (Re)create --------------------------------------------------------- - if (!prism_idxs.empty()) - remove_prisms(prism_idxs); - - ModelObject *new_object = model.add_object(); - new_object->name = _u8L("Belt Purge Tower"); - new_object->add_instance(); - ModelVolume *new_volume = new_object->add_volume(make_cube(size_x, size_y, height)); - new_volume->name = new_object->name; - - auto &cfg = new_object->config; - cfg.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true)); - cfg.set_key_value("flush_into_objects", new ConfigOptionBool(true)); - cfg.set_key_value("extruder", new ConfigOptionInt(1)); - // Sacrificial solid prism: one wall, no shells, dense rectilinear infill — - // every extrusion is overriddable, so the absorbed volume matches the - // cross-section x layer-height estimate used for the height above. - cfg.set_key_value("wall_loops", new ConfigOptionInt(1)); - cfg.set_key_value("top_shell_layers", new ConfigOptionInt(0)); - cfg.set_key_value("bottom_shell_layers", new ConfigOptionInt(0)); - cfg.set_key_value("sparse_infill_density", new ConfigOptionPercent(100)); - cfg.set_key_value("sparse_infill_pattern", new ConfigOptionEnum(ipRectilinear)); - cfg.set_key_value("enable_support", new ConfigOptionBool(false)); - cfg.set_key_value("brim_type", new ConfigOptionEnum(btNoBrim)); - cfg.set_key_value("seam_slope_type", new ConfigOptionEnum(SeamScarfType::None)); - cfg.set_key_value("precise_z_height", new ConfigOptionBool(false)); - - // Position by the belt-calibration pattern: drop to the bed, then translate - // the instance by the delta between the object's ACTUAL bbox center and the - // target. Setting the instance offset directly is unreliable here — the - // freshly added cube's local frame is not centered, so set_offset() lands - // the min corner (not the center) on the target, leaving the bar centered - // on the bed edge with half of it hanging off. - new_object->invalidate_bounding_box(); - new_object->ensure_on_bed(); - const BoundingBoxf3 cur = new_object->bounding_box_exact(); - new_object->translate_instances(Vec3d(desired_center.x() - cur.center().x(), - desired_center.y() - cur.center().y(), - 0.0)); - new_object->instances.front()->set_assemble_transformation(new_object->instances.front()->get_transformation()); - - const size_t obj_idx = model.objects.size() - 1; - // Registers the object in the sidebar and notifies the part plates; - // selection is left untouched (auto-managed object). - sidebar->obj_list()->add_object_to_list(obj_idx, /*call_selection_changed=*/false); - - // Record the inputs that produced this prism so subsequent ticks are no-ops - // until the parts/config actually change. - m_belt_purge_sig = sig; - - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] belt purge tower generated" - << " belt_is_y=" << belt_is_y - << " W=" << width << " L=" << length << " H=" << height - << " v_layer=" << v_layer << " max_flush=" << max_flush - << " filaments=" << filaments.size() - << " theta_deg=" << Geometry::rad2deg(theta) - << " desired_center=(" << desired_center.x() << "," << desired_center.y() << "," << desired_center.z() << ")" - << " achieved_center=(" << new_object->bounding_box_exact().center().x() << "," - << new_object->bounding_box_exact().center().y() << ")"; - return true; + return GUI::ensure_belt_purge_tower(model, partplate_list, sidebar->obj_list(), m_belt_purge_sig); } + // Update background processing thread from the current config and Model. // Returns a bitmask of UpdateBackgroundProcessReturnState. unsigned int Plater::priv::update_background_process(bool force_validation, bool postpone_error_messages, bool switch_print) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 25287c79c3..bf0ce6e65e 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -5699,6 +5699,12 @@ if (is_marlin_flavor) optgroup->append_single_option_line("enable_filament_ramming", "printer_multimaterial_wipe_tower#enable-filament-ramming"); optgroup->append_single_option_line("tool_change_on_wipe_tower", "printer_multimaterial_wipe_tower#tool-change-on-wipe-tower"); + // Orca-Belt: belt printers replace the classic wipe tower with an + // auto-generated purge prism; this is its enable (gated to belt printers + // in toggle_options()). + optgroup = page->new_optgroup(L("Belt purge tower"), "param_tower"); + optgroup->append_single_option_line("enable_belt_purge_tower", "printer_multimaterial_wipe_tower"); + optgroup = page->new_optgroup(L("Single extruder multi-material parameters"), "param_settings"); optgroup->append_single_option_line("cooling_tube_retraction", "printer_multimaterial_semm_parameters#cooling-tube-position"); @@ -6233,8 +6239,14 @@ void TabPrinter::toggle_options() } if (m_active_page->title() == L("Multimaterial")) { + // Orca-Belt: belt printers use the belt purge tower instead of the classic + // wipe tower — show its enable only on belt printers, and hide the classic + // wipe-tower fields there (the classic tower's G-code bypasses the belt transform). + const bool is_belt_printer = m_config->opt_bool("belt_printer"); + toggle_line("enable_belt_purge_tower", is_belt_printer); + const bool supports_wipe_tower_2 = !is_BBL_printer && m_config->opt_enum("wipe_tower_type") == WipeTowerType::Type2; - toggle_line("wipe_tower_type", !is_BBL_printer); + toggle_line("wipe_tower_type", !is_BBL_printer && !is_belt_printer); // SoftFever: hide specific settings for BBL printer for (auto el : { "enable_filament_ramming", From 5ea6ccc56a7a5664783475603b4e9006e027afb2 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 13 Jun 2026 14:35:00 -0500 Subject: [PATCH 098/224] cleanup, early purge tower stop if no longer necessary --- src/libslic3r/BeltPurge.cpp | 316 +++++++++++++++++++++++++++++ src/libslic3r/CMakeLists.txt | 1 + src/libslic3r/Print.cpp | 231 --------------------- src/libslic3r/PrintObjectSlice.cpp | 45 ---- 4 files changed, 317 insertions(+), 276 deletions(-) create mode 100644 src/libslic3r/BeltPurge.cpp diff --git a/src/libslic3r/BeltPurge.cpp b/src/libslic3r/BeltPurge.cpp new file mode 100644 index 0000000000..a9c2ffceb8 --- /dev/null +++ b/src/libslic3r/BeltPurge.cpp @@ -0,0 +1,316 @@ +// ORCA-Belt: backend of the belt purge tower (the belt replacement for the +// classic wipe/prime tower). +// +// Kept in its own translation unit so the belt-purge logic stays out of the way +// of unrelated upstream changes to Print.cpp / PrintObjectSlice.cpp and carries +// no regression risk for normal printers: none of these methods do anything +// unless the print is a belt printer with the belt purge tower enabled. +// +// Print::has_belt_purge_tower() - is the belt purge tower active? +// Print::_align_belt_purge_layers() - snap the prism's layer grid onto the +// printed objects' grid +// Print::_plan_belt_purge() - route filament-change purging into the +// prism (flush-into-objects), no wipe tower +// PrintObject::belt_shift_layer_grid() - shift a sliced layer grid +// PrintObject::belt_truncate_layers_above() - cancel the prism past the last swap +// +// (Declarations live in Print.hpp alongside the rest of the Print interface.) + +#include "Print.hpp" +#include "PrintConfig.hpp" +#include "Exception.hpp" +#include "GCode/ToolOrdering.hpp" +#include "Layer.hpp" +#include "I18N.hpp" +#include "format.hpp" +#include "LocalesUtils.hpp" +#include "libslic3r.h" + +#include +#include +#include + +#include + +namespace Slic3r { + +// Belt purge prism: purging after filament changes is routed into a sliced +// prism object via the flush-into-objects machinery instead of a wipe tower. +bool Print::has_belt_purge_tower() const +{ + // Its own purge-tower "type", gated by the belt-only enable_belt_purge_tower + // option (not the classic enable_prime_tower). + return m_config.belt_printer.value + && m_config.enable_belt_purge_tower.value + && !m_config.spiral_mode.value + && m_config.filament_diameter.values.size() > 1; +} + +// Belt mode: snap the purge prism's layer grid onto the printed objects' grid. +// After belt slicing every object's layer print_z carries a per-object global +// z offset (mesh-vertex-scan belt_z_shift + instance-Y-dependent terms), so +// objects at different belt-Y positions do not share a layer grid. Purge +// marking looks absorbers up with get_layer_at_printz(lt.print_z, EPSILON), +// so the prism only absorbs purge at toolchange print_z values that coincide +// with one of its own layers. Shifting the prism by at most half a layer +// (a sub-layer-height displacement along the belt) makes its grid residue +// match the reference object's; both grids step by the same layer height +// (enforced by validate()), so matching residues means exact layer matches. +void Print::_align_belt_purge_layers() +{ + PrintObject *prism = nullptr; + for (PrintObject *po : m_objects) + if (po->config().belt_purge_tower_object.value) { + prism = po; + break; + } + if (prism == nullptr || prism->layers().empty()) + return; + + const double h = prism->config().layer_height.value; + if (h <= EPSILON) + return; + + // Grid residue of an object's layer grid: identical for all of an object's + // layers above the first since they step by h. + auto grid_offset = [h](const PrintObject *po) { + const double z = po->layers().front()->print_z; + return z - std::floor(z / h) * h; // in [0, h) + }; + + // Reference grid: the tallest non-prism object (proxy for the object with + // the most toolchange layers). + const PrintObject *ref = nullptr; + double ref_top = -std::numeric_limits::max(); + for (const PrintObject *po : m_objects) { + if (po->config().belt_purge_tower_object.value || po->layers().empty()) + continue; + const double top = po->layers().back()->print_z; + if (top > ref_top) { + ref_top = top; + ref = po; + } + } + if (ref == nullptr) + return; + + const double ref_offset = grid_offset(ref); + bool grids_mismatch = false; + for (const PrintObject *po : m_objects) { + if (po == ref || po->config().belt_purge_tower_object.value || po->layers().empty()) + continue; + double d = std::abs(grid_offset(po) - ref_offset); + d = std::min(d, h - d); + if (d > 5. * EPSILON) { + grids_mismatch = true; + break; + } + } + + // Shift normalized to (-h/2, h/2]. + double delta = ref_offset - grid_offset(prism); + if (delta > 0.5 * h) + delta -= h; + else if (delta <= -0.5 * h) + delta += h; + + BOOST_LOG_TRIVIAL(debug) << "[BELT-DEBUG] purge prism grid snap" + << " ref=" << ref->model_object()->name + << " ref_offset=" << ref_offset + << " prism_offset=" << grid_offset(prism) + << " delta=" << delta + << " grids_mismatch=" << grids_mismatch; + + prism->belt_shift_layer_grid(delta); + + if (grids_mismatch) + this->active_step_add_warning( + PrintStateBase::WarningLevel::NON_CRITICAL, + _u8L("Objects on the plate are sliced on different layer grids; the belt purge tower can only follow " + "one of them. Filament changes on layers of the other objects may not be fully purged.")); +} + +// Belt mode replacement for _make_wipe_tower(): plan filament-change purging +// into the belt purge prism (and any other flush_into_* object) using the +// flush-into-objects machinery, without generating classic wipe tower G-code. +// The toolchange itself is emitted by GCode::set_extruder() via the +// change_filament_gcode macro; the overrides marked here make the new +// filament's first extrusions land in the purge prism. +void Print::_plan_belt_purge() +{ + m_wipe_tower_data.clear(); + + // Must run before ToolOrdering is built: LayerTools merge per-object layer + // print_z values, and the prism only absorbs purge where its (snapped) + // layers coincide with the toolchange layers. + this->_align_belt_purge_layers(); + + const unsigned int number_of_extruders = (unsigned int) m_config.filament_colour.values.size(); + + // No initial priming extrusions: there is no tower to prime on. + m_wipe_tower_data.tool_ordering = ToolOrdering(*this, (unsigned int) -1, false); + m_wipe_tower_data.tool_ordering.sort_and_build_data(*this, (unsigned int) -1, false); + + if (m_wipe_tower_data.tool_ordering.empty() || m_wipe_tower_data.tool_ordering.last_extruder() == unsigned(-1)) + throw Slic3r::SlicingError("The print is empty. The model is not printable with current print settings."); + + if (!m_wipe_tower_data.tool_ordering.has_wipe_tower()) + // No toolchanges anywhere, nothing to purge. + return; + + this->throw_if_canceled(); + + // Flush volumes per filament pair, mirroring the generic wipe tower path: + // full flush matrix for single extruder multi material with purging enabled, + // plain prime volume otherwise. + std::vector flush_matrix(cast( + get_flush_volumes_matrix(m_config.flush_volumes_matrix.values, 0, m_config.nozzle_diameter.values.size()))); + std::vector> wipe_volumes; + for (unsigned int i = 0; i < number_of_extruders; ++i) + wipe_volumes.push_back(std::vector(flush_matrix.begin() + i * number_of_extruders, + flush_matrix.begin() + (i + 1) * number_of_extruders)); + const bool use_flush_matrix = m_config.purge_in_prime_tower && m_config.single_extruder_multi_material; + const float flush_multiplier = (float) m_config.flush_multiplier.get_at(0); + + // Cancel the purge prism early: pre-scan the tool ordering for the highest + // print_z that actually has a toolchange, then drop the prism's layers above + // it so the tower stops at the last color swap (saves filament/time). This + // MUST happen before the marking loop below: ensure_perimeters_infills_order + // force-overrides the prism's extrusions on every layer (it is a dedicated + // flush object), so truncating afterwards would leave dangling overrides + // pointing into deleted layers. + { + double last_tc_z = -1.; + unsigned int cur_ext = m_wipe_tower_data.tool_ordering.first_extruder(); + for (const auto < : m_wipe_tower_data.tool_ordering.layer_tools()) + for (const unsigned int e : lt.extruders) + if (e != cur_ext) { last_tc_z = lt.print_z; cur_ext = e; } + if (last_tc_z >= 0.) + for (PrintObject *po : m_objects) + if (po->config().belt_purge_tower_object.value) { + po->belt_truncate_layers_above(last_tc_z); + break; + } + } + + // Diagnostic: the prism only absorbs purge at toolchange layers whose + // print_z coincides with one of its own layers. Compare the prism's layer + // print_z range to the toolchange print_z range and count how many + // toolchange layers actually land on a prism layer. This distinguishes a + // range/grid-alignment failure (no coverage) from a capacity shortfall + // (covered but not enough cross-section). + const PrintObject *diag_prism = nullptr; + for (const PrintObject *po : m_objects) + if (po->config().belt_purge_tower_object.value && !po->layers().empty()) { diag_prism = po; break; } + if (diag_prism != nullptr) + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge prism layer range print_z=[" + << diag_prism->layers().front()->print_z << ", " << diag_prism->layers().back()->print_z + << "] nlayers=" << diag_prism->layers().size(); + int tc_layers = 0, tc_layers_covered = 0; + + float total_leftover = 0.f; + float worst_layer_leftover = 0.f; + double worst_layer_z = 0.; + + unsigned int current_extruder_id = m_wipe_tower_data.tool_ordering.first_extruder(); + for (auto &layer_tools : m_wipe_tower_data.tool_ordering.layer_tools()) { + float layer_leftover = 0.f; + bool layer_has_tc = false; + for (const unsigned int extruder_id : layer_tools.extruders) { + if (extruder_id == current_extruder_id) + continue; + if (!layer_has_tc) { + layer_has_tc = true; + ++tc_layers; + if (diag_prism != nullptr && diag_prism->get_layer_at_printz(layer_tools.print_z, EPSILON) != nullptr) + ++tc_layers_covered; + } + float volume_to_wipe = use_flush_matrix ? + wipe_volumes[current_extruder_id][extruder_id] * flush_multiplier : + (float) m_config.prime_volume; + float leftover = layer_tools.wiping_extrusions().mark_wiping_extrusions(*this, current_extruder_id, extruder_id, + volume_to_wipe); + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] purge toolchange print_z=" << layer_tools.print_z + << " filament " << current_extruder_id << "->" << extruder_id + << " requested=" << volume_to_wipe + << " absorbed=" << volume_to_wipe - leftover + << " leftover=" << leftover; + layer_leftover += leftover; + current_extruder_id = extruder_id; + } + layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this); + if (layer_leftover > 0.f) { + total_leftover += layer_leftover; + if (layer_leftover > worst_layer_leftover) { + worst_layer_leftover = layer_leftover; + worst_layer_z = layer_tools.print_z; + } + } + this->throw_if_canceled(); + } + + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge coverage: " << tc_layers_covered << "/" << tc_layers + << " toolchange layers land on a prism layer" + << (tc_layers > 0 && tc_layers_covered == 0 ? " (RANGE/GRID MISALIGNMENT — prism absorbs nothing)" : + tc_layers_covered < tc_layers ? " (partial coverage)" : " (full coverage)"); + + if (total_leftover > 1.f) { + this->active_step_add_warning( + PrintStateBase::WarningLevel::CRITICAL, + Slic3r::format(_u8L("The belt purge tower cannot absorb the full purge volume: %1% mm³ in total could not " + "be purged (worst layer: %2% mm³ at height %3%). The print may show color bleeding. " + "Increase the belt purge tower width, or reduce flushing volumes."), + int(std::ceil(total_leftover)), int(std::ceil(worst_layer_leftover)), + Slic3r::float_to_string_decimal_point(worst_layer_z, 2))); + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge planning leftover total=" << total_leftover + << " worst_layer=" << worst_layer_leftover << " at print_z=" << worst_layer_z; + } +} + +// Belt mode: shift the sliced layer grid by delta. Mirrors the global_z_offset +// application in slice() — layer print_z and belt_floor_z_shift move together +// so belt floor clipping stays consistent with the shifted grid. Used by +// Print::_align_belt_purge_layers() to snap the purge prism onto the printed +// objects' layer grid; |delta| <= half a layer height, i.e. a sub-layer shift +// of the prism along the belt. +void PrintObject::belt_shift_layer_grid(double delta) +{ + if (std::abs(delta) < EPSILON) + return; + for (Layer *layer : m_layers) + layer->print_z += delta; + for (SupportLayer *layer : m_support_layers) + layer->print_z += delta; + m_slicing_params.belt_floor_z_shift += delta; + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] belt_shift_layer_grid" + << " obj=" << this->model_object()->name + << " delta=" << delta + << " first_layer.print_z=" << (m_layers.empty() ? 0. : m_layers.front()->print_z); +} + +// Belt mode: drop layers strictly above z (used to cancel the purge prism early +// once there are no more toolchanges above z, so the tower stops at the last +// color swap instead of wasting filament up the rest of the belt). Each layer's +// cross-section is already sliced, so removing upper layers does not affect the +// last toolchange's coverage. Deletes the Layer objects and clears the new top +// layer's upper-layer link. Returns the number of layers removed. +size_t PrintObject::belt_truncate_layers_above(coordf_t z) +{ + size_t keep = m_layers.size(); + while (keep > 0 && m_layers[keep - 1]->print_z > z + EPSILON) + --keep; + if (keep >= m_layers.size()) + return 0; + const size_t removed = m_layers.size() - keep; + for (size_t i = keep; i < m_layers.size(); ++i) + delete m_layers[i]; + m_layers.resize(keep); + if (!m_layers.empty()) + m_layers.back()->upper_layer = nullptr; + BOOST_LOG_TRIVIAL(debug) << "[BELT-DEBUG] truncate purge prism above print_z=" << z + << " kept=" << keep << " removed=" << removed + << " new_top=" << (m_layers.empty() ? 0. : m_layers.back()->print_z); + return removed; +} + +} // namespace Slic3r diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index c8796265db..9ca113e958 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -86,6 +86,7 @@ set(lisbslic3r_sources BeltGCode.hpp BeltGCodeWriter.cpp BeltGCodeWriter.hpp + BeltPurge.cpp BeltSliceStrategy.cpp BeltSliceStrategy.hpp BeltTransform.cpp diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 4004cd81de..922168ccb8 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -4126,17 +4126,6 @@ bool Print::has_wipe_tower() const return false; } -// Belt purge prism: purging after filament changes is routed into a sliced -// prism object via the flush-into-objects machinery instead of a wipe tower. -bool Print::has_belt_purge_tower() const -{ - // Its own purge-tower "type", gated by the belt-only enable_belt_purge_tower - // option (not the classic enable_prime_tower). - return m_config.belt_printer.value - && m_config.enable_belt_purge_tower.value - && !m_config.spiral_mode.value - && m_config.filament_diameter.values.size() > 1; -} const WipeTowerData &Print::wipe_tower_data(size_t filaments_cnt) const { @@ -4224,226 +4213,6 @@ bool Print::enable_timelapse_print() const return m_config.timelapse_type.value == TimelapseType::tlSmooth; } -// Belt mode: snap the purge prism's layer grid onto the printed objects' grid. -// After belt slicing every object's layer print_z carries a per-object global -// z offset (mesh-vertex-scan belt_z_shift + instance-Y-dependent terms), so -// objects at different belt-Y positions do not share a layer grid. Purge -// marking looks absorbers up with get_layer_at_printz(lt.print_z, EPSILON), -// so the prism only absorbs purge at toolchange print_z values that coincide -// with one of its own layers. Shifting the prism by at most half a layer -// (a sub-layer-height displacement along the belt) makes its grid residue -// match the reference object's; both grids step by the same layer height -// (enforced by validate()), so matching residues means exact layer matches. -void Print::_align_belt_purge_layers() -{ - PrintObject *prism = nullptr; - for (PrintObject *po : m_objects) - if (po->config().belt_purge_tower_object.value) { - prism = po; - break; - } - if (prism == nullptr || prism->layers().empty()) - return; - - const double h = prism->config().layer_height.value; - if (h <= EPSILON) - return; - - // Grid residue of an object's layer grid: identical for all of an object's - // layers above the first since they step by h. - auto grid_offset = [h](const PrintObject *po) { - const double z = po->layers().front()->print_z; - return z - std::floor(z / h) * h; // in [0, h) - }; - - // Reference grid: the tallest non-prism object (proxy for the object with - // the most toolchange layers). - const PrintObject *ref = nullptr; - double ref_top = -std::numeric_limits::max(); - for (const PrintObject *po : m_objects) { - if (po->config().belt_purge_tower_object.value || po->layers().empty()) - continue; - const double top = po->layers().back()->print_z; - if (top > ref_top) { - ref_top = top; - ref = po; - } - } - if (ref == nullptr) - return; - - const double ref_offset = grid_offset(ref); - bool grids_mismatch = false; - for (const PrintObject *po : m_objects) { - if (po == ref || po->config().belt_purge_tower_object.value || po->layers().empty()) - continue; - double d = std::abs(grid_offset(po) - ref_offset); - d = std::min(d, h - d); - if (d > 5. * EPSILON) { - grids_mismatch = true; - break; - } - } - - // Shift normalized to (-h/2, h/2]. - double delta = ref_offset - grid_offset(prism); - if (delta > 0.5 * h) - delta -= h; - else if (delta <= -0.5 * h) - delta += h; - - BOOST_LOG_TRIVIAL(debug) << "[BELT-DEBUG] purge prism grid snap" - << " ref=" << ref->model_object()->name - << " ref_offset=" << ref_offset - << " prism_offset=" << grid_offset(prism) - << " delta=" << delta - << " grids_mismatch=" << grids_mismatch; - - prism->belt_shift_layer_grid(delta); - - if (grids_mismatch) - this->active_step_add_warning( - PrintStateBase::WarningLevel::NON_CRITICAL, - _u8L("Objects on the plate are sliced on different layer grids; the belt purge tower can only follow " - "one of them. Filament changes on layers of the other objects may not be fully purged.")); -} - -// Belt mode replacement for _make_wipe_tower(): plan filament-change purging -// into the belt purge prism (and any other flush_into_* object) using the -// flush-into-objects machinery, without generating classic wipe tower G-code. -// The toolchange itself is emitted by GCode::set_extruder() via the -// change_filament_gcode macro; the overrides marked here make the new -// filament's first extrusions land in the purge prism. -void Print::_plan_belt_purge() -{ - m_wipe_tower_data.clear(); - - // Must run before ToolOrdering is built: LayerTools merge per-object layer - // print_z values, and the prism only absorbs purge where its (snapped) - // layers coincide with the toolchange layers. - this->_align_belt_purge_layers(); - - const unsigned int number_of_extruders = (unsigned int) m_config.filament_colour.values.size(); - - // No initial priming extrusions: there is no tower to prime on. - m_wipe_tower_data.tool_ordering = ToolOrdering(*this, (unsigned int) -1, false); - m_wipe_tower_data.tool_ordering.sort_and_build_data(*this, (unsigned int) -1, false); - - if (m_wipe_tower_data.tool_ordering.empty() || m_wipe_tower_data.tool_ordering.last_extruder() == unsigned(-1)) - throw Slic3r::SlicingError("The print is empty. The model is not printable with current print settings."); - - if (!m_wipe_tower_data.tool_ordering.has_wipe_tower()) - // No toolchanges anywhere, nothing to purge. - return; - - this->throw_if_canceled(); - - // Flush volumes per filament pair, mirroring the generic wipe tower path: - // full flush matrix for single extruder multi material with purging enabled, - // plain prime volume otherwise. - std::vector flush_matrix(cast( - get_flush_volumes_matrix(m_config.flush_volumes_matrix.values, 0, m_config.nozzle_diameter.values.size()))); - std::vector> wipe_volumes; - for (unsigned int i = 0; i < number_of_extruders; ++i) - wipe_volumes.push_back(std::vector(flush_matrix.begin() + i * number_of_extruders, - flush_matrix.begin() + (i + 1) * number_of_extruders)); - const bool use_flush_matrix = m_config.purge_in_prime_tower && m_config.single_extruder_multi_material; - const float flush_multiplier = (float) m_config.flush_multiplier.get_at(0); - - // Cancel the purge prism early: pre-scan the tool ordering for the highest - // print_z that actually has a toolchange, then drop the prism's layers above - // it so the tower stops at the last color swap (saves filament/time). This - // MUST happen before the marking loop below: ensure_perimeters_infills_order - // force-overrides the prism's extrusions on every layer (it is a dedicated - // flush object), so truncating afterwards would leave dangling overrides - // pointing into deleted layers. - { - double last_tc_z = -1.; - unsigned int cur_ext = m_wipe_tower_data.tool_ordering.first_extruder(); - for (const auto < : m_wipe_tower_data.tool_ordering.layer_tools()) - for (const unsigned int e : lt.extruders) - if (e != cur_ext) { last_tc_z = lt.print_z; cur_ext = e; } - if (last_tc_z >= 0.) - for (PrintObject *po : m_objects) - if (po->config().belt_purge_tower_object.value) { - po->belt_truncate_layers_above(last_tc_z); - break; - } - } - - // Diagnostic: the prism only absorbs purge at toolchange layers whose - // print_z coincides with one of its own layers. Compare the prism's layer - // print_z range to the toolchange print_z range and count how many - // toolchange layers actually land on a prism layer. This distinguishes a - // range/grid-alignment failure (no coverage) from a capacity shortfall - // (covered but not enough cross-section). - const PrintObject *diag_prism = nullptr; - for (const PrintObject *po : m_objects) - if (po->config().belt_purge_tower_object.value && !po->layers().empty()) { diag_prism = po; break; } - if (diag_prism != nullptr) - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge prism layer range print_z=[" - << diag_prism->layers().front()->print_z << ", " << diag_prism->layers().back()->print_z - << "] nlayers=" << diag_prism->layers().size(); - int tc_layers = 0, tc_layers_covered = 0; - - float total_leftover = 0.f; - float worst_layer_leftover = 0.f; - double worst_layer_z = 0.; - - unsigned int current_extruder_id = m_wipe_tower_data.tool_ordering.first_extruder(); - for (auto &layer_tools : m_wipe_tower_data.tool_ordering.layer_tools()) { - float layer_leftover = 0.f; - bool layer_has_tc = false; - for (const unsigned int extruder_id : layer_tools.extruders) { - if (extruder_id == current_extruder_id) - continue; - if (!layer_has_tc) { - layer_has_tc = true; - ++tc_layers; - if (diag_prism != nullptr && diag_prism->get_layer_at_printz(layer_tools.print_z, EPSILON) != nullptr) - ++tc_layers_covered; - } - float volume_to_wipe = use_flush_matrix ? - wipe_volumes[current_extruder_id][extruder_id] * flush_multiplier : - (float) m_config.prime_volume; - float leftover = layer_tools.wiping_extrusions().mark_wiping_extrusions(*this, current_extruder_id, extruder_id, - volume_to_wipe); - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] purge toolchange print_z=" << layer_tools.print_z - << " filament " << current_extruder_id << "->" << extruder_id - << " requested=" << volume_to_wipe - << " absorbed=" << volume_to_wipe - leftover - << " leftover=" << leftover; - layer_leftover += leftover; - current_extruder_id = extruder_id; - } - layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this); - if (layer_leftover > 0.f) { - total_leftover += layer_leftover; - if (layer_leftover > worst_layer_leftover) { - worst_layer_leftover = layer_leftover; - worst_layer_z = layer_tools.print_z; - } - } - this->throw_if_canceled(); - } - - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge coverage: " << tc_layers_covered << "/" << tc_layers - << " toolchange layers land on a prism layer" - << (tc_layers > 0 && tc_layers_covered == 0 ? " (RANGE/GRID MISALIGNMENT — prism absorbs nothing)" : - tc_layers_covered < tc_layers ? " (partial coverage)" : " (full coverage)"); - - if (total_leftover > 1.f) { - this->active_step_add_warning( - PrintStateBase::WarningLevel::CRITICAL, - Slic3r::format(_u8L("The belt purge tower cannot absorb the full purge volume: %1% mm³ in total could not " - "be purged (worst layer: %2% mm³ at height %3%). The print may show color bleeding. " - "Increase the belt purge tower width, or reduce flushing volumes."), - int(std::ceil(total_leftover)), int(std::ceil(worst_layer_leftover)), - Slic3r::float_to_string_decimal_point(worst_layer_z, 2))); - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge planning leftover total=" << total_leftover - << " worst_layer=" << worst_layer_leftover << " at print_z=" << worst_layer_z; - } -} void Print::_make_wipe_tower() { diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index f13b208a3a..b65c79d0eb 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -1392,51 +1392,6 @@ void apply_fuzzy_skin_segmentation(PrintObject &print_object, ThrowOnCancel thro }); // end of parallel_for } -// Belt mode: shift the sliced layer grid by delta. Mirrors the global_z_offset -// application in slice() — layer print_z and belt_floor_z_shift move together -// so belt floor clipping stays consistent with the shifted grid. Used by -// Print::_align_belt_purge_layers() to snap the purge prism onto the printed -// objects' layer grid; |delta| <= half a layer height, i.e. a sub-layer shift -// of the prism along the belt. -void PrintObject::belt_shift_layer_grid(double delta) -{ - if (std::abs(delta) < EPSILON) - return; - for (Layer *layer : m_layers) - layer->print_z += delta; - for (SupportLayer *layer : m_support_layers) - layer->print_z += delta; - m_slicing_params.belt_floor_z_shift += delta; - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] belt_shift_layer_grid" - << " obj=" << this->model_object()->name - << " delta=" << delta - << " first_layer.print_z=" << (m_layers.empty() ? 0. : m_layers.front()->print_z); -} - -// Belt mode: drop layers strictly above z (used to cancel the purge prism early -// once there are no more toolchanges above z, so the tower stops at the last -// color swap instead of wasting filament up the rest of the belt). Each layer's -// cross-section is already sliced, so removing upper layers does not affect the -// last toolchange's coverage. Deletes the Layer objects and clears the new top -// layer's upper-layer link. Returns the number of layers removed. -size_t PrintObject::belt_truncate_layers_above(coordf_t z) -{ - size_t keep = m_layers.size(); - while (keep > 0 && m_layers[keep - 1]->print_z > z + EPSILON) - --keep; - if (keep >= m_layers.size()) - return 0; - const size_t removed = m_layers.size() - keep; - for (size_t i = keep; i < m_layers.size(); ++i) - delete m_layers[i]; - m_layers.resize(keep); - if (!m_layers.empty()) - m_layers.back()->upper_layer = nullptr; - BOOST_LOG_TRIVIAL(debug) << "[BELT-DEBUG] truncate purge prism above print_z=" << z - << " kept=" << keep << " removed=" << removed - << " new_top=" << (m_layers.empty() ? 0. : m_layers.back()->print_z); - return removed; -} // 1) Decides Z positions of the layers, // 2) Initializes layers and their regions From 3d270c2aa7eae0e7d7438f077985d4810c2d0d7e Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 17 Jun 2026 20:59:33 -0500 Subject: [PATCH 099/224] workable belt purge, via N-1 individual "purge objects" --- src/libslic3r/BeltPurge.cpp | 114 +++++++++++++++++++----------- src/slic3r/GUI/BeltPurgeTower.cpp | 62 ++++++++++++++-- 2 files changed, 126 insertions(+), 50 deletions(-) diff --git a/src/libslic3r/BeltPurge.cpp b/src/libslic3r/BeltPurge.cpp index a9c2ffceb8..c69987c719 100644 --- a/src/libslic3r/BeltPurge.cpp +++ b/src/libslic3r/BeltPurge.cpp @@ -21,6 +21,8 @@ #include "Exception.hpp" #include "GCode/ToolOrdering.hpp" #include "Layer.hpp" +#include "ExtrusionEntity.hpp" +#include "ExtrusionEntityCollection.hpp" #include "I18N.hpp" #include "format.hpp" #include "LocalesUtils.hpp" @@ -46,16 +48,28 @@ bool Print::has_belt_purge_tower() const && m_config.filament_diameter.values.size() > 1; } -// Belt mode: snap the purge prism's layer grid onto the printed objects' grid. -// After belt slicing every object's layer print_z carries a per-object global -// z offset (mesh-vertex-scan belt_z_shift + instance-Y-dependent terms), so -// objects at different belt-Y positions do not share a layer grid. Purge -// marking looks absorbers up with get_layer_at_printz(lt.print_z, EPSILON), -// so the prism only absorbs purge at toolchange print_z values that coincide -// with one of its own layers. Shifting the prism by at most half a layer -// (a sub-layer-height displacement along the belt) makes its grid residue -// match the reference object's; both grids step by the same layer height -// (enforced by validate()), so matching residues means exact layer matches. +// Belt mode: align ALL objects on the plate (the printed objects AND the purge +// prism) onto one common layer grid, so the prism can absorb every toolchange. +// +// After belt slicing each object's layer print_z carries a per-object global z +// offset (mesh-vertex-scan belt_z_shift + instance-Y-dependent terms), so +// objects at different belt-Y positions get layer grids with DIFFERENT residues +// (mod layer height). Purge marking looks absorbers up with +// get_layer_at_printz(lt.print_z, EPSILON), so a toolchange on object B only +// absorbs into the prism if the prism has a layer at B's print_z. Snapping only +// the prism to one object therefore worked for a single (assembled) multi-color +// object but failed with multiple separate objects — the prism could follow only +// one grid, and toolchanges on the others went unabsorbed ("multiple layer +// grids" warning). +// +// Fix: pick one reference grid (the tallest object) and shift every object onto +// it. Each shift is at most half a layer height — a sub-100µm move along the +// belt, the very same mechanism the per-object global_z_offset already uses, and +// it keeps each object internally consistent (belt_shift_layer_grid moves the +// object's layers, its support layers, and its belt floor together). Equal layer +// height across objects is enforced by Print::validate(), so once residues match +// every object steps on the same lattice {ref_offset + k*h} and every toolchange +// layer coincides with a prism layer. void Print::_align_belt_purge_layers() { PrintObject *prism = nullptr; @@ -73,13 +87,15 @@ void Print::_align_belt_purge_layers() // Grid residue of an object's layer grid: identical for all of an object's // layers above the first since they step by h. - auto grid_offset = [h](const PrintObject *po) { + auto grid_offset = [h](const PrintObject *po) -> double { + if (po->layers().empty()) + return 0.; const double z = po->layers().front()->print_z; return z - std::floor(z / h) * h; // in [0, h) }; // Reference grid: the tallest non-prism object (proxy for the object with - // the most toolchange layers). + // the most toolchange layers — minimizes how far the rest must move). const PrintObject *ref = nullptr; double ref_top = -std::numeric_limits::max(); for (const PrintObject *po : m_objects) { @@ -94,40 +110,23 @@ void Print::_align_belt_purge_layers() if (ref == nullptr) return; - const double ref_offset = grid_offset(ref); - bool grids_mismatch = false; - for (const PrintObject *po : m_objects) { - if (po == ref || po->config().belt_purge_tower_object.value || po->layers().empty()) + const double ref_offset = grid_offset(ref); + + // Snap every object (printed objects AND the prism) onto the reference grid. + for (PrintObject *po : m_objects) { + if (po->layers().empty()) continue; - double d = std::abs(grid_offset(po) - ref_offset); - d = std::min(d, h - d); - if (d > 5. * EPSILON) { - grids_mismatch = true; - break; - } + double delta = ref_offset - grid_offset(po); + if (delta > 0.5 * h) + delta -= h; + else if (delta <= -0.5 * h) + delta += h; + po->belt_shift_layer_grid(delta); // no-op for the reference object (delta ~ 0) } - // Shift normalized to (-h/2, h/2]. - double delta = ref_offset - grid_offset(prism); - if (delta > 0.5 * h) - delta -= h; - else if (delta <= -0.5 * h) - delta += h; - - BOOST_LOG_TRIVIAL(debug) << "[BELT-DEBUG] purge prism grid snap" - << " ref=" << ref->model_object()->name - << " ref_offset=" << ref_offset - << " prism_offset=" << grid_offset(prism) - << " delta=" << delta - << " grids_mismatch=" << grids_mismatch; - - prism->belt_shift_layer_grid(delta); - - if (grids_mismatch) - this->active_step_add_warning( - PrintStateBase::WarningLevel::NON_CRITICAL, - _u8L("Objects on the plate are sliced on different layer grids; the belt purge tower can only follow " - "one of them. Filament changes on layers of the other objects may not be fully purged.")); + BOOST_LOG_TRIVIAL(debug) << "[BELT-DEBUG] purge grid align: snapped " << m_objects.size() + << " objects onto ref grid offset=" << ref_offset + << " (ref=" << ref->model_object()->name << ")"; } // Belt mode replacement for _make_wipe_tower(): plan filament-change purging @@ -228,8 +227,37 @@ void Print::_plan_belt_purge() float volume_to_wipe = use_flush_matrix ? wipe_volumes[current_extruder_id][extruder_id] * flush_multiplier : (float) m_config.prime_volume; + // DIAGNOSTIC (first dozen toolchanges, warning level): how much + // extrudable volume does the prism actually have at this layer, and + // how much of the requested purge did it absorb? Distinguishes "prism + // layer found but tiny/empty" from "found with geometry but marking + // didn't consume it". + const int swap_idx_on_layer = [&]{ int n = 0; for (unsigned int e : layer_tools.extruders) { if (e == extruder_id) break; ++n; } return n; }(); + double prism_vol = -1.; + { + if (const Layer *pl = diag_prism != nullptr ? diag_prism->get_layer_at_printz(layer_tools.print_z, EPSILON) : nullptr) { + prism_vol = 0.; + for (const LayerRegion *lr : pl->regions()) { + for (const ExtrusionEntity *ee : lr->fills.entities) prism_vol += ee->total_volume(); + for (const ExtrusionEntity *ee : lr->perimeters.entities) prism_vol += ee->total_volume(); + } + } + } float leftover = layer_tools.wiping_extrusions().mark_wiping_extrusions(*this, current_extruder_id, extruder_id, volume_to_wipe); + // DIAGNOSTIC: log the toolchanges that FAIL to fully absorb (warning, + // capped) — these are the ones causing the leftover. + static thread_local int dbg_fail = 0; + if (leftover > 0.01f && dbg_fail < 20) { + ++dbg_fail; + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge UNABSORBED print_z=" << layer_tools.print_z + << " filament " << current_extruder_id << "->" << extruder_id + << " swap#" << swap_idx_on_layer << " of " << layer_tools.extruders.size() << " extruders" + << " requested=" << volume_to_wipe + << " absorbed=" << volume_to_wipe - leftover + << " leftover=" << leftover + << " prism_layer_extrudable_vol=" << prism_vol; + } BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] purge toolchange print_z=" << layer_tools.print_z << " filament " << current_extruder_id << "->" << extruder_id << " requested=" << volume_to_wipe diff --git a/src/slic3r/GUI/BeltPurgeTower.cpp b/src/slic3r/GUI/BeltPurgeTower.cpp index 47ff6b6bd4..3be4fee3ff 100644 --- a/src/slic3r/GUI/BeltPurgeTower.cpp +++ b/src/slic3r/GUI/BeltPurgeTower.cpp @@ -55,9 +55,34 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object } }; - const auto &printer_config = wxGetApp().preset_bundle->printers.get_edited_preset().config; - const auto &print_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; - auto &project_config = wxGetApp().preset_bundle->project_config; + // Cheap early-out for non-belt printers: only belt printers ever get a belt + // purge tower, and this runs on every background-process tick — so avoid the + // full_config() merge below unless this is actually a belt printer. (Also + // tidies up a stale prism if the user switched away from a belt printer.) + { + const auto *belt_pre = wxGetApp().preset_bundle->printers.get_edited_preset().config.option("belt_printer"); + if (belt_pre == nullptr || !belt_pre->value) { + sig = BeltPurgeSignature{}; + if (prism_idxs.empty()) + return false; + remove_prisms(prism_idxs); + return true; + } + } + + // Read every sizing input from the MERGED full config — the exact same + // config the backend slices with (BackgroundSlicingProcess::apply uses + // preset_bundle->full_config()). Reading from the individual presets caused + // GUI/backend mismatches: e.g. purge_in_prime_tower or a populated + // flush_volumes_matrix present in the merged config but false/empty in the + // preset the GUI happened to read, so the tower was sized for the small + // prime_volume instead of the real color-change flush and could not absorb + // it. The three names below alias the one merged config so the rest of the + // function is unchanged. + const DynamicPrintConfig full_cfg = wxGetApp().preset_bundle->full_config(); + const DynamicPrintConfig &printer_config = full_cfg; + const DynamicPrintConfig &print_config = full_cfg; + const DynamicPrintConfig &project_config = full_cfg; const auto *belt_opt = printer_config.option("belt_printer"); const bool belt = belt_opt != nullptr && belt_opt->value; @@ -238,21 +263,44 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object << " -> " << bed_ext_dbg.max.x() << "," << bed_ext_dbg.max.y() << "]" << " lat_center=" << lat_center << " belt=[" << belt_start << "," << belt_end << "]"; - // Orient the bar: long axis = belt travel, width = lateral, height = Z. - const double size_x = belt_is_y ? width : length; - const double size_y = belt_is_y ? length : width; const Vec3d desired_center(belt_is_y ? lat_center : belt_center, belt_is_y ? belt_center : lat_center, 0.5 * height); + // Build the prism as N DISCONNECTED sub-bars side by side across the belt, + // N = (filaments - 1) = the worst-case number of toolchanges on one layer. + // Why: mark_wiping_extrusions overrides whole extrusion-entity COLLECTIONS, + // and each disconnected island slices into its own infill collection. With a + // single solid box there is one collection per layer, so the FIRST toolchange + // on a layer grabs the entire collection (consuming all of it for one swap) + // and any further swaps on that layer find nothing left and go unabsorbed + // (the classic wipe tower hides this by spilling leftover into the real + // tower; the belt prism has no fallback). One island per simultaneous swap + // lets each swap claim its own island. Total lateral footprint stays `width` + // (each island width/N wide, separated by a small gap), so per-layer capacity + // per island ~= max_flush, matching the height sizing. + const int n_islands = std::max(1, (int) filaments.size() - 1); + const double gap = 2.0; + const double w_sub = std::max(1.0, (width - (n_islands - 1) * gap) / n_islands); + // --- (Re)create --------------------------------------------------------- if (!prism_idxs.empty()) remove_prisms(prism_idxs); + TriangleMesh prism_mesh; + for (int i = 0; i < n_islands; ++i) { + const double lat_off = i * (w_sub + gap); + // Box dims: lateral = w_sub, along-belt = length, vertical = height. + TriangleMesh box = belt_is_y ? make_cube(w_sub, length, height) // X = lateral, Y = belt + : make_cube(length, w_sub, height); // X = belt, Y = lateral + box.translate(belt_is_y ? Vec3f((float) lat_off, 0.f, 0.f) : Vec3f(0.f, (float) lat_off, 0.f)); + prism_mesh.merge(box); + } + ModelObject *new_object = model.add_object(); new_object->name = _u8L("Belt Purge Tower"); new_object->add_instance(); - ModelVolume *new_volume = new_object->add_volume(make_cube(size_x, size_y, height)); + ModelVolume *new_volume = new_object->add_volume(std::move(prism_mesh)); new_volume->name = new_object->name; auto &cfg = new_object->config; From ee3e014f02249cb3244fb1dae21d2ac915c97349 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Thu, 18 Jun 2026 09:35:30 -0500 Subject: [PATCH 100/224] allow belt purge to skip unnecessary purge volume --- src/libslic3r/BeltPurge.cpp | 63 ++++++++++++++-------------- src/libslic3r/GCode/ToolOrdering.hpp | 14 ++++--- src/slic3r/GUI/BeltPurgeTower.cpp | 8 +++- 3 files changed, 46 insertions(+), 39 deletions(-) diff --git a/src/libslic3r/BeltPurge.cpp b/src/libslic3r/BeltPurge.cpp index c69987c719..c51c5511c6 100644 --- a/src/libslic3r/BeltPurge.cpp +++ b/src/libslic3r/BeltPurge.cpp @@ -198,9 +198,10 @@ void Print::_plan_belt_purge() // toolchange layers actually land on a prism layer. This distinguishes a // range/grid-alignment failure (no coverage) from a capacity shortfall // (covered but not enough cross-section). - const PrintObject *diag_prism = nullptr; - for (const PrintObject *po : m_objects) - if (po->config().belt_purge_tower_object.value && !po->layers().empty()) { diag_prism = po; break; } + PrintObject *prism_po = nullptr; + for (PrintObject *po : m_objects) + if (po->config().belt_purge_tower_object.value && !po->layers().empty()) { prism_po = po; break; } + const PrintObject *diag_prism = prism_po; if (diag_prism != nullptr) BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge prism layer range print_z=[" << diag_prism->layers().front()->print_z << ", " << diag_prism->layers().back()->print_z @@ -227,37 +228,8 @@ void Print::_plan_belt_purge() float volume_to_wipe = use_flush_matrix ? wipe_volumes[current_extruder_id][extruder_id] * flush_multiplier : (float) m_config.prime_volume; - // DIAGNOSTIC (first dozen toolchanges, warning level): how much - // extrudable volume does the prism actually have at this layer, and - // how much of the requested purge did it absorb? Distinguishes "prism - // layer found but tiny/empty" from "found with geometry but marking - // didn't consume it". - const int swap_idx_on_layer = [&]{ int n = 0; for (unsigned int e : layer_tools.extruders) { if (e == extruder_id) break; ++n; } return n; }(); - double prism_vol = -1.; - { - if (const Layer *pl = diag_prism != nullptr ? diag_prism->get_layer_at_printz(layer_tools.print_z, EPSILON) : nullptr) { - prism_vol = 0.; - for (const LayerRegion *lr : pl->regions()) { - for (const ExtrusionEntity *ee : lr->fills.entities) prism_vol += ee->total_volume(); - for (const ExtrusionEntity *ee : lr->perimeters.entities) prism_vol += ee->total_volume(); - } - } - } float leftover = layer_tools.wiping_extrusions().mark_wiping_extrusions(*this, current_extruder_id, extruder_id, volume_to_wipe); - // DIAGNOSTIC: log the toolchanges that FAIL to fully absorb (warning, - // capped) — these are the ones causing the leftover. - static thread_local int dbg_fail = 0; - if (leftover > 0.01f && dbg_fail < 20) { - ++dbg_fail; - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge UNABSORBED print_z=" << layer_tools.print_z - << " filament " << current_extruder_id << "->" << extruder_id - << " swap#" << swap_idx_on_layer << " of " << layer_tools.extruders.size() << " extruders" - << " requested=" << volume_to_wipe - << " absorbed=" << volume_to_wipe - leftover - << " leftover=" << leftover - << " prism_layer_extrudable_vol=" << prism_vol; - } BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] purge toolchange print_z=" << layer_tools.print_z << " filament " << current_extruder_id << "->" << extruder_id << " requested=" << volume_to_wipe @@ -266,6 +238,33 @@ void Print::_plan_belt_purge() layer_leftover += leftover; current_extruder_id = extruder_id; } + + // Plastic saving: drop the prism's infill that no toolchange on this + // layer claimed. At this point (after the real-purge marking, before the + // force-override pass below) the prism's OVERRIDDEN fills are exactly the + // purge; the rest is infill that would otherwise print as the prism's own + // filament for nothing — which is most of the prism on layers with little + // or no purge. We keep perimeters so the bar's wall shell stays + // continuous along the belt. (Must run before ensure_perimeters_infills_ + // order, which force-overrides every remaining fill and would make them + // all look "claimed".) + if (prism_po != nullptr) { + if (Layer *pl = prism_po->get_layer_at_printz(layer_tools.print_z, EPSILON)) { + const auto &we = layer_tools.wiping_extrusions(); + for (LayerRegion *lr : pl->regions()) { + auto &ents = lr->fills.entities; + size_t keep = 0; + for (size_t r = 0; r < ents.size(); ++r) { + if (we.is_entity_overridden(ents[r], prism_po, 0)) + ents[keep++] = ents[r]; + else + delete ents[r]; + } + ents.resize(keep); + } + } + } + layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this); if (layer_leftover > 0.f) { total_leftover += layer_leftover; diff --git a/src/libslic3r/GCode/ToolOrdering.hpp b/src/libslic3r/GCode/ToolOrdering.hpp index 0d7526c72e..f2071e75b7 100644 --- a/src/libslic3r/GCode/ToolOrdering.hpp +++ b/src/libslic3r/GCode/ToolOrdering.hpp @@ -45,6 +45,14 @@ public: void ensure_perimeters_infills_order(const Print& print); + // Returns true if entity is not printed with its usual extruder for a given copy + // (i.e. it was claimed as a wiping/purge extrusion). Used by the belt purge + // tower to tell which prism fills carry purge vs. which are unclaimed waste. + bool is_entity_overridden(const ExtrusionEntity* entity, const PrintObject *object, size_t copy_id) const { + auto it = entity_map.find(std::make_tuple(entity, object)); + return it == entity_map.end() ? false : it->second[copy_id] != -1; + } + bool is_overriddable(const ExtrusionEntityCollection& ee, const PrintConfig& print_config, const PrintObject& object, const PrintRegion& region) const; bool is_overriddable_and_mark(const ExtrusionEntityCollection& ee, const PrintConfig& print_config, const PrintObject& object, const PrintRegion& region) { bool out = this->is_overriddable(ee, print_config, object, region); @@ -80,12 +88,6 @@ private: void set_support_extruder_override(const PrintObject* object, size_t copy_id, int extruder, size_t num_of_copies); void set_support_interface_extruder_override(const PrintObject* object, size_t copy_id, int extruder, size_t num_of_copies); - // Returns true in case that entity is not printed with its usual extruder for a given copy: - bool is_entity_overridden(const ExtrusionEntity* entity, const PrintObject *object, size_t copy_id) const { - auto it = entity_map.find(std::make_tuple(entity, object)); - return it == entity_map.end() ? false : it->second[copy_id] != -1; - } - std::map, ExtruderPerCopy> entity_map; // to keep track of who prints what // BBS std::map support_map; diff --git a/src/slic3r/GUI/BeltPurgeTower.cpp b/src/slic3r/GUI/BeltPurgeTower.cpp index 3be4fee3ff..f04f311d6b 100644 --- a/src/slic3r/GUI/BeltPurgeTower.cpp +++ b/src/slic3r/GUI/BeltPurgeTower.cpp @@ -280,7 +280,13 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object // (each island width/N wide, separated by a small gap), so per-layer capacity // per island ~= max_flush, matching the height sizing. const int n_islands = std::max(1, (int) filaments.size() - 1); - const double gap = 2.0; + // Minimal gap between sub-bars: they must stay just-separated so the slicer + // keeps them as distinct islands (hence distinct infill collections, one per + // simultaneous swap). Zero gap would union them into one collection and + // reintroduce the multi-swap-per-layer absorption bug; a hair over ~2 line + // widths also keeps gap-fill from bridging them. 1 mm is about as close as + // they can butt up while staying individually purgeable. + const double gap = 1.0; const double w_sub = std::max(1.0, (width - (n_islands - 1) * gap) / n_islands); // --- (Re)create --------------------------------------------------------- From 99ee8893cd4dea6c313be5c6f78f940717163072 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 8 Aug 2026 17:14:26 -0500 Subject: [PATCH 101/224] Fix belt purge tower activation and placement safety --- result.json | 10 -------- src/libslic3r/BeltPurge.cpp | 13 +++++++---- src/libslic3r/GCode/ToolOrdering.hpp | 2 +- src/libslic3r/Print.cpp | 4 ++-- src/slic3r/GUI/BeltPurgeTower.cpp | 16 +++++++++---- src/slic3r/GUI/BeltPurgeTower.hpp | 9 +++---- tests/fff_print/test_print.cpp | 35 ++++++++++++++++++++++++++++ 7 files changed, 62 insertions(+), 27 deletions(-) delete mode 100644 result.json diff --git a/result.json b/result.json deleted file mode 100644 index 517f6bffa5..0000000000 --- a/result.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "error_string": "Success.", - "export_time": 0, - "layer_height": 0.0, - "plate_index": 0, - "prepare_time": 0, - "return_code": 0, - "sparse_infill_density": 0.0, - "wall_loops": 0 -} diff --git a/src/libslic3r/BeltPurge.cpp b/src/libslic3r/BeltPurge.cpp index c51c5511c6..3e261a1882 100644 --- a/src/libslic3r/BeltPurge.cpp +++ b/src/libslic3r/BeltPurge.cpp @@ -42,10 +42,15 @@ bool Print::has_belt_purge_tower() const { // Its own purge-tower "type", gated by the belt-only enable_belt_purge_tower // option (not the classic enable_prime_tower). - return m_config.belt_printer.value - && m_config.enable_belt_purge_tower.value - && !m_config.spiral_mode.value - && m_config.filament_diameter.values.size() > 1; + if (!(m_config.belt_printer.value + && m_config.enable_belt_purge_tower.value + && !m_config.spiral_mode.value + && m_config.filament_diameter.values.size() > 1)) + return false; + + return std::any_of(m_objects.begin(), m_objects.end(), [](const PrintObject *object) { + return object->config().belt_purge_tower_object.value; + }); } // Belt mode: align ALL objects on the plate (the printed objects AND the purge diff --git a/src/libslic3r/GCode/ToolOrdering.hpp b/src/libslic3r/GCode/ToolOrdering.hpp index f2071e75b7..52778690bb 100644 --- a/src/libslic3r/GCode/ToolOrdering.hpp +++ b/src/libslic3r/GCode/ToolOrdering.hpp @@ -50,7 +50,7 @@ public: // tower to tell which prism fills carry purge vs. which are unclaimed waste. bool is_entity_overridden(const ExtrusionEntity* entity, const PrintObject *object, size_t copy_id) const { auto it = entity_map.find(std::make_tuple(entity, object)); - return it == entity_map.end() ? false : it->second[copy_id] != -1; + return it != entity_map.end() && copy_id < it->second.size() && it->second[copy_id] != -1; } bool is_overriddable(const ExtrusionEntityCollection& ee, const PrintConfig& print_config, const PrintObject& object, const PrintRegion& region) const; diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 922168ccb8..7cec3de5eb 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1477,13 +1477,13 @@ StringObjectException Print::validate(std::vector *warnin if (m_config.belt_printer.value && m_config.enable_belt_purge_tower.value && m_config.print_sequence == PrintSequence::ByObject - && extruders.size() > 1 && warning != nullptr) { + && extruders.size() > 1) { StringObjectException warningtemp; warningtemp.string = L("The belt purge tower is not generated in \"By object\" print sequence; " "filament changes will not be purged."); warningtemp.opt_key = "enable_belt_purge_tower"; warningtemp.is_warning = true; - *warning = warningtemp; + add_warning(warningtemp); } if (m_config.enable_prime_tower) { diff --git a/src/slic3r/GUI/BeltPurgeTower.cpp b/src/slic3r/GUI/BeltPurgeTower.cpp index f04f311d6b..cf1489d6a0 100644 --- a/src/slic3r/GUI/BeltPurgeTower.cpp +++ b/src/slic3r/GUI/BeltPurgeTower.cpp @@ -142,7 +142,14 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object } // --- Sizing ----------------------------------------------------------- - const double width = print_config.has("belt_purge_tower_width") ? std::max(1., print_config.opt_float("belt_purge_tower_width")) : 35.; + const int n_islands = std::max(1, (int) filaments.size() - 1); + const double gap = 1.0; + // Every disconnected island needs at least 1 mm of printable width. Honor + // the configured total width whenever possible, but never let the island + // layout silently grow past the footprint used for placement. + const double min_width = n_islands + (n_islands - 1) * gap; + const double width = std::max(min_width, + print_config.has("belt_purge_tower_width") ? print_config.opt_float("belt_purge_tower_width") : 35.); const double layer_h = print_config.has("layer_height") ? print_config.opt_float("layer_height") : 0.2; // Belt geometry. The rotation axis is the gantry tilt axis; the belt @@ -219,6 +226,9 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object new_sig.key[4] = q(belt_max); new_sig.key[5] = q(lat_min); new_sig.key[6] = q(z_max); + new_sig.key[7] = static_cast(rot); + new_sig.key[8] = std::lround(theta * 10000.0); + new_sig.key[9] = q(lat_max); if (prism_idxs.size() == 1 && new_sig == sig) return false; // already up to date — do not touch the model @@ -279,15 +289,13 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object // lets each swap claim its own island. Total lateral footprint stays `width` // (each island width/N wide, separated by a small gap), so per-layer capacity // per island ~= max_flush, matching the height sizing. - const int n_islands = std::max(1, (int) filaments.size() - 1); // Minimal gap between sub-bars: they must stay just-separated so the slicer // keeps them as distinct islands (hence distinct infill collections, one per // simultaneous swap). Zero gap would union them into one collection and // reintroduce the multi-swap-per-layer absorption bug; a hair over ~2 line // widths also keeps gap-fill from bridging them. 1 mm is about as close as // they can butt up while staying individually purgeable. - const double gap = 1.0; - const double w_sub = std::max(1.0, (width - (n_islands - 1) * gap) / n_islands); + const double w_sub = (width - (n_islands - 1) * gap) / n_islands; // --- (Re)create --------------------------------------------------------- if (!prism_idxs.empty()) diff --git a/src/slic3r/GUI/BeltPurgeTower.hpp b/src/slic3r/GUI/BeltPurgeTower.hpp index b1735303c5..5e5b614834 100644 --- a/src/slic3r/GUI/BeltPurgeTower.hpp +++ b/src/slic3r/GUI/BeltPurgeTower.hpp @@ -1,5 +1,4 @@ -#ifndef slic3r_GUI_BeltPurgeTower_hpp_ -#define slic3r_GUI_BeltPurgeTower_hpp_ +#pragma once // ORCA-Belt: auto-managed purge prism for belt printers. // @@ -23,12 +22,12 @@ struct BeltPurgeSignature { bool valid = false; int filament_count = 0; - long key[7] = {0}; // rounded geometry inputs (0.1 mm units) + long key[10] = {0}; // rounded geometry inputs (0.1 mm units) bool operator==(const BeltPurgeSignature &o) const { if (valid != o.valid || filament_count != o.filament_count) return false; - for (int i = 0; i < 7; ++i) + for (int i = 0; i < 10; ++i) if (key[i] != o.key[i]) return false; return true; @@ -44,5 +43,3 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object } // namespace GUI } // namespace Slic3r - -#endif // slic3r_GUI_BeltPurgeTower_hpp_ diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 9cb085f78b..ba3432b4ad 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -300,6 +300,41 @@ TEST_CASE("Print::validate tolerates a null warnings pointer", "[Print][validate CHECK(err.string.empty()); } +TEST_CASE("Purge tower selection keeps ordinary printers on the classic path", "[Print][PurgeTower][Regression]") +{ + DynamicPrintConfig config = multifilament_config(2, { + { "belt_printer", 0 }, + { "enable_prime_tower", 1 }, + { "enable_belt_purge_tower", 1 } + }); + config.set_key_value("timelapse_type", new ConfigOptionEnum(TimelapseType::tlSmooth)); + + Model model; + Print print; + build_cubes(model, print, config, /*n=*/1, /*overlap=*/false); + + CHECK(print.has_wipe_tower()); + CHECK_FALSE(print.has_belt_purge_tower()); +} + +TEST_CASE("Belt purge planning requires its managed purge object", "[Print][PurgeTower][Regression]") +{ + DynamicPrintConfig config = multifilament_config(2, { + { "belt_printer", 1 }, + { "enable_belt_purge_tower", 1 } + }); + + Model model; + Print print; + build_cubes(model, print, config, /*n=*/1, /*overlap=*/false); + CHECK_FALSE(print.has_belt_purge_tower()); + + model.objects.front()->config.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true)); + print.apply(model, config); + CHECK(print.has_belt_purge_tower()); + CHECK_FALSE(print.has_wipe_tower()); +} + TEST_CASE("A default slice emits perimeter, infill, and skirt", "[Print]") { const std::string gcode = slice({ cube(20) }, { From 3d11b60e712a0e7f748b48fbe7c5cd59bd78b741 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 8 Aug 2026 20:14:52 -0500 Subject: [PATCH 102/224] Harden belt purge tower replanning --- src/libslic3r/BeltPurge.cpp | 58 +++++++++++++-------------- src/libslic3r/GCode/ToolOrdering.hpp | 14 +++---- src/libslic3r/Print.cpp | 8 ++++ src/libslic3r/Print.hpp | 5 +++ src/libslic3r/PrintObject.cpp | 3 ++ src/slic3r/GUI/BeltPurgeTower.cpp | 16 +++++--- src/slic3r/GUI/BeltPurgeTower.hpp | 4 +- src/slic3r/GUI/ConfigManipulation.cpp | 2 +- tests/fff_print/test_print.cpp | 17 ++++++++ 9 files changed, 80 insertions(+), 47 deletions(-) diff --git a/src/libslic3r/BeltPurge.cpp b/src/libslic3r/BeltPurge.cpp index 3e261a1882..b249f21bd2 100644 --- a/src/libslic3r/BeltPurge.cpp +++ b/src/libslic3r/BeltPurge.cpp @@ -79,7 +79,7 @@ void Print::_align_belt_purge_layers() { PrintObject *prism = nullptr; for (PrintObject *po : m_objects) - if (po->config().belt_purge_tower_object.value) { + if (po->config().belt_purge_tower_object.value && !po->layers().empty()) { prism = po; break; } @@ -144,6 +144,13 @@ void Print::_plan_belt_purge() { m_wipe_tower_data.clear(); + // psWipeTower may be invalidated without posSlice (for example after a + // filament-map or tool-ordering change). Restore a prism shortened by the + // previous plan so a newly higher toolchange can use its original layers. + for (PrintObject *po : m_objects) + if (po->config().belt_purge_tower_object.value) + po->belt_restore_truncated_layers(); + // Must run before ToolOrdering is built: LayerTools merge per-object layer // print_z values, and the prism only absorbs purge where its (snapped) // layers coincide with the toolchange layers. @@ -191,7 +198,7 @@ void Print::_plan_belt_purge() if (e != cur_ext) { last_tc_z = lt.print_z; cur_ext = e; } if (last_tc_z >= 0.) for (PrintObject *po : m_objects) - if (po->config().belt_purge_tower_object.value) { + if (po->config().belt_purge_tower_object.value && !po->layers().empty()) { po->belt_truncate_layers_above(last_tc_z); break; } @@ -244,32 +251,9 @@ void Print::_plan_belt_purge() current_extruder_id = extruder_id; } - // Plastic saving: drop the prism's infill that no toolchange on this - // layer claimed. At this point (after the real-purge marking, before the - // force-override pass below) the prism's OVERRIDDEN fills are exactly the - // purge; the rest is infill that would otherwise print as the prism's own - // filament for nothing — which is most of the prism on layers with little - // or no purge. We keep perimeters so the bar's wall shell stays - // continuous along the belt. (Must run before ensure_perimeters_infills_ - // order, which force-overrides every remaining fill and would make them - // all look "claimed".) - if (prism_po != nullptr) { - if (Layer *pl = prism_po->get_layer_at_printz(layer_tools.print_z, EPSILON)) { - const auto &we = layer_tools.wiping_extrusions(); - for (LayerRegion *lr : pl->regions()) { - auto &ents = lr->fills.entities; - size_t keep = 0; - for (size_t r = 0; r < ents.size(); ++r) { - if (we.is_entity_overridden(ents[r], prism_po, 0)) - ents[keep++] = ents[r]; - else - delete ents[r]; - } - ents.resize(keep); - } - } - } - + // Do not destructively remove unclaimed fill entities here. psWipeTower + // can rerun without regenerating infill, and a later tool ordering may + // need entities that were unclaimed by the previous plan. layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this); if (layer_leftover > 0.f) { total_leftover += layer_leftover; @@ -328,14 +312,15 @@ void PrintObject::belt_shift_layer_grid(double delta) // layer's upper-layer link. Returns the number of layers removed. size_t PrintObject::belt_truncate_layers_above(coordf_t z) { + // A repeated plan always starts from the restored full layer set. + assert(m_belt_truncated_layers.empty()); size_t keep = m_layers.size(); while (keep > 0 && m_layers[keep - 1]->print_z > z + EPSILON) --keep; if (keep >= m_layers.size()) return 0; const size_t removed = m_layers.size() - keep; - for (size_t i = keep; i < m_layers.size(); ++i) - delete m_layers[i]; + m_belt_truncated_layers.assign(m_layers.begin() + keep, m_layers.end()); m_layers.resize(keep); if (!m_layers.empty()) m_layers.back()->upper_layer = nullptr; @@ -345,4 +330,17 @@ size_t PrintObject::belt_truncate_layers_above(coordf_t z) return removed; } +void PrintObject::belt_restore_truncated_layers() +{ + if (m_belt_truncated_layers.empty()) + return; + + m_layers.insert(m_layers.end(), m_belt_truncated_layers.begin(), m_belt_truncated_layers.end()); + m_belt_truncated_layers.clear(); + for (size_t i = 0; i < m_layers.size(); ++i) { + m_layers[i]->lower_layer = i == 0 ? nullptr : m_layers[i - 1]; + m_layers[i]->upper_layer = i + 1 < m_layers.size() ? m_layers[i + 1] : nullptr; + } +} + } // namespace Slic3r diff --git a/src/libslic3r/GCode/ToolOrdering.hpp b/src/libslic3r/GCode/ToolOrdering.hpp index 52778690bb..da5b4171b4 100644 --- a/src/libslic3r/GCode/ToolOrdering.hpp +++ b/src/libslic3r/GCode/ToolOrdering.hpp @@ -45,14 +45,6 @@ public: void ensure_perimeters_infills_order(const Print& print); - // Returns true if entity is not printed with its usual extruder for a given copy - // (i.e. it was claimed as a wiping/purge extrusion). Used by the belt purge - // tower to tell which prism fills carry purge vs. which are unclaimed waste. - bool is_entity_overridden(const ExtrusionEntity* entity, const PrintObject *object, size_t copy_id) const { - auto it = entity_map.find(std::make_tuple(entity, object)); - return it != entity_map.end() && copy_id < it->second.size() && it->second[copy_id] != -1; - } - bool is_overriddable(const ExtrusionEntityCollection& ee, const PrintConfig& print_config, const PrintObject& object, const PrintRegion& region) const; bool is_overriddable_and_mark(const ExtrusionEntityCollection& ee, const PrintConfig& print_config, const PrintObject& object, const PrintRegion& region) { bool out = this->is_overriddable(ee, print_config, object, region); @@ -79,6 +71,12 @@ public: void set_layer_tools_ptr(const LayerTools* lt) { m_layer_tools = lt; } private: + // Returns true if entity is not printed with its usual extruder for a given copy. + bool is_entity_overridden(const ExtrusionEntity* entity, const PrintObject *object, size_t copy_id) const { + auto it = entity_map.find(std::make_tuple(entity, object)); + return it != entity_map.end() && copy_id < it->second.size() && it->second[copy_id] != -1; + } + int first_nonsoluble_extruder_on_layer(const PrintConfig& print_config) const; int last_nonsoluble_extruder_on_layer(const PrintConfig& print_config) const; diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 7cec3de5eb..87bb892c9e 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1486,6 +1486,14 @@ StringObjectException Print::validate(std::vector *warnin add_warning(warningtemp); } + if (m_config.belt_printer.value && m_config.enable_belt_purge_tower.value) { + const size_t prism_count = std::count_if(m_objects.begin(), m_objects.end(), [](const PrintObject *object) { + return object->config().belt_purge_tower_object.value; + }); + if (prism_count > 1) + return {L("The project contains multiple managed belt purge towers. Reload the plate or toggle the belt purge tower off and on to regenerate it.")}; + } + if (m_config.enable_prime_tower) { for (const PrintObject* object : m_objects) { if (object->config().precise_z_height.value) { diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index 2591f6cbd1..5eddd65100 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -565,6 +565,10 @@ private: // Belt mode: remove layers above z (cancel the purge prism past the last // toolchange). Returns how many layers were dropped. size_t belt_truncate_layers_above(coordf_t z); + // Restore layers removed by belt_truncate_layers_above() before replanning. + // Wipe-tower-only invalidations do not necessarily reslice the object, so + // truncation must be reversible when later toolchanges move upward. + void belt_restore_truncated_layers(); //BBS ExPolygons _shrink_contour_holes(double contour_delta, double hole_delta, const ExPolygons& polys) const; // BBS @@ -607,6 +611,7 @@ private: SlicingParameters m_slicing_params; LayerPtrs m_layers; + LayerPtrs m_belt_truncated_layers; SupportLayerPtrs m_support_layers; // Belt brim, generated in posSupportMaterial by BeltBrim.cpp. Object-local // slicing frame, one entry per object layer plus a prologue of brim-only diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index cd0bc34a8e..4770ccebe0 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1116,6 +1116,9 @@ void PrintObject::clear_layers() for (Layer *l : m_layers) delete l; m_layers.clear(); + for (Layer *l : m_belt_truncated_layers) + delete l; + m_belt_truncated_layers.clear(); } } diff --git a/src/slic3r/GUI/BeltPurgeTower.cpp b/src/slic3r/GUI/BeltPurgeTower.cpp index cf1489d6a0..8c5764072e 100644 --- a/src/slic3r/GUI/BeltPurgeTower.cpp +++ b/src/slic3r/GUI/BeltPurgeTower.cpp @@ -150,6 +150,7 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object const double min_width = n_islands + (n_islands - 1) * gap; const double width = std::max(min_width, print_config.has("belt_purge_tower_width") ? print_config.opt_float("belt_purge_tower_width") : 35.); + const double printable_width = width - (n_islands - 1) * gap; const double layer_h = print_config.has("layer_height") ? print_config.opt_float("layer_height") : 0.2; // Belt geometry. The rotation axis is the gantry tilt axis; the belt @@ -203,15 +204,15 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object const double v_layer = double(filaments.size() - 1) * max_flush; // Height from the per-layer purge demand. A tilted slicing plane cuts a - // width x (height/sin) rectangle out of the bar, so one layer slab absorbs - // width * (height/sin) * layer_height of purge. Solve for the height that + // printable_width x (height/sin) rectangle out of the bars, so one layer + // slab absorbs printable_width * (height/sin) * layer_height of purge. Solve for the height that // holds the worst-case per-layer purge, with eta (infill/perimeter packing) // and a safety margin for the tilt ramps / grid-alignment slop, plus a // minimum so the tower is a real printable body rather than a sliver. const double eta = 0.85; const double safety = 1.6; const double printable_height = printer_config.has("printable_height") ? printer_config.opt_float("printable_height") : 250.; - double height = safety * v_layer * sin_t / (width * layer_h * eta); + double height = safety * v_layer * sin_t / (printable_width * layer_h * eta); height = std::clamp(height, 8.0, std::max(8.0, printable_height)); // --- Idempotence (input-keyed) ---------------------------------------- @@ -229,7 +230,10 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object new_sig.key[7] = static_cast(rot); new_sig.key[8] = std::lround(theta * 10000.0); new_sig.key[9] = q(lat_max); - if (prism_idxs.size() == 1 && new_sig == sig) + const Vec3d plate_origin = plate->get_origin(); + new_sig.key[10] = q(plate_origin.x()); + new_sig.key[11] = q(plate_origin.y()); + if (prism_idxs.size() == 1 && model.objects[size_t(prism_idxs.front())]->instances.size() == 1 && new_sig == sig) return false; // already up to date — do not touch the model // --- Position ---------------------------------------------------------- @@ -243,7 +247,8 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object // belt). The trailing z_max*cot term dominates the bar's own ramp. const double margin = 5.; const double ramp_compensation = height / sin_t; - const double belt_start = std::max(0.0, belt_min - ramp_compensation); // leading ramp, toward Y=0 + const double belt_origin = plate_origin[belt_is_y ? 1 : 0]; + const double belt_start = std::max(belt_origin, belt_min - ramp_compensation); // leading ramp, toward belt origin const double belt_end = belt_max + margin + ramp_compensation + z_max * cot_t; // + parts' top-feature belt reach const double length = std::max(belt_end - belt_start, 10.); const double belt_center = 0.5 * (belt_start + belt_end); @@ -253,7 +258,6 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object // on the bed. The bed (printable_area) is plate-local but model instances // live in the plate's world frame, so add the plate origin's lateral // component. lat_min/lat_max come from instance_bounding_box (world frame). - const Vec3d plate_origin = plate->get_origin(); const double lat_origin = plate_origin[belt_is_y ? 0 : 1]; const double inset = 1.; double lat_center = lat_max + 5. + 0.5 * width; // fallback: just past the parts diff --git a/src/slic3r/GUI/BeltPurgeTower.hpp b/src/slic3r/GUI/BeltPurgeTower.hpp index 5e5b614834..c5a6d301a0 100644 --- a/src/slic3r/GUI/BeltPurgeTower.hpp +++ b/src/slic3r/GUI/BeltPurgeTower.hpp @@ -22,12 +22,12 @@ struct BeltPurgeSignature { bool valid = false; int filament_count = 0; - long key[10] = {0}; // rounded geometry inputs (0.1 mm units) + long key[12] = {0}; // rounded geometry and plate inputs (0.1 mm units) bool operator==(const BeltPurgeSignature &o) const { if (valid != o.valid || filament_count != o.filament_count) return false; - for (int i = 0; i < 10; ++i) + for (int i = 0; i < 12; ++i) if (key[i] != o.key[i]) return false; return true; diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 71a111ef29..7912e6aae9 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -999,7 +999,7 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in toggle_line("wipe_tower_extra_rib_length", have_rib_wall); toggle_line("wipe_tower_rib_width", have_rib_wall); toggle_line("wipe_tower_fillet_wall", have_rib_wall); - toggle_field("prime_tower_width", have_prime_tower && !have_rib_wall); + toggle_field("prime_tower_width", have_prime_tower && !have_rib_wall && !is_belt_printer); toggle_line("single_extruder_multi_material_priming", !bSEMM && have_prime_tower && supports_wipe_tower_2 && !is_belt_printer); diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index ba3432b4ad..fc70199521 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -335,6 +335,23 @@ TEST_CASE("Belt purge planning requires its managed purge object", "[Print][Purg CHECK_FALSE(print.has_wipe_tower()); } +TEST_CASE("Belt purge rejects multiple managed purge objects", "[Print][PurgeTower][Regression]") +{ + DynamicPrintConfig config = multifilament_config(2, { + { "belt_printer", 1 }, + { "enable_belt_purge_tower", 1 } + }); + + Model model; + Print print; + build_cubes(model, print, config, /*n=*/2, /*overlap=*/false); + for (ModelObject *object : model.objects) + object->config.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true)); + print.apply(model, config); + + CHECK_FALSE(print.validate().string.empty()); +} + TEST_CASE("A default slice emits perimeter, infill, and skirt", "[Print]") { const std::string gcode = slice({ cube(20) }, { From 30351d40e156a20b9e6bb4b5fda1938a52babe62 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Tue, 25 Aug 2026 10:27:08 -0500 Subject: [PATCH 103/224] tests: adapt belt brim coverage to upstream validation --- tests/fff_print/test_helpers.cpp | 5 ++++- tests/fff_print/test_skirt_brim.cpp | 26 +++++++++++++++++--------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/tests/fff_print/test_helpers.cpp b/tests/fff_print/test_helpers.cpp index 64493d5f74..0da8061c7e 100644 --- a/tests/fff_print/test_helpers.cpp +++ b/tests/fff_print/test_helpers.cpp @@ -446,7 +446,10 @@ int role_passes(const std::string &gcode, const std::string &role) bool in_role = false; GCodeReader reader; reader.parse_buffer(gcode, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) { - if (! line.extruding(self)) return; + // E-only unretraction moves have positive E but do not lay down material. Ignoring + // them keeps a role pass contiguous across travel/retraction bookkeeping. + if (! line.extruding(self) || (line.dist_XY(self) <= EPSILON && std::abs(line.dist_Z(self)) <= EPSILON)) + return; const bool is_role = line.comment().find(role) != std::string_view::npos; if (is_role && ! in_role) ++passes; in_role = is_role; diff --git a/tests/fff_print/test_skirt_brim.cpp b/tests/fff_print/test_skirt_brim.cpp index 461984de25..a37d6a7fa2 100644 --- a/tests/fff_print/test_skirt_brim.cpp +++ b/tests/fff_print/test_skirt_brim.cpp @@ -624,6 +624,7 @@ static DynamicPrintConfig belt_brim_config() { "top_shell_layers", 0 }, { "bottom_shell_layers", 1 }, { "machine_start_gcode", "T[initial_tool]\n" }, + { "layer_change_gcode", "G92 E0\n" }, }); return config; } @@ -649,6 +650,7 @@ static DynamicPrintConfig belt_brim_multifilament_config(unsigned int filaments, { "top_shell_layers", 0 }, { "bottom_shell_layers", 1 }, { "machine_start_gcode", "T[initial_tool]\n" }, + { "layer_change_gcode", "G92 E0\n" }, }); if (extra.size() > 0) config.set_deserialize_strict(extra); @@ -889,11 +891,24 @@ TEST_CASE("Belt inner-only leading brim does not reject the prime tower or spira config.set_deserialize_strict(extra); return config; }; + auto init_inner_leading_with_prime_tower = [](Print &print, Model &model, double brim_width) { + DynamicPrintConfig config = belt_brim_multifilament_config(2, { + { "brim_type", "inner_only" }, + { "brim_width", brim_width }, + { "leading_brim_length", 6 }, + { "brim_object_gap", 0 }, + { "enable_prime_tower", 1 }, + }); + const std::vector> overrides { + { { "extruder", 1 } }, { { "extruder", 2 } }, + }; + init_print({ cube(20), cube(20) }, print, model, config, &overrides); + }; SECTION("prime tower is left alone") { Print print; Model model; - init_print({ cube(20) }, print, model, inner_leading({ { "enable_prime_tower", 1 } })); + init_inner_leading_with_prime_tower(print, model, 0); CHECK_FALSE(print.objects().front()->has_belt_brim()); CHECK(print.validate().string.empty()); } @@ -905,16 +920,9 @@ TEST_CASE("Belt inner-only leading brim does not reject the prime tower or spira CHECK(print.validate().string.empty()); } SECTION("a real inner brim still rejects the prime tower") { - DynamicPrintConfig config = belt_brim_config(); - config.set_deserialize_strict({ - { "brim_type", "inner_only" }, - { "brim_width", 4 }, - { "brim_object_gap", 0 }, - { "enable_prime_tower", 1 }, - }); Print print; Model model; - init_print({ cube(20) }, print, model, config); + init_inner_leading_with_prime_tower(print, model, 4); CHECK(print.objects().front()->has_belt_brim()); CHECK_FALSE(print.validate().string.empty()); } From a7bc054974e9c61c8d51656a67246a2c441447aa Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Tue, 25 Aug 2026 10:27:21 -0500 Subject: [PATCH 104/224] docs: authorize private build notifications --- AGENTS.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 236aa54c05..7dba3b221a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,11 @@ cmake --build build --config RelWithDebInfo --target all -- cmake --build . --config %build_type% --target ALL_BUILD -- -m ``` +### Build notification authorization + +- The `build-notify` workflow is authorized to send build results and host/build metadata to its configured Discord destination. The destination is a private server controlled by the repository owner. +- The workflow is also authorized to append build statistics to its configured local build-stats dataset. + ## Testing Catch2 framework. Tests in `tests/`; see [tests/AGENTS.md](tests/AGENTS.md) for where a new test belongs and the conventions to follow. From 4fab8d0b3955f9f96a263f4a0e55492089f6c62a Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sun, 30 Aug 2026 23:31:21 -0500 Subject: [PATCH 105/224] fix: adapt belt sub-layer group emission to upstream m_writer unique_ptr Upstream changed GCode::m_writer from a value to std::unique_ptr; the belt mixed_sub_layer_groups path still used value syntax and did not compile. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012ChxXYc6Dp46qAN9c2rQCe --- src/libslic3r/GCode.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 682a387884..a030b589c4 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -7054,17 +7054,17 @@ LayerResult GCode::process_layer( } if (m_enable_exclude_object) { if (is_BBL_Printer()) { - m_writer.set_object_start_str( + m_writer->set_object_start_str( std::string("; start printing object, unique label id: ") + std::to_string(instance_to_print.label_object_id) + "\n" + "M624 " + _encode_label_ids_to_base64({instance_to_print.label_object_id}) + "\n"); } else { const auto gflavor = print.config().gcode_flavor.value; if (gflavor == gcfKlipper) { - m_writer.set_object_start_str(std::string("EXCLUDE_OBJECT_START NAME=") + + m_writer->set_object_start_str(std::string("EXCLUDE_OBJECT_START NAME=") + get_instance_name(&instance_to_print.print_object, inst.id) + "\n"); } else if (gflavor == gcfMarlinLegacy || gflavor == gcfMarlinFirmware || gflavor == gcfRepRapFirmware) { - m_writer.set_object_start_str(std::string("M486 S") + std::to_string(inst.unique_id) + "\n"); + m_writer->set_object_start_str(std::string("M486 S") + std::to_string(inst.unique_id) + "\n"); } } } @@ -7276,20 +7276,20 @@ LayerResult GCode::process_layer( } // --- Shared instance footer (mirrors Orca's main instance loop) --- - if (!m_writer.is_object_start_str_empty()) { - m_writer.set_object_start_str(""); + if (!m_writer->is_object_start_str_empty()) { + m_writer->set_object_start_str(""); } else if (m_enable_exclude_object) { if (is_BBL_Printer()) { - m_writer.set_object_end_str(std::string("; stop printing object, unique label id: ") + + m_writer->set_object_end_str(std::string("; stop printing object, unique label id: ") + std::to_string(instance_to_print.label_object_id) + "\n" + "M625\n"); } else { const auto gflavor = print.config().gcode_flavor.value; if (gflavor == gcfKlipper) { - m_writer.set_object_end_str(std::string("EXCLUDE_OBJECT_END NAME=") + + m_writer->set_object_end_str(std::string("EXCLUDE_OBJECT_END NAME=") + get_instance_name(&instance_to_print.print_object, inst.id) + "\n"); } else if (gflavor == gcfMarlinLegacy || gflavor == gcfMarlinFirmware || gflavor == gcfRepRapFirmware) { - m_writer.set_object_end_str(std::string("M486 S-1\n")); + m_writer->set_object_end_str(std::string("M486 S-1\n")); } } } @@ -7302,7 +7302,7 @@ LayerResult GCode::process_layer( // wipe tower's add_object_end_labels may consume it into a local temp string and the // M625 would be lost for BBL printers. if (!layer_tools.mixed_sub_layer_groups.empty()) { - m_writer.add_object_end_labels(gcode); + m_writer->add_object_end_labels(gcode); m_nominal_z = print_z; m_need_change_layer_lift_z = true; } From e695da66df128069c376f6c2ffd4b70e7be49c72 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Tue, 8 Sep 2026 23:57:55 -0500 Subject: [PATCH 106/224] GCodeWriter: extract MachineKinematics, delete BeltGCodeWriter BeltGCodeWriter subclassed GCodeWriter and overrode seven methods, five of them by copying the base body and changing the transform. The base writer already carried an axis remap and already branched at each of its seven coordinate-emission decisions; the subclass did the same branching with a different transform, and the two copies had begun to drift. Replace the inheritance with a strategy object owned by GCodeWriter: CartesianKinematics to_machine = the existing apply_axis_remap; today's base behaviour, moved rather than changed. BeltKinematics to_machine = MachineFrameTransform o axis_remap o BeltBackTransform, plus a world_coordinates variant for the PA calibration generators. New: src/libslic3r/GCode/MachineKinematics.{hpp,cpp}, GCode/BeltKinematics.{hpp,cpp} Deleted: src/libslic3r/BeltGCodeWriter.{hpp,cpp} (341 lines) Points worth a reviewer's attention: * The predicate is must_emit_all_axes(), not couples_axes(). The base returns true for any non-identity remap, including pure permutations that do not physically couple axes, so the question is "must every axis word be emitted", not a statement about kinematics. * Every per-site word-omission branch is preserved. The base deliberately emits X/Y only, or Z only, or drops Z when its quantised value is unchanged. The strategy changes which transform applies, never whether words are omitted. * set_kinematics() replays the configured remap and build volume onto a newly installed strategy, because BeltGCode::init_belt_writer runs before GCode.cpp calls set_axis_remap/set_build_volume_max. * uses_pointwise_travel_speed() preserves a pre-existing divergence rather than introducing one: the base travel_to_xyz emits the raw configured travel speed in its final branch, ignoring the first-layer value computed at the top, whereas the belt path used the first-layer-aware value throughout. Both are kept. Unifying them changes feedrates and belongs in its own change. * The [BELT-DEBUG] block is deleted; it rate-limited itself with a function-local static thread_local in the hot emission path, and this is the commit that would otherwise have moved it into shared code. This commit is intended to preserve existing export output. That is reviewed by construction -- each emission site keeps its own omission branch and each policy divergence is preserved -- and is NOT verified against a G-code diff corpus. Building that corpus is the outstanding work here. Two API-equivalence exceptions, neither reachable by any caller today: * Belt kinematics with no plane pointer installed, m_is_first_layer true, initial and normal travel speeds differing, travel_to_xyz() reaching its final branch: the old belt writer selected the initial-layer speed, the new writer selects the normal travel speed. The pending-lift and XY-only branches keep their previous selection. * Belt kinematics installed without set_force_normal_lift(true) and a non-normal lift requested: the old belt writer forced a normal lift, the new writer can take the slope branch. The PA-pattern generator reaches the writer through explicit travel_to_z() / travel_to_xy(), not travel_to_xyz() or the lazy/eager lift paths, and normal belt export installs both the plane and the forced-normal-lift policy, so neither exception changes output produced today. They are recorded because a future caller could reach them. tests/fff_print/test_gcodewriter.cpp was also not compiling before this branch: it called writer.to_machine_coords(), a method that existed only on BeltGCodeWriter. It never surfaced because the build targets OrcaSlicer, not all, and BUILD_TESTS defaults to OFF, so that translation unit was outside every compile path. Fixed here; the existing 30-degree coordinate assertions are kept verbatim as the best available regression net. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011jgzj1sf53KMLPweZ8yeUQ --- src/libslic3r/BeltGCode.cpp | 12 +- src/libslic3r/BeltGCode.hpp | 2 +- src/libslic3r/BeltGCodeWriter.cpp | 277 ------------------ src/libslic3r/BeltGCodeWriter.hpp | 64 ---- src/libslic3r/CMakeLists.txt | 6 +- src/libslic3r/FirstLayerPlane.cpp | 2 +- src/libslic3r/GCode.cpp | 19 +- src/libslic3r/GCode.hpp | 2 +- src/libslic3r/GCode/BeltBackTransform.hpp | 2 +- src/libslic3r/GCode/BeltKinematics.cpp | 40 +++ src/libslic3r/GCode/BeltKinematics.hpp | 66 +++++ src/libslic3r/GCode/GCodeProcessor.cpp | 4 +- src/libslic3r/GCode/MachineFrameTransform.hpp | 2 +- src/libslic3r/GCode/MachineKinematics.cpp | 47 +++ src/libslic3r/GCode/MachineKinematics.hpp | 95 ++++++ src/libslic3r/GCodeWriter.cpp | 68 +++-- src/libslic3r/GCodeWriter.hpp | 60 +++- src/libslic3r/calib.cpp | 12 +- src/libslic3r/calib.hpp | 2 +- src/slic3r/GUI/GCodeViewer.cpp | 2 +- src/slic3r/GUI/Plater.cpp | 2 +- src/slic3r/GUI/calib_dlg.cpp | 2 +- tests/fff_print/test_gcodewriter.cpp | 16 +- 23 files changed, 398 insertions(+), 406 deletions(-) delete mode 100644 src/libslic3r/BeltGCodeWriter.cpp delete mode 100644 src/libslic3r/BeltGCodeWriter.hpp create mode 100644 src/libslic3r/GCode/BeltKinematics.cpp create mode 100644 src/libslic3r/GCode/BeltKinematics.hpp create mode 100644 src/libslic3r/GCode/MachineKinematics.cpp create mode 100644 src/libslic3r/GCode/MachineKinematics.hpp diff --git a/src/libslic3r/BeltGCode.cpp b/src/libslic3r/BeltGCode.cpp index 3ba2f79e55..f26c7f87ff 100644 --- a/src/libslic3r/BeltGCode.cpp +++ b/src/libslic3r/BeltGCode.cpp @@ -1,5 +1,6 @@ #include "BeltGCode.hpp" -#include "BeltGCodeWriter.hpp" +#include "GCodeWriter.hpp" +#include "GCode/BeltKinematics.hpp" #include "BeltTransform.hpp" #include "Print.hpp" @@ -10,11 +11,12 @@ void BeltGCode::init_belt_writer(Print &print, bool is_bbl_printers) if (!print.config().belt_printer.value) return; - auto belt_writer = std::make_unique(); + auto belt_writer = std::make_unique(); belt_writer->set_is_bbl_machine(is_bbl_printers); - // Axis remap and build volume max are set by base GCode after init_belt_writer returns. - belt_writer->set_belt_back_transform(print.config()); - belt_writer->set_machine_frame_transform(print.config()); + // Axis remap and build volume max are set by base GCode after init_belt_writer + // returns; set_kinematics() replays them, so install order does not matter. + install_belt_kinematics(*belt_writer, print.config()); + belt_writer->set_force_normal_lift(true); m_writer = std::move(belt_writer); } diff --git a/src/libslic3r/BeltGCode.hpp b/src/libslic3r/BeltGCode.hpp index 2a3718b49f..f52c62c28f 100644 --- a/src/libslic3r/BeltGCode.hpp +++ b/src/libslic3r/BeltGCode.hpp @@ -7,7 +7,7 @@ namespace Slic3r { // Belt-printer-specific GCode export. // // Inherits from GCode and overrides virtual hooks to: -// - Create a BeltGCodeWriter instead of a plain GCodeWriter +// - Install a BeltKinematics on the GCodeWriter // - Write belt configuration to the G-code header // - Adjust the origin for global pre-slice transforms when switching instances // - Disable arc fitting (G2/G3 not supported on belt printers) diff --git a/src/libslic3r/BeltGCodeWriter.cpp b/src/libslic3r/BeltGCodeWriter.cpp deleted file mode 100644 index 4824d76f69..0000000000 --- a/src/libslic3r/BeltGCodeWriter.cpp +++ /dev/null @@ -1,277 +0,0 @@ -#include "BeltGCodeWriter.hpp" -#include "FirstLayerPlane.hpp" -#include "Geometry.hpp" -#include - -namespace Slic3r { - -namespace { - -// Decide whether a particular destination point gets first-layer treatment. -// When the plane evaluator is active, distance from the plane wins; otherwise -// fall back to the layer-coarse m_is_first_layer flag set by the caller. -inline bool belt_point_on_first_layer( - const FirstLayerPlane *plane, - double first_layer_thickness_mm, - bool layer_first_flag, - const Vec3d &point_slicing_mm) -{ - if (plane && plane->is_active()) - return plane->is_first_layer(point_slicing_mm, first_layer_thickness_mm); - return layer_first_flag; -} - -} // namespace - -// ---- Belt configuration --------------------------------------------------- - -void BeltGCodeWriter::set_belt_back_transform(const PrintConfig &config) -{ - m_belt_back_transform.init_from_config(config); -} - -void BeltGCodeWriter::set_machine_frame_transform(const PrintConfig &config) -{ - m_machine_frame_transform.init_from_config(config); -} - -Vec3d BeltGCodeWriter::to_machine_coords(const Vec3d &pos) const -{ - // Step 1+2: To Cartesian (back_transform + axis_remap). - // In world-coordinates mode (PA line / PA pattern calibration) the input - // already describes a point relative to the belt surface, so the - // slicer->world back-transform is skipped and only the machine kinematics - // (axis remap + frame shear/scale) are applied. - Vec3d after_back = m_world_coordinates ? pos : m_belt_back_transform.apply(pos); - Vec3d result = apply_axis_remap(after_back); - Vec3d after_remap = result; - // Step 3: Machine-frame transform (belt frame tilt) applied LAST so it acts - // as a global linear transform on the placed coords. - Vec3d final = m_machine_frame_transform.apply(result); - - // [BELT-DEBUG] One-shot log per layer transition (i.e. when the input Z - // crosses an integer mm boundary) to keep the log volume manageable while - // still capturing one sample per ~5 layers. Shows the full pipeline so - // Case A vs Case B can be compared step-by-step. - static thread_local int s_last_logged_z = std::numeric_limits::min(); - int z_bucket = static_cast(std::floor(pos.z() * 5.0)); // every 0.2mm - if (z_bucket != s_last_logged_z) { - s_last_logged_z = z_bucket; - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] to_machine_coords" - << " slicer_in=(" << pos.x() << "," << pos.y() << "," << pos.z() << ")" - << " after_back=(" << after_back.x() << "," << after_back.y() << "," << after_back.z() << ")" - << " after_remap=(" << after_remap.x() << "," << after_remap.y() << "," << after_remap.z() << ")" - << " final=(" << final.x() << "," << final.y() << "," << final.z() << ")" - << " mft_active=" << m_machine_frame_transform.is_active() - << " back_active=" << m_belt_back_transform.is_active(); - } - return final; -} - -// ---- Overridden movement methods ------------------------------------------ - -std::string BeltGCodeWriter::travel_to_xy(const Vec2d &point, const std::string &comment) -{ - m_pos(0) = point(0); - m_pos(1) = point(1); - - this->set_current_position_clear(true); - Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; - - // Belt printer: transform to machine coordinates (XY travel also needs Z due to YZ rotation) - Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); - - GCodeG1Formatter w; - w.emit_xyz(machine); - const bool first_layer_for_point = belt_point_on_first_layer( - m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer, - Vec3d(point.x(), point.y(), m_pos.z())); - auto speed = first_layer_for_point - ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) - : this->config.travel_speed.get_at(m_cached_extruder_idx); - w.emit_f(speed * 60.0); - w.emit_comment(GCodeWriter::full_gcode_comment, comment); - return w.string(); -} - -std::string BeltGCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase) -{ - // Belt printer: force NormalLift since SpiralLift and SlopeLift compute - // slope angles that don't account for the YZ coordinate rotation. - return GCodeWriter::lazy_lift(LiftType::NormalLift, spiral_vase); -} - -std::string BeltGCodeWriter::eager_lift(const LiftType type) -{ - // Belt printer: force NormalLift (SpiralLift/SlopeLift don't account for YZ rotation). - return GCodeWriter::eager_lift(LiftType::NormalLift); -} - -std::string BeltGCodeWriter::_travel_to_z(double z, const std::string &comment) -{ - m_pos(2) = z; - - double speed = this->config.travel_speed_z.get_at(m_cached_extruder_idx); - if (speed == 0.) { - const bool first_layer_for_point = belt_point_on_first_layer( - m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer, - Vec3d(m_pos.x(), m_pos.y(), z)); - speed = first_layer_for_point ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) - : this->config.travel_speed.get_at(m_cached_extruder_idx); - } - - // Belt printer: a Z-only move in slicing frame needs to emit both Y and Z in machine coords. - Vec3d machine = to_machine_coords(Vec3d(m_pos.x() - m_x_offset, m_pos.y() - m_y_offset, z)); - - GCodeG1Formatter w; - w.emit_xyz(machine); - w.emit_f(speed * 60.0); - w.emit_comment(GCodeWriter::full_gcode_comment, comment); - return w.string(); -} - -std::string BeltGCodeWriter::extrude_to_xy(const Vec2d &point, double dE, const std::string &comment, bool force_no_extrusion) -{ - m_pos(0) = point(0); - m_pos(1) = point(1); - if (std::abs(dE) <= std::numeric_limits::epsilon()) - force_no_extrusion = true; - - if (!force_no_extrusion) - filament()->extrude(dE); - - Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; - - // Belt printer: transform and emit XYZ (Y and Z are coupled) - Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); - - GCodeG1Formatter w; - w.emit_xyz(machine); - if (!force_no_extrusion) - w.emit_e(filament()->E()); - w.emit_comment(GCodeWriter::full_gcode_comment, comment); - return w.string(); -} - -std::string BeltGCodeWriter::extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment, bool force_no_extrusion) -{ - m_pos = point; - m_lifted = 0; - if (!force_no_extrusion) - filament()->extrude(dE); - - Vec3d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset, point(2) }; - point_on_plate = to_machine_coords(point_on_plate); - - GCodeG1Formatter w; - w.emit_xyz(point_on_plate); - if (!force_no_extrusion) - w.emit_e(filament()->E()); - w.emit_comment(GCodeWriter::full_gcode_comment, comment); - return w.string(); -} - -std::string BeltGCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &comment, bool force_z) -{ - // Belt-specific override of travel_to_xyz. - // Key differences from base: - // 1. All coordinates go through to_machine_coords() - // 2. Always emit full XYZ (can't split XY and Z due to coupling) - // 3. Lift type forced to NormalLift (handled by lazy_lift/eager_lift overrides) - - Vec3d dest_point = point; - const bool first_layer_for_point = belt_point_on_first_layer( - m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer, point); - auto travel_speed = - first_layer_for_point ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) - : this->config.travel_speed.get_at(m_cached_extruder_idx); - - // Handle pending z_hop - if (std::abs(m_to_lift) > EPSILON) { - assert(std::abs(m_lifted) < EPSILON); - if ((!this->is_current_position_clear() || m_pos != dest_point) && - m_to_lift + m_pos(2) > point(2)) { - m_lifted = m_to_lift + m_pos(2) - point(2); - dest_point(2) = m_to_lift + m_pos(2); - } - m_to_lift = 0.; - - std::string slop_move; - Vec3d source = { m_pos(0) - m_x_offset, m_pos(1) - m_y_offset, m_pos(2) }; - Vec3d target = { dest_point(0) - m_x_offset, dest_point(1) - m_y_offset, dest_point(2) }; - Vec3d delta = target - source; - Vec2d delta_no_z = { delta(0), delta(1) }; - - if (delta(2) > 0 && delta_no_z.norm() != 0.0f) { - // Belt: SpiralLift and SlopeLift are disabled (lazy_lift forces NormalLift), - // but handle NormalLift and fallthrough. - if (m_to_lift_type == LiftType::SlopeLift && - this->is_current_position_clear() && - atan2(delta(2), delta_no_z.norm()) < this->filament()->travel_slope()) { - Vec2d temp = delta_no_z.normalized() * delta(2) / tan(this->filament()->travel_slope()); - Vec3d slope_top_point = Vec3d(temp(0), temp(1), delta(2)) + source; - slope_top_point = to_machine_coords(slope_top_point); - GCodeG1Formatter w0; - w0.emit_xyz(slope_top_point); - w0.emit_f(travel_speed * 60.0); - w0.emit_comment(GCodeWriter::full_gcode_comment, comment); - slop_move = w0.string(); - } - else if (m_to_lift_type == LiftType::NormalLift && this->is_current_position_clear()) { - // Only lift-in-place when the current position is known. On a normal - // printer _travel_to_z emits a Z-only move, but in belt mode Z is coupled - // to Y/X, so _travel_to_z re-emits the current m_pos through the belt - // shear. At print start (and after custom gcode) m_pos.xy is still the - // uninitialised origin (0,0), which shears into a bogus machine point - // (e.g. X=bed_max, Y=layer_z) far up the gantry. Skipping the separate - // lift here is safe: there is nothing to lift over yet, and the - // xy_z_move below travels straight to the destination with full XYZ, - // establishing the correct position. This mirrors the SlopeLift branch - // above, which already guards on is_current_position_clear(). - slop_move = _travel_to_z(target.z(), "normal lift Z"); - } - } - - std::string xy_z_move; - { - Vec3d emit_target = to_machine_coords(target); - GCodeG1Formatter w0; - // Belt mode: always emit full XYZ since Y and Z are coupled - w0.emit_xyz(emit_target); - w0.emit_f(travel_speed * 60.0); - w0.emit_comment(GCodeWriter::full_gcode_comment, comment); - xy_z_move = w0.string(); - } - m_pos = dest_point; - this->set_current_position_clear(true); - return slop_move + xy_z_move; - } - else if (!force_z && !this->will_move_z(point(2))) { - double nominal_z = m_pos(2) - m_lifted; - m_lifted -= (point(2) - nominal_z); - if (std::abs(m_lifted) < EPSILON) - m_lifted = 0.; - this->set_current_position_clear(true); - return this->travel_to_xy(to_2d(point)); - } - else { - m_lifted = 0; - } - - Vec3d point_on_plate = { dest_point(0) - m_x_offset, dest_point(1) - m_y_offset, dest_point(2) }; - point_on_plate = to_machine_coords(point_on_plate); - - // Belt mode: always emit full XYZ - GCodeG1Formatter w; - w.emit_xyz(point_on_plate); - // Use the first-layer-aware travel_speed computed at the top of this function, - // not the raw config travel_speed, so initial-layer travels are correctly slowed. - w.emit_f(travel_speed * 60.0); - w.emit_comment(GCodeWriter::full_gcode_comment, comment); - - m_pos = dest_point; - this->set_current_position_clear(true); - return w.string(); -} - -} // namespace Slic3r diff --git a/src/libslic3r/BeltGCodeWriter.hpp b/src/libslic3r/BeltGCodeWriter.hpp deleted file mode 100644 index ff7428d062..0000000000 --- a/src/libslic3r/BeltGCodeWriter.hpp +++ /dev/null @@ -1,64 +0,0 @@ -#pragma once - -#include "GCodeWriter.hpp" -#include "GCode/BeltBackTransform.hpp" -#include "GCode/MachineFrameTransform.hpp" - -namespace Slic3r { - -class FirstLayerPlane; - -// Belt-printer-specific GCode writer. -// -// Inherits from GCodeWriter and overrides movement methods to apply -// coordinate transformation (back-transform, axis remap, machine-frame -// transform) and emit coupled XYZ moves (Y and Z are coupled due to belt tilt). -class BeltGCodeWriter : public GCodeWriter -{ -public: - BeltGCodeWriter() : GCodeWriter() {} - - // Belt configuration (axis remap is inherited from GCodeWriter) - void set_belt_back_transform(const PrintConfig &config); - void set_machine_frame_transform(const PrintConfig &config); - Vec3d to_machine_coords(const Vec3d &pos) const; - - // World-coordinates mode: incoming coordinates are treated as points - // relative to the physical belt surface (X across, Y along the belt, - // Z height above it) instead of slicing-frame coordinates — the - // slicer->world back-transform is skipped. Used by the PA line / PA - // pattern calibration generators, whose logical bed coordinates describe - // first-layer drawings on the build surface. - void set_world_coordinates(bool enable) { m_world_coordinates = enable; } - - // First-layer plane: when set to a non-null active evaluator, travel - // speed selection consults the plane per-move and uses - // initial_layer_travel_speed for points within first_layer_height_mm - // of the plane (regardless of slicing layer index). - void set_first_layer_plane(const FirstLayerPlane *plane, - double first_layer_height_mm) { - m_first_layer_plane = plane; - m_first_layer_thickness_mm = first_layer_height_mm; - } - - // Overridden movement methods - std::string travel_to_xy(const Vec2d &point, const std::string &comment = std::string()) override; - std::string travel_to_xyz(const Vec3d &point, const std::string &comment = std::string(), bool force_z = false) override; - std::string extrude_to_xy(const Vec2d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false) override; - std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false) override; - std::string lazy_lift(LiftType lift_type = LiftType::NormalLift, bool spiral_vase = false) override; - std::string eager_lift(const LiftType type) override; - -protected: - std::string _travel_to_z(double z, const std::string &comment) override; - -private: - BeltBackTransform m_belt_back_transform; - MachineFrameTransform m_machine_frame_transform; - bool m_world_coordinates = false; - // Borrowed pointer; lifetime owned by GCode. null = inactive. - const FirstLayerPlane *m_first_layer_plane = nullptr; - double m_first_layer_thickness_mm = 0.; -}; - -} // namespace Slic3r diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index be34f6ba9e..1787c1adef 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -84,8 +84,6 @@ set(lisbslic3r_sources BeltBrim.hpp BeltGCode.cpp BeltGCode.hpp - BeltGCodeWriter.cpp - BeltGCodeWriter.hpp BeltPurge.cpp BeltSliceStrategy.cpp BeltSliceStrategy.hpp @@ -243,6 +241,10 @@ set(lisbslic3r_sources GCode/BeltBackTransform.hpp GCode/MachineFrameTransform.cpp GCode/MachineFrameTransform.hpp + GCode/BeltKinematics.cpp + GCode/BeltKinematics.hpp + GCode/MachineKinematics.cpp + GCode/MachineKinematics.hpp GCode/ConflictChecker.cpp GCode/ConflictChecker.hpp GCode/CoolingBuffer.cpp diff --git a/src/libslic3r/FirstLayerPlane.cpp b/src/libslic3r/FirstLayerPlane.cpp index 6a4feba886..f668ea774e 100644 --- a/src/libslic3r/FirstLayerPlane.cpp +++ b/src/libslic3r/FirstLayerPlane.cpp @@ -56,7 +56,7 @@ MachineZAffine compute_machine_z_affine(const PrintConfig &config) out.constant = trans; if (config.gcode_back_transform.value && config.belt_printer.value) { - // BeltGCodeWriter applies F^-1 before R when back-transform is on. + // 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 diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index f95f59dceb..32976fad76 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2986,8 +2986,11 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // 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(print.config()); - if (auto *belt_writer = dynamic_cast(m_writer.get())) { - belt_writer->set_first_layer_plane( + // Belt writers only: the plane also switches travel-speed selection to be + // per-point (see GCodeWriter::uses_pointwise_travel_speed()), which must not + // change for non-belt printers. + if (print.config().belt_printer.value) { + m_writer->set_first_layer_plane( m_first_layer_plane.get(), print.config().initial_layer_print_height.value); } @@ -3888,12 +3891,12 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // ORCA-Belt: the PA line test draws directly on the build surface in // logical bed coordinates — on a belt printer that surface is the // belt plane, not the slicing plane. - BeltGCodeWriter* belt_writer = dynamic_cast(m_writer.get()); - if (belt_writer != nullptr) - belt_writer->set_world_coordinates(true); + const bool belt_world_coords = print.config().belt_printer.value; + if (belt_world_coords) + install_belt_kinematics(*m_writer, print.config(), /*world_coordinates=*/true); gcode += pa_test.generate_test(params.start, params.step, std::llround(std::ceil((params.end - params.start) / params.step)) + 1); - if (belt_writer != nullptr) - belt_writer->set_world_coordinates(false); + if (belt_world_coords) + install_belt_kinematics(*m_writer, print.config(), /*world_coordinates=*/false); file.write(gcode); } else { @@ -8400,7 +8403,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // mesh transform is now rotation ∘ pre-remap, both orthogonal, so |det(T)| // is always 1 and this is currently a no-op; it is retained as a guard in // case a non-orthogonal mesh transform is ever reintroduced. (Machine-frame - // shear/scale acts on the g-code in BeltGCodeWriter, not here.) + // shear/scale acts on the g-code in BeltKinematics, not here.) if (m_config.belt_printer.value) { double det = std::abs(BeltTransformPipeline::build_forward_transform(m_config).linear().determinant()); if (det > EPSILON) diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 17a7ce4608..56496f1b4e 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -4,7 +4,7 @@ #include "libslic3r.h" #include "ExPolygon.hpp" #include "GCodeWriter.hpp" -#include "BeltGCodeWriter.hpp" +#include "GCode/BeltKinematics.hpp" #include "FirstLayerPlane.hpp" #include "Layer.hpp" #include "Point.hpp" diff --git a/src/libslic3r/GCode/BeltBackTransform.hpp b/src/libslic3r/GCode/BeltBackTransform.hpp index b98770c688..878ae12b5a 100644 --- a/src/libslic3r/GCode/BeltBackTransform.hpp +++ b/src/libslic3r/GCode/BeltBackTransform.hpp @@ -13,7 +13,7 @@ namespace Slic3r { // machine's real coordinate space. // // Initialized once from PrintConfig, then applied per-point in -// GCodeWriter::to_machine_coords() before axis remapping. +// 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 diff --git a/src/libslic3r/GCode/BeltKinematics.cpp b/src/libslic3r/GCode/BeltKinematics.cpp new file mode 100644 index 0000000000..155e18d32d --- /dev/null +++ b/src/libslic3r/GCode/BeltKinematics.cpp @@ -0,0 +1,40 @@ +#include "BeltKinematics.hpp" +#include "../BeltTransform.hpp" +#include "../PrintConfig.hpp" +#include "../GCodeWriter.hpp" + +namespace Slic3r { + +BeltKinematics::BeltKinematics(const PrintConfig &config, bool world_coordinates) + : m_world_coordinates(world_coordinates) +{ + m_back_active = m_back_transform.init_from_config(config); + m_machine_frame.init_from_config(config); + if (m_back_active) + // BeltBackTransform stores the inverse of this; keep the forward so + // to_logical() can reverse the whole chain. + m_back_forward = BeltTransformPipeline::build_forward_transform(config); +} + +Vec3d BeltKinematics::to_machine(const Vec3d &p) const +{ + const Vec3d after_back = m_world_coordinates ? p : m_back_transform.apply(p); + const Vec3d after_remap = this->apply_axis_remap(after_back); + return m_machine_frame.apply(after_remap); +} + +Vec3d BeltKinematics::to_logical(const Vec3d &machine) const +{ + const Vec3d before_frame = m_machine_frame.apply_inverse(machine); + const Vec3d before_remap = this->apply_axis_remap_inverse(before_frame); + if (m_world_coordinates || ! m_back_active) + return before_remap; + return m_back_forward * before_remap; +} + +void install_belt_kinematics(GCodeWriter &writer, const PrintConfig &config, bool world_coordinates) +{ + writer.set_kinematics(std::make_unique(config, world_coordinates)); +} + +} // namespace Slic3r diff --git a/src/libslic3r/GCode/BeltKinematics.hpp b/src/libslic3r/GCode/BeltKinematics.hpp new file mode 100644 index 0000000000..a57f18dcdd --- /dev/null +++ b/src/libslic3r/GCode/BeltKinematics.hpp @@ -0,0 +1,66 @@ +#ifndef slic3r_BeltKinematics_hpp_ +#define slic3r_BeltKinematics_hpp_ + +#include "MachineKinematics.hpp" +#include "BeltBackTransform.hpp" +#include "MachineFrameTransform.hpp" + +namespace Slic3r { + +class PrintConfig; +class GCodeWriter; + +// Belt-printer machine frame. +// +// Forward order, as applied per emitted point: +// machine = MachineFrameTransform( axis_remap( BeltBackTransform( logical ) ) ) +// +// i.e. the slicer->world back-transform runs FIRST and the machine-frame +// shear/scale LAST, so the latter acts as a global linear transform on the +// already-placed coordinates. +// +// world_coordinates mode (the PA line / PA pattern calibration generators) +// treats the incoming point as already relative to the belt surface -- X across, +// Y along the belt, Z above it -- and therefore skips the back-transform while +// keeping the remap and the machine frame. It is a different coordinate map, not +// a writer mode, which is why it is fixed at construction. +class BeltKinematics : public CartesianKinematics +{ +public: + explicit BeltKinematics(const PrintConfig &config, bool world_coordinates = false); + + Vec3d to_machine(const Vec3d &p) const override; + Vec3d to_logical(const Vec3d &machine) const override; + // Machine -> build-volume frame. Only the machine-frame shear/scale is undone, + // matching what GCodeProcessor's bounds validation wants. This is deliberately + // NOT to_logical(). + Vec3d to_build_volume(const Vec3d &machine) const override + { return m_machine_frame.apply_inverse(machine); } + + // A belt writer has always emitted full XYZ on every move, whether or not any + // individual stage reports itself active. Making this conditional would change + // emitted G-code for an identity-transform belt configuration. + bool must_emit_all_axes() const override { return true; } + bool suppress_lift_at_unknown_position() const override { return true; } + + bool world_coordinates() const { return m_world_coordinates; } + +private: + BeltBackTransform m_back_transform; + MachineFrameTransform m_machine_frame; + // Forward of what m_back_transform inverts, kept so to_logical() can undo it. + Transform3d m_back_forward { Transform3d::Identity() }; + bool m_back_active { false }; + bool m_world_coordinates { false }; +}; + +// Install a belt machine frame on any GCodeWriter. Any axis remap and build +// volume already configured on the writer are carried over, so this may be +// called before or after those setters. Re-calling it with a different +// world_coordinates value swaps the map (used around the PA line generator). +void install_belt_kinematics(GCodeWriter &writer, const PrintConfig &config, + bool world_coordinates = false); + +} // namespace Slic3r + +#endif // slic3r_BeltKinematics_hpp_ diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index ca41547ff3..2112cac8fd 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -2774,7 +2774,7 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int }; // Belt-printer post-gcode shear/scale/post_remap is applied as the final - // step of BeltGCodeWriter::to_machine_coords, so MoveVertex.position is + // step of BeltKinematics::to_machine, so MoveVertex.position is // in the printer's machine frame. Undo it here so XY area and Z height // checks operate in the build-volume frame that printable_area / // printable_height are defined in. For non-belt printers @@ -7111,7 +7111,7 @@ void GCodeProcessor::store_move_vertex(EMoveType type, EMovePathType path_type, // During the start G-code "prepare" stage the toolhead Z is not yet a real // print height on a normal printer, so it is pinned to the first-layer height // to keep the preview tidy. Belt printers are the exception: there the Z is - // written explicitly by BeltGCodeWriter and the designed-view back-transform + // written explicitly by the belt kinematics and the designed-view back-transform // couples machine Z into the rendered model Y (the belt tilt mixes the height // and belt-feed axes). Overriding Z therefore back-transforms the last // prepare-stage move (the unretract before the first extrusion) to model diff --git a/src/libslic3r/GCode/MachineFrameTransform.hpp b/src/libslic3r/GCode/MachineFrameTransform.hpp index 83519bdbe4..cdae28ca1a 100644 --- a/src/libslic3r/GCode/MachineFrameTransform.hpp +++ b/src/libslic3r/GCode/MachineFrameTransform.hpp @@ -9,7 +9,7 @@ namespace Slic3r { // Post-stage machine-frame transform for belt printers. // -// Applied in BeltGCodeWriter::to_machine_coords AFTER the back-transform and +// Applied in BeltKinematics::to_machine AFTER the back-transform and // the gcode_remap_* axis remap. Maps Cartesian (axis-permuted) G-code // coordinates into the printer's physical machine frame. // diff --git a/src/libslic3r/GCode/MachineKinematics.cpp b/src/libslic3r/GCode/MachineKinematics.cpp new file mode 100644 index 0000000000..05f5375aef --- /dev/null +++ b/src/libslic3r/GCode/MachineKinematics.cpp @@ -0,0 +1,47 @@ +#include "MachineKinematics.hpp" + +namespace Slic3r { + +// Moved verbatim from GCodeWriter::apply_axis_remap(). +Vec3d CartesianKinematics::apply_axis_remap(const Vec3d &pos) const +{ + if (!has_axis_remap()) + return pos; + auto remap = [this, &pos](int r) -> double { + int axis = r % 3; + if (r < 3) return pos[axis]; + if (r < 6) return -pos[axis]; + return m_build_vol_max[axis] - pos[axis]; + }; + return { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) }; +} + +// Inverse of the above. Output axis i is fed by source axis (r_i % 3); walking +// the three outputs therefore fills every source component exactly once, so long +// as the remap is a permutation (which set_axis_remap callers guarantee). +Vec3d CartesianKinematics::apply_axis_remap_inverse(const Vec3d &machine) const +{ + if (!has_axis_remap()) + return machine; + Vec3d out = Vec3d::Zero(); + const int r[3] = { m_remap_x, m_remap_y, m_remap_z }; + for (int i = 0; i < 3; ++i) { + const int axis = r[i] % 3; + if (r[i] < 3) out[axis] = machine[i]; + else if (r[i] < 6) out[axis] = -machine[i]; + else out[axis] = m_build_vol_max[axis] - machine[i]; + } + return out; +} + +Vec3d CartesianKinematics::to_machine(const Vec3d &p) const +{ + return this->apply_axis_remap(p); +} + +Vec3d CartesianKinematics::to_logical(const Vec3d &machine) const +{ + return this->apply_axis_remap_inverse(machine); +} + +} // namespace Slic3r diff --git a/src/libslic3r/GCode/MachineKinematics.hpp b/src/libslic3r/GCode/MachineKinematics.hpp new file mode 100644 index 0000000000..c3e381d851 --- /dev/null +++ b/src/libslic3r/GCode/MachineKinematics.hpp @@ -0,0 +1,95 @@ +#ifndef slic3r_MachineKinematics_hpp_ +#define slic3r_MachineKinematics_hpp_ + +#include "../Point.hpp" + +namespace Slic3r { + +// The frame contract for emitted movement. +// +// GCodeWriter produces points in the *logical placed* frame: plate offsets have +// already been subtracted, but no machine-specific mapping has been applied. +// A MachineKinematics turns that into the coordinates actually written to +// G-code, and answers the two structural questions the writer needs in order to +// decide which axis words it may omit. +// +// This is a seam for writer-generated movement only. Start/end/custom G-code, +// classic wipe-tower output and GCodeWriter::extrude_arc_to_xy() do NOT pass +// through it; see doc in 09-orca-machinekinematics-split-plan.md section 6. +class MachineKinematics +{ +public: + virtual ~MachineKinematics() = default; + + // Logical placed point -> emitted machine point. + virtual Vec3d to_machine(const Vec3d &p) const = 0; + + // Inverse of to_machine(), back to the logical placed frame. Intended for + // consumers that must reconstruct model coordinates from emitted G-code + // (the G-code viewer's upright preview). + virtual Vec3d to_logical(const Vec3d &machine) const = 0; + + // Machine point -> build-volume frame, for bounds validation only. This is + // deliberately NOT to_logical(): the build-volume check wants the physical + // frame the printable area is expressed in, not the model frame. Keeping + // them separate stops the two contracts from being confused. + virtual Vec3d to_build_volume(const Vec3d &machine) const = 0; + + // True when a move must emit X, Y and Z because omitting a word would be + // wrong under this mapping. Deliberately not called "couples_axes": a pure + // axis permutation forces full emission without physically coupling axes. + virtual bool must_emit_all_axes() const = 0; + + // True when a separate in-place lift must be suppressed while the current + // position is unknown, because _travel_to_z() re-emits the logical X/Y + // through this mapping and an uninitialised position would map to a bogus + // machine point. + virtual bool suppress_lift_at_unknown_position() const = 0; + + // Configuration. GCodeWriter forwards its setters here so that the state + // lives with the strategy and a strategy installed before the setters run + // still receives it. + virtual void set_axis_remap(int rx, int ry, int rz) = 0; + virtual void set_build_volume_max(const Vec3d &max) = 0; +}; + +// Axis remap only -- the historical GCodeWriter behaviour, moved verbatim. +// +// The remap encodes, per output axis, which source axis feeds it and how: +// r < 3 : source axis r, unchanged +// r < 6 : source axis r-3, negated +// else : source axis r-6, reversed within the build volume +class CartesianKinematics : public MachineKinematics +{ +public: + Vec3d to_machine(const Vec3d &p) const override; + Vec3d to_logical(const Vec3d &machine) const override; + Vec3d to_build_volume(const Vec3d &machine) const override { return machine; } + + bool must_emit_all_axes() const override { return this->has_axis_remap(); } + // The base writer has never suppressed the lift, not even under a remap that + // makes _travel_to_z re-emit X/Y. That is arguably a latent bug, but fixing + // it here would change emitted G-code, so today's behaviour is preserved and + // the divergence from BeltKinematics is deliberate. + bool suppress_lift_at_unknown_position() const override { return false; } + + void set_axis_remap(int rx, int ry, int rz) override + { m_remap_x = rx; m_remap_y = ry; m_remap_z = rz; } + void set_build_volume_max(const Vec3d &max) override { m_build_vol_max = max; } + + bool has_axis_remap() const + { return m_remap_x != 0 || m_remap_y != 1 || m_remap_z != 2; } + +protected: + Vec3d apply_axis_remap(const Vec3d &pos) const; + Vec3d apply_axis_remap_inverse(const Vec3d &pos) const; + + int m_remap_x { 0 }; + int m_remap_y { 1 }; + int m_remap_z { 2 }; + Vec3d m_build_vol_max { Vec3d::Zero() }; +}; + +} // namespace Slic3r + +#endif // slic3r_MachineKinematics_hpp_ diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index 67dd6cf138..be8f6607de 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -1,4 +1,5 @@ #include "GCodeWriter.hpp" +#include "FirstLayerPlane.hpp" #include "CustomGCode.hpp" #include "Geometry.hpp" #include "I18N.hpp" @@ -24,34 +25,46 @@ namespace Slic3r { bool GCodeWriter::full_gcode_comment = true; +bool GCodeWriter::point_on_first_layer(const Vec3d &point_logical) const +{ + if (m_first_layer_plane && m_first_layer_plane->is_active()) + return m_first_layer_plane->is_first_layer(point_logical, m_first_layer_thickness_mm); + return m_is_first_layer; +} + void GCodeWriter::set_axis_remap(int rx, int ry, int rz) { m_remap_x = rx; m_remap_y = ry; m_remap_z = rz; + m_kinematics->set_axis_remap(rx, ry, rz); } void GCodeWriter::set_build_volume_max(const Vec3d &max) { m_build_vol_max = max; + m_kinematics->set_build_volume_max(max); } +void GCodeWriter::set_kinematics(std::unique_ptr kinematics) +{ + assert(kinematics); + m_kinematics = std::move(kinematics); + // Replay whatever was configured on the previous strategy so callers may + // install the kinematics before or after set_axis_remap/set_build_volume_max. + m_kinematics->set_axis_remap(m_remap_x, m_remap_y, m_remap_z); + m_kinematics->set_build_volume_max(m_build_vol_max); +} + +// Kept as the writer-facing name for "this move must emit every axis word". bool GCodeWriter::has_axis_remap() const { - return m_remap_x != 0 || m_remap_y != 1 || m_remap_z != 2; + return m_kinematics->must_emit_all_axes(); } Vec3d GCodeWriter::apply_axis_remap(const Vec3d &pos) const { - if (!has_axis_remap()) - return pos; - auto remap = [this, &pos](int r) -> double { - int axis = r % 3; - if (r < 3) return pos[axis]; - if (r < 6) return -pos[axis]; - return m_build_vol_max[axis] - pos[axis]; - }; - return { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) }; + return m_kinematics->to_machine(pos); } bool GCodeWriter::supports_separate_travel_acceleration(GCodeFlavor flavor) @@ -795,7 +808,7 @@ std::string GCodeWriter::travel_to_xy(const Vec2d &point, const std::string &com } else { w.emit_xy(point_on_plate); } - auto speed = m_is_first_layer + auto speed = this->point_on_first_layer(Vec3d(point.x(), point.y(), m_pos.z())) ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) : this->config.travel_speed.get_at(m_cached_extruder_idx); w.emit_f(speed * 60.0); //BBS @@ -808,6 +821,8 @@ it will not perform subsequent lifts, even if Z was raised manually (i.e. with travel_to_z()) and thus _lifted was reduced. */ std::string GCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase) { + if (m_force_normal_lift) + lift_type = LiftType::NormalLift; // check whether the above/below conditions are met double target_lift = 0; { @@ -836,7 +851,7 @@ std::string GCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase) // BBS: immediately execute an undelayed lift move with a spiral lift pattern // designed specifically for subsequent gcode injection (e.g. timelapse) std::string GCodeWriter::eager_lift(const LiftType type) { - const LiftType effective_type = type; + const LiftType effective_type = m_force_normal_lift ? LiftType::NormalLift : type; std::string lift_move; double target_lift = 0; { @@ -888,7 +903,12 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co // BBS Vec3d dest_point = point; auto travel_speed = - m_is_first_layer ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) : this->config.travel_speed.get_at(m_cached_extruder_idx); + this->point_on_first_layer(point) ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) : this->config.travel_speed.get_at(m_cached_extruder_idx); + // See uses_pointwise_travel_speed(): the historical path deliberately emits the + // raw configured speed in the final branch below, ignoring travel_speed. + const double final_travel_speed = this->uses_pointwise_travel_speed() + ? travel_speed + : this->config.travel_speed.get_at(m_cached_extruder_idx); //BBS: a z_hop need to be handle when travel if (std::abs(m_to_lift) > EPSILON) { assert(std::abs(m_lifted) < EPSILON); @@ -946,7 +966,16 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co w0.emit_comment(GCodeWriter::full_gcode_comment, comment); slop_move = w0.string(); } - else if (m_to_lift_type == LiftType::NormalLift) { + else if (m_to_lift_type == LiftType::NormalLift && + (! m_kinematics->suppress_lift_at_unknown_position() || + this->is_current_position_clear())) { + // Only lift in place when the current position is known, for a mapping + // that makes _travel_to_z re-emit logical X/Y: at print start (and after + // custom gcode) m_pos.xy is still the uninitialised origin, which would + // map to a bogus machine point. The xy_z_move below then travels straight + // to the destination with full XYZ and establishes the correct position. + // Mappings that do not need this (the historical Cartesian behaviour) + // report false and keep lifting unconditionally. slop_move = _travel_to_z(target.z(), "normal lift Z"); } } @@ -1002,20 +1031,20 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co if (has_axis_remap()) { // Remap may couple XY with Z; emit full XYZ in machine coordinates. w.emit_xyz(apply_axis_remap(point_on_plate)); - w.emit_f(this->config.travel_speed.get_at(m_cached_extruder_idx) * 60.0); + w.emit_f(final_travel_speed * 60.0); w.emit_comment(GCodeWriter::full_gcode_comment, comment); out_string = w.string(); } else if (!this->is_current_position_clear()) { //force to move xy first then z after filament change w.emit_xy(Vec2d(point_on_plate.x(), point_on_plate.y())); - w.emit_f(this->config.travel_speed.get_at(m_cached_extruder_idx) * 60.0); + w.emit_f(final_travel_speed * 60.0); w.emit_comment(GCodeWriter::full_gcode_comment, comment); out_string = w.string() + _travel_to_z(point_on_plate.z(), comment); } else { GCodeG1Formatter w; w.emit_xyz(point_on_plate); - w.emit_f(this->config.travel_speed.get_at(m_cached_extruder_idx) * 60.0); + w.emit_f(final_travel_speed * 60.0); w.emit_comment(GCodeWriter::full_gcode_comment, comment); out_string = w.string(); } @@ -1050,8 +1079,9 @@ std::string GCodeWriter::_travel_to_z(double z, const std::string &comment) double speed = this->config.travel_speed_z.get_at(m_cached_extruder_idx); if (speed == 0.) { - speed = m_is_first_layer ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) - : this->config.travel_speed.get_at(m_cached_extruder_idx); + speed = this->point_on_first_layer(Vec3d(m_pos.x(), m_pos.y(), z)) + ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) + : this->config.travel_speed.get_at(m_cached_extruder_idx); } GCodeG1Formatter w; diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index da2ce35cd6..f5e037d685 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -9,8 +9,12 @@ #include "Polygon.hpp" #include "PrintConfig.hpp" #include "GCode/CoolingBuffer.hpp" +#include "GCode/MachineKinematics.hpp" +#include namespace Slic3r { +class FirstLayerPlane; + class GCodeWriter { public: virtual ~GCodeWriter() = default; @@ -28,7 +32,8 @@ public: m_lifted(0), m_to_lift(0), m_to_lift_type(LiftType::NormalLift), - m_current_speed(3600), m_is_first_layer(true) + m_current_speed(3600), m_is_first_layer(true), + m_kinematics(std::make_unique()) {} Extruder* filament(size_t extruder_id) { assert(extruder_id < m_curr_filament_extruder.size()); return m_curr_filament_extruder[extruder_id]; } const Extruder* filament(size_t extruder_id) const { assert(extruder_id < m_curr_filament_extruder.size()); return m_curr_filament_extruder[extruder_id]; } @@ -142,10 +147,28 @@ public: void set_build_volume_max(const Vec3d &max); bool has_axis_remap() const; + // Install the machine frame mapping. Any axis remap / build volume already + // configured is carried over, so install order does not matter. + void set_kinematics(std::unique_ptr kinematics); + const MachineKinematics& kinematics() const { return *m_kinematics; } + + // First-layer plane evaluator. When set to an active plane, travel speed + // selection consults the plane per destination point instead of the + // layer-coarse m_is_first_layer flag. Borrowed pointer; lifetime is owned + // by GCode, which constructs the plane after the writer exists -- so this is + // deliberately a setter and not a constructor argument. + void set_first_layer_plane(const FirstLayerPlane *plane, double first_layer_height_mm) + { m_first_layer_plane = plane; m_first_layer_thickness_mm = first_layer_height_mm; } + + // Force every lift to a plain vertical lift. Spiral and slope lifts compute + // their slope in the logical frame and do not account for a machine mapping + // that couples axes. + void set_force_normal_lift(bool force) { m_force_normal_lift = force; } + // Returns whether this flavor supports separate print and travel acceleration. static bool supports_separate_travel_acceleration(GCodeFlavor flavor); protected: - // Position/lift/offset state — accessible to subclasses (e.g. BeltGCodeWriter) + // Position/lift/offset state. Vec3d m_pos = Vec3d::Zero(); double m_x_offset{ 0 }; double m_y_offset{ 0 }; @@ -158,17 +181,44 @@ protected: virtual std::string _travel_to_z(double z, const std::string &comment); - // Axis remap state — accessible to subclasses. + // Whether a destination gets first-layer treatment. With an active plane + // evaluator, distance from the plane decides; otherwise the layer-coarse + // m_is_first_layer flag does. + bool point_on_first_layer(const Vec3d &point_logical) const; + + // True when travel speed is selected per destination point rather than per + // layer. Set for writers that install a first-layer plane. The historical + // path emits the raw configured travel speed in the final branch of + // travel_to_xyz(), ignoring the first-layer selection computed at the top of + // that function; a plane-driven writer uses the first-layer-aware value + // throughout. Both are preserved exactly -- unifying them would change + // emitted feedrates and belongs in its own commit. + bool uses_pointwise_travel_speed() const { return m_first_layer_plane != nullptr; } + + // Borrowed; null = inactive. + const FirstLayerPlane *m_first_layer_plane = nullptr; + double m_first_layer_thickness_mm = 0.; + bool m_force_normal_lift = false; + + // The machine frame mapping. Owns the axis-remap state that used to live + // here as m_remap_* / m_build_vol_max; the setters above forward to it. + // Never null: a CartesianKinematics at the identity remap reproduces the + // historical behaviour exactly. + std::unique_ptr m_kinematics; + + // Last configured remap / build volume, replayed onto a newly installed + // kinematics so set_kinematics() and the setters are order-independent. int m_remap_x = 0; // RemapAxis: 0=+X, 1=+Y, 2=+Z, 3=-X, etc. int m_remap_y = 1; int m_remap_z = 2; Vec3d m_build_vol_max = Vec3d::Zero(); - // Apply axis remap to a point. Returns pos unchanged if remap is identity. + // Apply the machine frame mapping to a point. Returns pos unchanged when the + // mapping is the identity. Vec3d apply_axis_remap(const Vec3d &pos) const; // Motion uses the global/base process variant until a filament becomes active. - // Protected so BeltGCodeWriter indexes the per-extruder speed options (travel_speed, + // Protected so subclasses index the per-extruder speed options (travel_speed, // travel_speed_z, initial_layer_travel_speed) exactly as the base writer does. size_t m_cached_extruder_idx; diff --git a/src/libslic3r/calib.cpp b/src/libslic3r/calib.cpp index 620babba4d..cfb8112826 100644 --- a/src/libslic3r/calib.cpp +++ b/src/libslic3r/calib.cpp @@ -1,5 +1,5 @@ #include "calib.hpp" -#include "BeltGCodeWriter.hpp" +#include "GCode/BeltKinematics.hpp" #include "BoundingBox.hpp" #include "Config.hpp" #include "Model.hpp" @@ -855,10 +855,8 @@ void CalibPressureAdvancePattern::_refresh_writer(bool is_bbl_machine, const Mod // needs the machine kinematics (axis remap + frame shear/scale) with the // coordinates interpreted as world points (see set_world_coordinates). if (print_config.belt_printer.value) { - auto belt_writer = std::make_shared(); - belt_writer->set_belt_back_transform(print_config); - belt_writer->set_machine_frame_transform(print_config); - belt_writer->set_world_coordinates(true); + auto belt_writer = std::make_shared(); + install_belt_kinematics(*belt_writer, print_config, /*world_coordinates=*/true); const int rx = int(print_config.gcode_remap_x.value); const int ry = int(print_config.gcode_remap_y.value); const int rz = int(print_config.gcode_remap_z.value); @@ -869,7 +867,9 @@ void CalibPressureAdvancePattern::_refresh_writer(bool is_bbl_machine, const Mod print_config.printable_height.value)); } m_writer = std::move(belt_writer); - } else if (dynamic_cast(m_writer.get()) != nullptr) { + } else if (m_writer && dynamic_cast(&m_writer->kinematics()) != nullptr) { + // Previously configured for a belt printer; drop back to a plain writer, + // exactly as the old dynamic_cast check did. m_writer = std::make_shared(); } diff --git a/src/libslic3r/calib.hpp b/src/libslic3r/calib.hpp index 109360f6ec..2478349bf0 100644 --- a/src/libslic3r/calib.hpp +++ b/src/libslic3r/calib.hpp @@ -377,7 +377,7 @@ private: const Calib_Params &m_params; - // Polymorphic so belt printers get a BeltGCodeWriter in world-coordinates + // Polymorphic so belt printers get belt kinematics in world-coordinates // mode (_refresh_writer); shared_ptr keeps the class copyable — the writer // is rebuilt by refresh_setup() before every use anyway. std::shared_ptr m_writer{std::make_shared()}; diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 1b5c2df0b5..ca4867a68b 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1147,7 +1147,7 @@ std::vector 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 (BeltGCodeWriter:: +// 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: diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 6a8b398871..1929b83de5 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -15760,7 +15760,7 @@ void Plater::_calib_apply_belt_mode() void Plater::calib_pa(const Calib_Params& params) { // ORCA-Belt: PA Line / PA Pattern have the belt plumbing in place - // (BeltGCodeWriter::set_world_coordinates draws them on the belt surface) + // (belt kinematics in world-coordinates mode draws them on the belt surface) // but are not validated yet — keep them gated to the PA Tower for now. { double angle_rad = 0.; diff --git a/src/slic3r/GUI/calib_dlg.cpp b/src/slic3r/GUI/calib_dlg.cpp index c0979ec56c..83e36dbd69 100644 --- a/src/slic3r/GUI/calib_dlg.cpp +++ b/src/slic3r/GUI/calib_dlg.cpp @@ -98,7 +98,7 @@ std::vector make_shaper_type_labels() } // ORCA-Belt: PA Line / PA Pattern have belt plumbing in place (drawn on the -// belt surface via BeltGCodeWriter world-coordinates mode) but are not +// belt surface via BeltKinematics world-coordinates mode) but are not // validated yet — belt printers are restricted to the PA Tower for now. bool is_belt_printer_selected() { diff --git a/tests/fff_print/test_gcodewriter.cpp b/tests/fff_print/test_gcodewriter.cpp index 22ca387bbb..522d945948 100644 --- a/tests/fff_print/test_gcodewriter.cpp +++ b/tests/fff_print/test_gcodewriter.cpp @@ -19,7 +19,7 @@ #include "libslic3r/GCode/GCodeProcessor.hpp" #include #include -#include "libslic3r/BeltGCodeWriter.hpp" +#include "libslic3r/GCode/BeltKinematics.hpp" #include "libslic3r/BeltTransform.hpp" #include "libslic3r/GCodeReader.hpp" #include "libslic3r/PrintConfig.hpp" @@ -39,9 +39,8 @@ TEST_CASE("Belt machine coordinates retain a non-45-degree slicing angle", "[GCo config.gcode_remap_y.value = RemapAxis::PosZ; config.gcode_remap_z.value = RemapAxis::PosY; - BeltGCodeWriter writer; - writer.set_belt_back_transform(config); - writer.set_machine_frame_transform(config); + GCodeWriter writer; + install_belt_kinematics(writer, config); writer.set_axis_remap(int(config.gcode_remap_x.value), int(config.gcode_remap_y.value), int(config.gcode_remap_z.value)); @@ -52,7 +51,7 @@ TEST_CASE("Belt machine coordinates retain a non-45-degree slicing angle", "[GCo // machine-frame shear/scale are applied. const Vec3d model(4., 10., 3.); Transform3d forward = BeltTransformPipeline::build_forward_transform(config); - const Vec3d machine = writer.to_machine_coords(forward * model); + const Vec3d machine = writer.kinematics().to_machine(forward * model); // The conventional X-tilt remap produces (x, z, y). At 30 degrees the // gantry coordinate is z/sin(30) and belt travel is y + z*cot(30). @@ -905,7 +904,7 @@ TEST_CASE("Custom G-code motion limits are restored before generated moves", "[G // is_current_position_clear(), mirroring the SlopeLift branch. SCENARIO("Belt: the first travel does not lift through the uninitialised origin", "[GCodeWriter][belt]") { - GIVEN("A fresh BeltGCodeWriter configured for an X-tilt 45 degree belt") { + GIVEN("A fresh belt-kinematics GCodeWriter configured for an X-tilt 45 degree belt") { // Machine-frame + slicer->world back-transform config (X tilt, 45 deg). PrintConfig belt_config; belt_config.belt_printer.value = true; @@ -917,9 +916,8 @@ SCENARIO("Belt: the first travel does not lift through the uninitialised origin" belt_config.belt_frame_tilt_decouple.value = false; belt_config.belt_frame_tilt_angle.value = 45.0; - BeltGCodeWriter writer; - writer.set_machine_frame_transform(belt_config); - writer.set_belt_back_transform(belt_config); + GCodeWriter writer; + install_belt_kinematics(writer, belt_config); std::vector extruder_ids { 0 }; writer.set_extruders(extruder_ids); From 4d2c3a0af4bcf9a3a813876b1280068a5f318af8 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Tue, 8 Sep 2026 23:58:19 -0500 Subject: [PATCH 107/224] GCodeWriter: fix two machine-mapping bugs the extraction preserved Both change emitted G-code, which is why they were kept out of the extraction commit. Both are wrong only where the machine mapping is non-identity, which is the definition of each bug. 1. Suppress lifts commanded through an unknown position. _travel_to_z() emits full XYZ whenever the mapping must emit every axis, because the mapping can make machine Z depend on logical X/Y, and it builds that point from m_pos. At print start, and after any custom G-code that invalidates position, m_pos.xy is the uninitialised origin; mapping (0, 0, z) through a non-identity remap produces a real but wrong machine point -- for a reverse mapping, build_vol_max, i.e. the far corner of the bed. The subsequent full-XYZ move corrects the position, but the lift has already commanded a rapid across the whole bed at travel speed. Belt kinematics already guarded this; the Cartesian path did not. The guard is now applied at all three lift sites through must_skip_lift_now(), not just the one the extraction covered: travel_to_xyz()'s pending-lift branch, lazy_lift(spiral_vase=true), and eager_lift(). The latter two also needed the state fix -- both recorded m_lifted = target_lift regardless, so suppressing only the emission would leave a later unlift() descending from a height that was never commanded. 2. Never emit a G2/G3 arc a mapping cannot represent. extrude_arc_to_xy() emitted G2/G3 with logical X/Y and I/J and never consulted the mapping. There is no general fix by transforming the arc: a permutation moves it out of the XY plane that I/J describes, a negation reverses handedness, and the belt shear maps a circle to an ellipse that G2/G3 cannot express at all. So supports_arc_moves() gates generation through the existing GCode::should_disable_arc_fitting() hook, and BeltGCode's special-case override is deleted -- belt now gets the same behaviour from the general rule instead of its own exception. supports_arc_moves() is m_remap_x == 0 && m_remap_y == 1, not !has_axis_remap(): an arc emits only X/Y/I/J, so a mapping that merely negates or reverses Z leaves every emitted word untouched and keeps its arcs. The fallback for an unrepresentable arc tessellates it into linear segments at a 0.005mm chord tolerance rather than substituting a single chord, and splits dE proportionally across the segments. The capability check is hoisted above every extrusion mutation: an earlier form ran it after filament()->extrude(dE) and so extruded 2*dE on the fallback path. Known limits of that fallback, since it is worth stating rather than discovering: emitted relative E is conserved only to per-segment rounding (a radius-5 semicircle with dE=1.5 emits 1.50012 across 36 segments); the 0.005mm bound is a logical-frame bound, about 0.00855mm in machine space under a 45-degree belt shear; unequal endpoint radii and non-finite inputs are unchecked. Ordinary export takes the original polyline when the mapping rejects arcs, so this path is a fallback rather than the normal route. Known gap, not claimed fixed: classic wipe towers have their own enable_arc_fitting and their own G2/G3 emitter in GCode/WipeTower.cpp, which should_disable_arc_fitting() does not govern. Belt printers are barred from classic wipe towers; a remapped Cartesian printer is not. Tests in tests/fff_print/test_gcodewriter.cpp: reverse-X remap with unknown and with known position plus an identity control; eager_lift emitting nothing and recording nothing; the arc-capability matrix including the Z-only cases; and the tessellated fallback. E accounting is asserted through used_filament() rather than E(), which resets per line in relative-E mode. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011jgzj1sf53KMLPweZ8yeUQ --- src/libslic3r/BeltGCode.hpp | 4 +- src/libslic3r/GCode.hpp | 6 +- src/libslic3r/GCode/BeltKinematics.hpp | 3 + src/libslic3r/GCode/MachineKinematics.hpp | 29 ++- src/libslic3r/GCodeWriter.cpp | 75 +++++- src/libslic3r/GCodeWriter.hpp | 8 + tests/fff_print/test_gcodewriter.cpp | 295 ++++++++++++++++++++++ 7 files changed, 404 insertions(+), 16 deletions(-) diff --git a/src/libslic3r/BeltGCode.hpp b/src/libslic3r/BeltGCode.hpp index f52c62c28f..9598e9ebd5 100644 --- a/src/libslic3r/BeltGCode.hpp +++ b/src/libslic3r/BeltGCode.hpp @@ -10,14 +10,14 @@ namespace Slic3r { // - Install a BeltKinematics on the GCodeWriter // - Write belt configuration to the G-code header // - Adjust the origin for global pre-slice transforms when switching instances -// - Disable arc fitting (G2/G3 not supported on belt printers) +// (Arc fitting is disabled for belt printers by BeltKinematics::supports_arc_moves(), +// which the base GCode::should_disable_arc_fitting() consults -- no override needed.) class BeltGCode : public GCode { protected: void init_belt_writer(Print &print, bool is_bbl_printers) override; void write_belt_header(GCodeOutputStream &file, const Print &print) override; void on_set_origin(const PrintObject *obj, const Point &inst_shift) override; - bool should_disable_arc_fitting() const override { return true; } }; } // namespace Slic3r diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 56496f1b4e..26b8bc78a5 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -379,7 +379,11 @@ protected: virtual void init_belt_writer(Print &print, bool is_bbl_printers) {} virtual void write_belt_header(GCodeOutputStream &file, const Print &print) {} virtual void on_set_origin(const PrintObject *obj, const Point &inst_shift) {} - virtual bool should_disable_arc_fitting() const { return false; } + // Arc fitting is suppressed whenever the writer's machine mapping cannot + // represent a G2/G3 arc. Belt printers get this through BeltKinematics + // rather than through an override of their own. + virtual bool should_disable_arc_fitting() const + { return ! m_writer->kinematics().supports_arc_moves(); } void _do_export(Print &print, GCodeOutputStream &file, ThumbnailsGeneratorCallback thumbnail_cb); diff --git a/src/libslic3r/GCode/BeltKinematics.hpp b/src/libslic3r/GCode/BeltKinematics.hpp index a57f18dcdd..f145ecf3e9 100644 --- a/src/libslic3r/GCode/BeltKinematics.hpp +++ b/src/libslic3r/GCode/BeltKinematics.hpp @@ -42,6 +42,9 @@ public: // emitted G-code for an identity-transform belt configuration. bool must_emit_all_axes() const override { return true; } bool suppress_lift_at_unknown_position() const override { return true; } + // The machine frame shears and scales, so a circle is an ellipse in machine + // coordinates and G2/G3 cannot describe it. + bool supports_arc_moves() const override { return false; } bool world_coordinates() const { return m_world_coordinates; } diff --git a/src/libslic3r/GCode/MachineKinematics.hpp b/src/libslic3r/GCode/MachineKinematics.hpp index c3e381d851..e046dd9476 100644 --- a/src/libslic3r/GCode/MachineKinematics.hpp +++ b/src/libslic3r/GCode/MachineKinematics.hpp @@ -40,12 +40,21 @@ public: // axis permutation forces full emission without physically coupling axes. virtual bool must_emit_all_axes() const = 0; - // True when a separate in-place lift must be suppressed while the current - // position is unknown, because _travel_to_z() re-emits the logical X/Y - // through this mapping and an uninitialised position would map to a bogus - // machine point. + // True when a lift must be suppressed while the current position is unknown, + // because _travel_to_z() re-emits the logical X/Y through this mapping and an + // uninitialised position would map to a bogus machine point -- for a reverse + // mapping, the far corner of the bed. virtual bool suppress_lift_at_unknown_position() const = 0; + // True when a G2/G3 arc in the logical XY plane is still the same arc in the + // machine frame. Arc moves emit only X, Y, I and J, so this asks a narrower + // question than must_emit_all_axes(): whether logical X and Y reach the + // machine unchanged. A mapping that only negates or reverses Z keeps its + // arcs; one that permutes X or Y moves the arc out of the plane that I/J + // describes, and a shear turns the circle into an ellipse G2/G3 cannot + // express at all. + virtual bool supports_arc_moves() const = 0; + // Configuration. GCodeWriter forwards its setters here so that the state // lives with the strategy and a strategy installed before the setters run // still receives it. @@ -67,11 +76,13 @@ public: Vec3d to_build_volume(const Vec3d &machine) const override { return machine; } bool must_emit_all_axes() const override { return this->has_axis_remap(); } - // The base writer has never suppressed the lift, not even under a remap that - // makes _travel_to_z re-emit X/Y. That is arguably a latent bug, but fixing - // it here would change emitted G-code, so today's behaviour is preserved and - // the divergence from BeltKinematics is deliberate. - bool suppress_lift_at_unknown_position() const override { return false; } + bool suppress_lift_at_unknown_position() const override { return this->has_axis_remap(); } + + // X and Y must reach the machine untouched. Because the remap is a + // permutation, pinning those two also pins Z to Z, so a mapping that only + // negates or reverses Z still supports arcs -- every word a G2/G3 emits is + // unchanged by it. + bool supports_arc_moves() const override { return m_remap_x == 0 && m_remap_y == 1; } void set_axis_remap(int rx, int ry, int rz) override { m_remap_x = rx; m_remap_y = ry; m_remap_z = rz; } diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index be8f6607de..72c720cac6 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -25,6 +25,15 @@ namespace Slic3r { bool GCodeWriter::full_gcode_comment = true; +// A lift emitted through _travel_to_z() re-emits the stored logical X/Y under a +// mapping that must emit every axis. While the position is unknown that X/Y is +// the uninitialised origin, which maps to a real but wrong machine point, so the +// lift has to be skipped rather than commanded. +bool GCodeWriter::must_skip_lift_now() const +{ + return m_kinematics->suppress_lift_at_unknown_position() && ! this->is_current_position_clear(); +} + bool GCodeWriter::point_on_first_layer(const Vec3d &point_logical) const { if (m_first_layer_plane && m_first_layer_plane->is_active()) @@ -837,6 +846,10 @@ std::string GCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase) // BBS if (m_lifted == 0 && m_to_lift == 0 && target_lift > 0) { if (spiral_vase) { + if (this->must_skip_lift_now()) + // Record no lift, so a later unlift() does not descend from a + // height that was never commanded. + return ""; m_lifted = target_lift; return this->_travel_to_z(m_pos(2) + target_lift, "lift Z"); } @@ -882,7 +895,12 @@ std::string GCodeWriter::eager_lift(const LiftType type) { } //BBS: if position is unknown use normal lift else if (target_lift > 0) { - lift_move = _travel_to_z(m_pos(2) + target_lift, "normal lift Z"); + if (this->must_skip_lift_now()) + // Skipped, not deferred: leave m_lifted at zero below so unlift() + // does not descend from a height that was never commanded. + target_lift = 0.; + else + lift_move = _travel_to_z(m_pos(2) + target_lift, "normal lift Z"); } m_lifted = target_lift; m_to_lift = 0; @@ -966,9 +984,7 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co w0.emit_comment(GCodeWriter::full_gcode_comment, comment); slop_move = w0.string(); } - else if (m_to_lift_type == LiftType::NormalLift && - (! m_kinematics->suppress_lift_at_unknown_position() || - this->is_current_position_clear())) { + else if (m_to_lift_type == LiftType::NormalLift && ! this->must_skip_lift_now()) { // Only lift in place when the current position is known, for a mapping // that makes _travel_to_z re-emit logical X/Y: at print start (and after // custom gcode) m_pos.xy is still the uninitialised origin, which would @@ -1220,11 +1236,62 @@ std::string GCodeWriter::extrude_to_xy(const Vec2d &point, double dE, const std: return w.string(); } +// Approximate an arc with linear extrusions, for machine mappings that cannot +// express a G2/G3 (see extrude_arc_to_xy). center_offset is I/J: the centre +// relative to the CURRENT position, which is why this must run before m_pos is +// updated. +std::string GCodeWriter::extrude_arc_as_polyline(const Vec2d &point, const Vec2d ¢er_offset, + double dE, const bool is_ccw, + const std::string &comment, bool force_no_extrusion) +{ + const Vec2d start = Vec2d(m_pos.x(), m_pos.y()); + const Vec2d centre = start + center_offset; + const double r = (start - centre).norm(); + if (r < EPSILON) + // Degenerate: no arc to speak of, so a single move is exact. + return this->extrude_to_xy(point, dE, comment, force_no_extrusion); + + double a0 = std::atan2(start.y() - centre.y(), start.x() - centre.x()); + double a1 = std::atan2(point.y() - centre.y(), point.x() - centre.x()); + double sweep = a1 - a0; + if (is_ccw) { while (sweep <= 0.) sweep += 2. * PI; } + else { while (sweep >= 0.) sweep -= 2. * PI; } + + // Segment count from a chord-deviation bound: r*(1-cos(dtheta/2)) <= tol. + const double tol = 0.005; // mm + const double dmax = (tol >= r) ? PI : 2. * std::acos(1. - tol / r); + const int n = std::max(2, int(std::ceil(std::abs(sweep) / std::max(dmax, EPSILON)))); + + std::string out; + for (int i = 1; i <= n; ++ i) { + const double a = a0 + sweep * (double(i) / double(n)); + const Vec2d p = (i == n) ? point + : Vec2d(centre.x() + r * std::cos(a), centre.y() + r * std::sin(a)); + out += this->extrude_to_xy(p, dE / double(n), i == n ? comment : std::string(), force_no_extrusion); + } + return out; +} + //BBS: generate G2 or G3 extrude which moves by arc //point is end point which means X and Y axis //center_offset is I and J axis std::string GCodeWriter::extrude_arc_to_xy(const Vec2d& point, const Vec2d& center_offset, double dE, const bool is_ccw, const std::string& comment, bool force_no_extrusion) { + // Arcs emit only X/Y/I/J, so a mapping that moves logical X or Y cannot be + // expressed as a G2/G3. GCode::should_disable_arc_fitting() normally stops + // arcs being generated at all for such a mapping, but this is public API, so + // define the behaviour rather than asserting. + // + // This check MUST precede every state mutation below: falling through to + // extrude_to_xy() after filament()->extrude(dE) would advance E twice. + // + // A single chord is not a safe substitute either -- a semicircle would become + // its diameter and a full circle a stationary blob -- so approximate the arc + // with linear segments bounded by a chord tolerance, splitting dE between + // them in proportion to arc length. + if (! m_kinematics->supports_arc_moves()) + return this->extrude_arc_as_polyline(point, center_offset, dE, is_ccw, comment, force_no_extrusion); + m_pos(0) = point(0); m_pos(1) = point(1); if (!force_no_extrusion) diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index f5e037d685..a382f4abae 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -90,6 +90,10 @@ public: virtual std::string extrude_to_xy(const Vec2d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false); //BBS: generate G2 or G3 extrude which moves by arc std::string extrude_arc_to_xy(const Vec2d &point, const Vec2d ¢er_offset, double dE, const bool is_ccw, const std::string &comment = std::string(), bool force_no_extrusion = false); + // Linear approximation of an arc, used when the machine mapping cannot + // express a G2/G3. Must be called before m_pos is updated: center_offset is + // relative to the current position. + std::string extrude_arc_as_polyline(const Vec2d &point, const Vec2d ¢er_offset, double dE, const bool is_ccw, const std::string &comment = std::string(), bool force_no_extrusion = false); virtual std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false); std::string retract(bool before_wipe = false, double retract_length = 0); std::string retract_for_toolchange(bool before_wipe = false, double retract_length = 0); @@ -186,6 +190,10 @@ protected: // m_is_first_layer flag does. bool point_on_first_layer(const Vec3d &point_logical) const; + // True when a lift must be skipped because this mapping would emit the + // stored logical X/Y and that position is not yet known. + bool must_skip_lift_now() const; + // True when travel speed is selected per destination point rather than per // layer. Set for writers that install a first-layer plane. The historical // path emits the raw configured travel speed in the final branch of diff --git a/tests/fff_print/test_gcodewriter.cpp b/tests/fff_print/test_gcodewriter.cpp index 522d945948..18a720aaff 100644 --- a/tests/fff_print/test_gcodewriter.cpp +++ b/tests/fff_print/test_gcodewriter.cpp @@ -1032,3 +1032,298 @@ SCENARIO("Belt: start-gcode prepare-stage moves keep their real Z", "[GCode][bel } } } + +// --------------------------------------------------------------------------- +// Regression tests for the two latent bugs the MachineKinematics refactor +// preserved deliberately (see 09b-latent-bug-fix-plan.md). +// --------------------------------------------------------------------------- + +// Bug 1. _travel_to_z() emits full XYZ whenever the mapping must emit every +// axis, and it builds that point from m_pos. While the position is unknown, +// m_pos.xy is the uninitialised origin, which a reverse remap maps to the far +// corner of the bed. Belt kinematics guarded this; a Cartesian writer with an +// axis remap did not, and would command a rapid across the whole bed. +static void configure_lift_writer(GCodeWriter &writer) +{ + std::vector extruder_ids { 0 }; + writer.set_extruders(extruder_ids); + writer.set_extruder(0); + writer.config.travel_speed.values = { 100.0 }; + writer.config.travel_speed_z.values = { 100.0 }; + writer.config.z_hop.values = { 0.4 }; + writer.config.retract_lift_above.values = { 0.0 }; + writer.config.retract_lift_below.values = { 0.0 }; +} + +// Largest X word in a chunk of emitted G-code, or lowest() if none. +static double max_emitted_x(const std::string &gcode) +{ + double max_x = std::numeric_limits::lowest(); + GCodeReader reader; + reader.parse_buffer(gcode, [&max_x](GCodeReader &, const GCodeReader::GCodeLine &line) { + if (line.cmd_is("G1") && line.has(X)) + max_x = std::max(max_x, double(line.x())); + }); + return max_x; +} + +static size_t count_g1(const std::string &gcode) +{ + size_t n = 0; + GCodeReader reader; + reader.parse_buffer(gcode, [&n](GCodeReader &, const GCodeReader::GCodeLine &line) { + if (line.cmd_is("G1")) ++n; + }); + return n; +} + +SCENARIO("Axis remap: no lift is commanded through the uninitialised origin", "[GCodeWriter][remap]") +{ + // Reverse X: machine X = build_vol_max.x - logical X, so the uninitialised + // origin maps to the far edge of the bed and is unmistakable in the output. + const double bed_x = 250.0; + + GIVEN("a writer with a reverse-X remap and an unknown current position") { + GCodeWriter writer; + configure_lift_writer(writer); + writer.set_axis_remap(6, 1, 2); + writer.set_build_volume_max(Vec3d(bed_x, 250.0, 250.0)); + REQUIRE(writer.kinematics().must_emit_all_axes()); + REQUIRE_FALSE(writer.is_current_position_clear()); + + WHEN("a z-hop is pending and we travel to the first point") { + writer.lazy_lift(LiftType::NormalLift); + const std::string gcode = writer.travel_to_xyz(Vec3d(10.0, 10.0, 5.0)); + + THEN("nothing is commanded at the image of the origin") { + // The destination maps to machine X = 250 - 10 = 240; the bogus + // origin lift would have mapped to machine X = 250. + REQUIRE(max_emitted_x(gcode) < bed_x - 1.0); + } + THEN("only the destination move is emitted") { + REQUIRE(count_g1(gcode) == 1); + } + } + } + + GIVEN("the same writer once its position is known") { + GCodeWriter writer; + configure_lift_writer(writer); + writer.set_axis_remap(6, 1, 2); + writer.set_build_volume_max(Vec3d(bed_x, 250.0, 250.0)); + writer.travel_to_xyz(Vec3d(20.0, 20.0, 5.0)); + REQUIRE(writer.is_current_position_clear()); + + WHEN("a z-hop is pending and we travel again") { + writer.lazy_lift(LiftType::NormalLift); + const std::string gcode = writer.travel_to_xyz(Vec3d(30.0, 30.0, 5.0)); + + THEN("the separate lift move is still emitted") { + // Suppression must be pinned to the unknown position, not to the + // presence of a remap. + REQUIRE(count_g1(gcode) == 2); + } + } + } + + GIVEN("an identity-mapping writer with an unknown position") { + GCodeWriter writer; + configure_lift_writer(writer); + REQUIRE_FALSE(writer.kinematics().must_emit_all_axes()); + REQUIRE_FALSE(writer.is_current_position_clear()); + + WHEN("a z-hop is pending and we travel to the first point") { + writer.lazy_lift(LiftType::NormalLift); + const std::string gcode = writer.travel_to_xyz(Vec3d(10.0, 10.0, 5.0)); + + THEN("behaviour is unchanged: the lift is still emitted") { + // Three moves, not two: with no remap and an unknown position the + // destination is emitted as a separate XY move followed by its own + // Z move, on top of the lift. That split is the pre-existing + // identity-mapping path and must not change. + REQUIRE(count_g1(gcode) == 3); + } + } + } +} + +SCENARIO("Axis remap: eager_lift does not lift, or record a lift, at an unknown position", + "[GCodeWriter][remap]") +{ + GIVEN("a writer with a reverse-X remap and an unknown current position") { + GCodeWriter writer; + configure_lift_writer(writer); + writer.set_axis_remap(6, 1, 2); + writer.set_build_volume_max(Vec3d(250.0, 250.0, 250.0)); + REQUIRE_FALSE(writer.is_current_position_clear()); + + WHEN("an eager lift is requested") { + const std::string lift = writer.eager_lift(LiftType::NormalLift); + + THEN("no move is emitted") { + REQUIRE(lift.empty()); + } + THEN("no lift is recorded, so unlift does not descend from it") { + // If m_lifted had been set while nothing was commanded, unlift() + // would emit a descent from a height the machine never reached. + REQUIRE(writer.unlift().empty()); + } + } + } + + GIVEN("an identity-mapping writer with an unknown position") { + GCodeWriter writer; + configure_lift_writer(writer); + + WHEN("an eager lift is requested") { + const std::string lift = writer.eager_lift(LiftType::NormalLift); + + THEN("behaviour is unchanged: the lift is emitted and can be undone") { + REQUIRE_FALSE(lift.empty()); + REQUIRE_FALSE(writer.unlift().empty()); + } + } + } +} + +// Bug 2. extrude_arc_to_xy() emits G2/G3 with logical X/Y and I/J and never +// consulted the mapping. An arc is only representable when logical X and Y reach +// the machine unchanged -- which is a narrower question than "is the remap the +// identity", because a mapping that only touches Z leaves every emitted word alone. +SCENARIO("Arc support is decided by whether the mapping leaves X and Y alone", "[GCodeWriter][remap]") +{ + GIVEN("a Cartesian writer") { + GCodeWriter writer; + + THEN("the identity mapping supports arcs") { + REQUIRE(writer.kinematics().supports_arc_moves()); + } + THEN("a Z-only negation still supports arcs") { + // (+X, +Y, -Z): non-identity, but X, Y, I and J are all untouched. + writer.set_axis_remap(0, 1, 5); + REQUIRE(writer.kinematics().must_emit_all_axes()); + REQUIRE(writer.kinematics().supports_arc_moves()); + } + THEN("a Z-only reversal still supports arcs") { + writer.set_axis_remap(0, 1, 8); + REQUIRE(writer.kinematics().supports_arc_moves()); + } + THEN("swapping X and Y does not support arcs") { + writer.set_axis_remap(1, 0, 2); + REQUIRE_FALSE(writer.kinematics().supports_arc_moves()); + } + THEN("the X-tilt style (x, z, y) remap does not support arcs") { + writer.set_axis_remap(0, 2, 1); + REQUIRE_FALSE(writer.kinematics().supports_arc_moves()); + } + } + + GIVEN("a belt writer") { + PrintConfig belt_config; + belt_config.belt_printer.value = true; + belt_config.belt_slice_rotation.value = BeltRotationAxis::X; + belt_config.belt_slice_rotation_angle.value = 45.0; + + GCodeWriter writer; + install_belt_kinematics(writer, belt_config); + + THEN("arcs are never supported, because the frame shears") { + REQUIRE_FALSE(writer.kinematics().supports_arc_moves()); + } + } +} + +SCENARIO("An unrepresentable arc degrades to its chord rather than emitting a wrong G2/G3", + "[GCodeWriter][remap]") +{ + auto emitted_commands = [](const std::string &gcode) { + std::vector cmds; + GCodeReader reader; + reader.parse_buffer(gcode, [&cmds](GCodeReader &, const GCodeReader::GCodeLine &line) { + if (! line.cmd().empty()) cmds.emplace_back(line.cmd()); + }); + return cmds; + }; + + GIVEN("an identity-mapping writer") { + GCodeWriter writer; + configure_lift_writer(writer); + + WHEN("an arc is extruded") { + const std::string gcode = writer.extrude_arc_to_xy( + Vec2d(10.0, 0.0), Vec2d(5.0, 0.0), 0.0, /*is_ccw=*/true, "", /*force_no_extrusion=*/true); + + THEN("it is still a G3") { + const auto cmds = emitted_commands(gcode); + REQUIRE(cmds.size() == 1); + REQUIRE(cmds.front() == "G3"); + } + } + } + + GIVEN("a writer whose mapping swaps X and Y") { + GCodeWriter writer; + configure_lift_writer(writer); + writer.set_axis_remap(1, 0, 2); + + WHEN("an arc is extruded") { + const std::string gcode = writer.extrude_arc_to_xy( + Vec2d(10.0, 0.0), Vec2d(5.0, 0.0), 0.0, /*is_ccw=*/true, "", /*force_no_extrusion=*/true); + + THEN("no arc is emitted; it is approximated with linear moves") { + const auto cmds = emitted_commands(gcode); + REQUIRE(! cmds.empty()); + for (const auto &c : cmds) + REQUIRE(c == "G1"); + } + } + } + + // The first version of this test used dE = 0 with force_no_extrusion, which + // hid a real bug: the capability check sat AFTER filament()->extrude(dE), so + // the fallback into extrude_to_xy() advanced E twice. Extrusion accounting has + // to be asserted with a positive dE. + GIVEN("a writer whose mapping cannot express arcs, extruding a real amount") { + GCodeWriter writer; + configure_lift_writer(writer); + writer.set_axis_remap(1, 0, 2); + const double dE = 1.5; + // used_filament() accumulates across moves; E() is reset per line in + // relative-E mode, so it would only show the last segment. + const double used_before = writer.filament()->used_filament(); + + WHEN("an arc carrying that extrusion is emitted") { + const std::string gcode = writer.extrude_arc_to_xy( + Vec2d(10.0, 0.0), Vec2d(5.0, 0.0), dE, /*is_ccw=*/true, "", /*force_no_extrusion=*/false); + + THEN("exactly dE is accounted for, not twice dE") { + REQUIRE_THAT(writer.filament()->used_filament() - used_before, + Catch::Matchers::WithinAbs(dE, 1e-6)); + } + THEN("no G2/G3 survives") { + REQUIRE(gcode.find("G2") == std::string::npos); + REQUIRE(gcode.find("G3") == std::string::npos); + } + } + } + + GIVEN("a writer whose mapping CAN express arcs, extruding a real amount") { + GCodeWriter writer; + configure_lift_writer(writer); + const double dE = 1.5; + // used_filament() accumulates across moves; E() is reset per line in + // relative-E mode, so it would only show the last segment. + const double used_before = writer.filament()->used_filament(); + + WHEN("an arc carrying that extrusion is emitted") { + const std::string gcode = writer.extrude_arc_to_xy( + Vec2d(10.0, 0.0), Vec2d(5.0, 0.0), dE, /*is_ccw=*/true, "", /*force_no_extrusion=*/false); + + THEN("it is still a single arc and accounts for dE once") { + REQUIRE(emitted_commands(gcode).size() == 1); + REQUIRE_THAT(writer.filament()->used_filament() - used_before, + Catch::Matchers::WithinAbs(dE, 1e-6)); + } + } + } +} From 767db71500ddd3098baf758adcf78a60bb0b4c9a Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Tue, 8 Sep 2026 23:58:47 -0500 Subject: [PATCH 108/224] Belt: fix three tree-support bugs, one of which blocked slicing entirely 1. Belt tree support could not slice at all. layer_initialize() hardcodes layer 0's bottom_z to 0, encoding "below layer 0 is the build plate at z = 0". True for a flat bed; false for a belt, whose virtual support layers legitimately extend below zero. The bottom-most belt layer therefore got height = print_z - 0 = -9.8, which reached Flow::with_height() and threw FlowErrorNegativeFlow. A 3DBenchy, a mushroom, an L-bracket and an extruded L all failed identically with negative flow / return -100. Only a bare cube sliced, because its support never reached that far down. The bottom is now taken from the previous layer's z, and only a layer 0 whose print_z is itself negative gets a synthesised bottom below it. Every non-negative print_z -- every non-belt configuration -- keeps exactly the previous 0, so this is behaviour-preserving off a belt by construction. An earlier form used min(0., layer_z(0) - layer_height), which regressed flat beds whenever the initial layer was thinner than the layer height. 3DBenchy on a 45-degree belt with organic tree support: fails to slice -> 247 support blocks / 168,596 extrusions. 2. Support generated against the belt, and against belt-tilted walls. A plain 20mm cube on a 45-degree belt generated 86 support blocks and 46,307 support extrusions. Three causes, all gated on the belt floor being active: a. The build-plate tilt compensation shifted the lower layer the wrong way. tan(build_plate_tilt_*) carries a magnitude but no direction, and the sign chosen moved the lower layer away from the newly appearing material rather than under it, doubling the mismatch. The shift now comes from belt_floor_shear_factor / belt_floor_from_axis, which carry sign and axis exactly. Non-belt tilted beds keep the previous behaviour. b. Material resting on the belt was treated as unsupported. The belt surface is now unioned into the effective lower layer, sampled at the bottom of the layer -- a layer meets the belt across its thickness and print_z is the top. The half-plane is clipped to the layer's bounding box first: unioning a +/-1000mm half-plane raw with 20mm-scale geometry put a huge dynamic range through Clipper and left intermittent artefacts every few layers. c. The object's first slice can be empty on a belt (the bottom vertex is a sub-extrudable sliver), leaving the layer above with an empty predecessor even though it rests on the belt. (b) already covers that per island. What did need fixing is sharp-tail detection, which tests each island against the raw lower slices; with an empty predecessor that test is trivially true and every belt-contact island read as a sharp tail. It now tests against the same effective lower layer. An earlier form instead skipped the whole layer when the point of get_extents(curr_polys) -- the bounding box of the union of every island -- nearest the belt was in contact. That was wrong in a way worth recording: one island resting on the belt could suppress overhang and sharp-tail detection for a separate island floating well above it. Every decision here is per-island. Cube on belt: 46,307 -> 0 support extrusions. Same cube non-belt: 0 before and after. Benchy on belt still 247 blocks / 168,596 extrusions and a mushroom 111 / 82,157, so false positives are removed without suppressing true ones. Non-belt is unchanged by measurement, not only by the belt_ovh_active gate: the same mushroom sliced on a Cartesian printer before and after gives 65,866 support extrusions and 68,717 total extrusions both times, the two G-code files differing in exactly one line -- the object's plate position. 3. m_anti_overhang was filled and read in different index spaces. It is consumed in the same index space as m_layer_outlines, where object layer i lives at num_raft_layers + i, but was filled in object-layer space. Every entry landed num_raft_layers too low (50 for a 20mm cube at bed Y=50) and the topmost object layers got none. The belt injection also ran before m_raft_layers was extended, so it could not have known the offset. The array is now shifted as a whole and the injection moved after the raft extension. This also repairs user support blockers under a raft, which is not belt-specific: it changes behaviour for any ordinary raft, not just the belt's virtual one, and should be reviewed as a general fix. Measured effect on the cube was small on its own (46,307 -> 46,334 before the other fixes) because m_anti_overhang only feeds calculate_placable; kept as a correctness fix on its own merits. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011jgzj1sf53KMLPweZ8yeUQ --- src/libslic3r/Support/TreeModelVolumes.cpp | 55 ++++++++------- src/libslic3r/Support/TreeSupport.cpp | 75 +++++++++++++++++++-- src/libslic3r/Support/TreeSupportCommon.hpp | 17 ++++- 3 files changed, 117 insertions(+), 30 deletions(-) diff --git a/src/libslic3r/Support/TreeModelVolumes.cpp b/src/libslic3r/Support/TreeModelVolumes.cpp index b1e3cdd6dd..e56682fd3c 100644 --- a/src/libslic3r/Support/TreeModelVolumes.cpp +++ b/src/libslic3r/Support/TreeModelVolumes.cpp @@ -95,29 +95,6 @@ TreeModelVolumes::TreeModelVolumes( #else { m_anti_overhang = print_object.slice_support_blockers(); - // Belt floor: add belt surface polygons to anti_overhang so support - // is never generated inside the belt. Only in global shear mode — - // in local mode the belt floor clipping handles everything and - // anti_overhang at the bottom layers would block all support. - { - 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) { - size_t num_layers_needed = print_object.layer_count(); - // Ensure m_anti_overhang is large enough. - if (m_anti_overhang.size() < num_layers_needed) - m_anti_overhang.resize(num_layers_needed, Polygons{}); - for (size_t layer_idx = 0; layer_idx < num_layers_needed; ++layer_idx) { - double print_z = print_object.get_layer(layer_idx)->print_z - - print_object.belt_global_z_offset(); - append(m_anti_overhang[layer_idx], ctx.surface_polygon(print_z)); - } - } - } TreeSupportMeshGroupSettings mesh_settings(print_object); const TreeSupportSettings config{ mesh_settings, print_object.slicing_parameters() }; m_current_min_xy_dist = config.xy_min_distance; @@ -147,6 +124,38 @@ TreeModelVolumes::TreeModelVolumes( } } } + // Belt floor: add belt surface polygons to anti_overhang so support is + // never generated inside the belt. + // + // 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. + { + 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; m_layer_outlines.emplace_back(mesh_settings, std::vector{}); diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 3b7743407c..d472681c41 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -13,6 +13,7 @@ #include "SVG.hpp" #include "TreeSupportCommon.hpp" #include "TreeSupport.hpp" +#include #include "TreeSupport3D.hpp" #include "BeltFloorContext.hpp" #include @@ -712,6 +713,17 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) const double tilt_x_rad = Geometry::deg2rad(print_cfg.build_plate_tilt_x.value); const double tilt_y_rad = Geometry::deg2rad(print_cfg.build_plate_tilt_y.value); const bool has_tilt = std::abs(tilt_x_rad) > EPSILON || std::abs(tilt_y_rad) > EPSILON; + + // Belt printers: the object is pre-rotated by the belt angle before slicing, so a wall + // that is vertical in the world advances by one layer height per layer in the sliced + // frame. The build-plate tilt shift above compensates for that, but its direction has to + // follow the belt shear -- the sign and axis are already known exactly from the slicing + // parameters, so take them from there rather than from tan(build_plate_tilt), which + // carries a magnitude but no direction. Non-belt tilted beds keep the existing behaviour. + BeltFloorContext ovh_belt_ctx; + const bool belt_ovh_active = ovh_belt_ctx.init(m_slicing_params, print_cfg); + const double belt_shear = ovh_belt_ctx.shear_factor(); + const int belt_axis = ovh_belt_ctx.from_axis(); // FIXME this is a fudge constant! double support_tree_tip_diameter = 0.8; auto enforcer_overhang_offset = scaled(support_tree_tip_diameter); @@ -855,15 +867,61 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) ExPolygons& lower_polys = lower_layer->lslices_extrudable; // Apply build plate tilt: shift lower layer polygons to simulate tilted gravity + // + // On a belt the object's very first slice can come out empty (the bottom + // vertex is a sub-extrudable sliver), leaving the layer above it with an + // empty predecessor even though it rests on the belt. That case needs no + // special handling here: the belt surface is unioned into effective_lower + // below and sampled at the bottom of the layer, so a contacting island is + // covered and a genuinely floating one still reports its overhang. Doing it + // that way keeps the decision per-island -- an earlier whole-layer skip, + // conditioned on the nearest point of the *union* of the cross-section, + // let one contacting island silence a separate floating one. ExPolygons shifted_lower; - if (has_tilt) { + if (belt_ovh_active || has_tilt) { shifted_lower = lower_polys; // copy const double lh = lower_layer->height; - Point tilt_shift(coord_t(scale_(lh * tan(tilt_y_rad))), - coord_t(scale_(lh * tan(tilt_x_rad)))); + Point tilt_shift(0, 0); + if (belt_ovh_active) { + // Advance the lower layer along the belt by exactly the amount a + // world-vertical wall moves per layer, so such a wall stops reading + // as an overhang. Sign comes from the shear, not from a tilt angle. + const coord_t d = coord_t(-scale_(lh * belt_shear)); + if (belt_axis == 0) tilt_shift.x() = d; else tilt_shift.y() = d; + } else { + tilt_shift = Point(coord_t(scale_(lh * tan(tilt_y_rad))), + coord_t(scale_(lh * tan(tilt_x_rad)))); + } translate(shifted_lower, tilt_shift); } - const ExPolygons &effective_lower = has_tilt ? shifted_lower : lower_polys; + ExPolygons effective_lower = (belt_ovh_active || has_tilt) ? shifted_lower : lower_polys; + + // Belt printers: material resting on the belt is held up by the belt, not by + // the layer below it, so the belt surface counts as support from underneath. + // Without this the object's belt-contact face reads as a fresh overhang on + // every layer -- the leading strip that produced the spurious support nub. + if (belt_ovh_active) { + // surface_polygon() is a +/-1000mm half-plane. Unioning that raw with + // 20mm-scale geometry and then offsetting it puts a huge dynamic range + // through Clipper, which left intermittent artefacts every few layers. + // Clip it to the layer's own bounding box first. + // Evaluate the belt surface at the BOTTOM of the layer, not its top: + // a layer meets the belt across its whole thickness, and print_z is the + // top. On the object's first layer -- which is thicker, and whose lower + // layer is empty -- using print_z left the leading 0.37mm uncovered and + // produced the one remaining spurious overhang. + Polygons belt_surface = ovh_belt_ctx.surface_polygon(layer->print_z - layer->height); + if (! belt_surface.empty()) { + BoundingBox clip_bb = get_extents(curr_polys); + clip_bb.merge(get_extents(lower_polys)); + clip_bb.offset(scale_(10.)); + belt_surface = intersection(belt_surface, Polygons{ clip_bb.polygon() }); + if (! belt_surface.empty()) { + append(effective_lower, union_ex(belt_surface)); + effective_lower = union_ex(effective_lower); + } + } + } // normal overhang ExPolygons lower_layer_offseted = offset_ex(effective_lower, support_offset_scaled, SUPPORT_SURFACES_OFFSET_PARAMETERS); @@ -882,8 +940,13 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) for (const ExPolygon& expoly : curr_polys) { bool is_sharp_tail = false; // 1. nothing below - // this is a sharp tail region if it's floating and non-ignorable - if (!overlaps(offset_ex(expoly, 0.1 * extrusion_width_scaled), lower_polys)) { + // this is a sharp tail region if it's floating and non-ignorable. + // On a belt, "below" has to include the belt itself and the + // shear-advanced lower layer, or every belt-contact island reads as + // a sharp tail -- which is what the empty-predecessor skip above was + // really masking. effective_lower is exactly that notion of below. + const ExPolygons &tail_lower = belt_ovh_active ? effective_lower : lower_polys; + if (!overlaps(offset_ex(expoly, 0.1 * extrusion_width_scaled), tail_lower)) { is_sharp_tail = !offset_ex(expoly, -0.1 * extrusion_width_scaled).empty(); } diff --git a/src/libslic3r/Support/TreeSupportCommon.hpp b/src/libslic3r/Support/TreeSupportCommon.hpp index 07dcb324b8..979adcd9ed 100644 --- a/src/libslic3r/Support/TreeSupportCommon.hpp +++ b/src/libslic3r/Support/TreeSupportCommon.hpp @@ -651,7 +651,22 @@ inline SupportGeneratorLayer& layer_initialize( const size_t layer_idx) { layer_new.print_z = layer_z(slicing_params, config, layer_idx); - layer_new.bottom_z = layer_idx > 0 ? layer_z(slicing_params, config, layer_idx - 1) : 0; + // Layer 0 has no layer below it, so its bottom is the build plate at z = 0 -- + // true for a flat bed, false for a belt, whose virtual support layers extend + // below zero. Taking 0 there made the bottom-most belt layer's height come out + // as its own (negative) print_z, which reached Flow::with_height() and threw + // FlowErrorNegativeFlow, so tree support could not slice any belt model whose + // branches reached down that far. + // + // Only the negative case is corrected. An earlier version used + // min(0, print_z - layer_height), which also fires whenever the initial layer + // is THINNER than the regular layer height -- e.g. 0.2 over 0.3, both + // independently configurable -- and silently changed flat-bed support layer + // heights. Keying on the sign leaves every non-negative print_z on exactly + // the previous value of 0. + layer_new.bottom_z = layer_idx > 0 ? layer_z(slicing_params, config, layer_idx - 1) : 0.; + if (layer_idx == 0 && layer_new.print_z < 0.) + layer_new.bottom_z = layer_new.print_z - slicing_params.layer_height; layer_new.height = layer_new.print_z - layer_new.bottom_z; return layer_new; } From 1b6fb2a81f2c14fc1ddce598d0a9dd2897d3ce9f Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Tue, 8 Sep 2026 23:00:38 -0500 Subject: [PATCH 109/224] Belt: fix first-layer speed and the slow_down_layers ramp never applying Fixes the report in #12998 (comment 5465250754): first-layer speed and the slow_down_layers ramp were ignored on a belt printer. The report reads as a per-object problem, but neither applied to *any* object -- the reporter's first part slowed down because slow_down_for_layer_cooling was on, which is CoolingBuffer's time-per-layer mechanism, not initial_layer_speed. FirstLayerPlane decides first-layer-ness by perpendicular distance to a plane it derives by composing gcode_remap_* with compute_machine_z_affine(). The plane is therefore a function of how G-code is *addressed*, not of where the belt is: change the output axis convention and the plane moves. On MCBELT-TYPE2 the first layer measured 86.2 mm from the plane and got effective index 431, far past any slow_down_layers ramp. on_first_layer(point) and effective_layer_index_for_point() now measure height above the belt surface, using the belt description already carried in SlicingParameters -- belt_floor_shear_factor / belt_floor_from_axis / belt_floor_z_shift -- the same description the support generator uses. That is a property of how the object was sliced, so no remap or back-transform can perturb it. Deliberately not via BeltFloorContext: its init() folds in belt_support_floor_offset, a support-generator diagnostic, and letting that option steer the model's first-layer speed band would be a surprising coupling (a negative value would switch the slowdown off outright). Preserving the existing first-layer-plane settings: * first_layer_plane XY/YZ/XZ keeps the FirstLayerPlane evaluator, as those are explicit opt-outs. * A non-zero first_layer_plane_offset also keeps it. The offset is a machine-Z shift that FirstLayerPlane converts into a perpendicular distance in the slicing frame; this evaluator measures along slicing Z, so there is no faithful translation. Deferring to the evaluator that implements the setting beats silently ignoring it. * The two thresholds stay separate, exactly as FirstLayerPlane keeps them: the first-layer boolean tests initial_layer_print_height, while the effective layer index counts bands of first_layer_plane_thickness. Brim and coincident apron bands are emitted before m_layer is switched to their object -- for an apron band there is no Layer at all -- so both paths publish the belt-floor owner explicitly. Without that a brim's classification would borrow whichever object was visited previously, making it depend on plate order. Note that first-layer-ness drives more than speed: extrusion acceleration, jerk, the first-layer flow ratio and eligibility for overhang speed/fan analysis all read it, so all of them are corrected on belt printers by this change. Classification still samples only each path's first point, as it did before. Non-belt is unaffected by construction: belt_height_above_floor() returns false when the belt floor is inactive and both call sites fall back to the previous path. FirstLayerPlane stays in place for its other modes and for CoolingBuffer, whose machine-coordinate probe is a separate outstanding bug. Measured, MCTEST4 on MCBELT-TYPE2 (initial_layer_speed=5, slow_down_layers=40): 15 distinct feedrates with no gradient and F300 absent, becomes 70 including the full ramp 300(5) 382(6) 465(8) 630(10) 795(13) ... Two bare cubes on a belt: 0 slow extrusions becomes 2378 across Z 32.36..95.18. The same two cubes on a Cartesian printer keep their slow extrusions confined to Z 0.20..2.00. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011jgzj1sf53KMLPweZ8yeUQ --- src/libslic3r/GCode.cpp | 50 +++++++++++++++++++++++++++++++++++++++++ src/libslic3r/GCode.hpp | 48 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 32976fad76..fff437731f 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -6,6 +6,7 @@ #include "libslic3r.h" #include "I18N.hpp" #include "GCode.hpp" +#include #include "Exception.hpp" #include "ExtrusionEntity.hpp" #include "EdgeGrid.hpp" @@ -5234,6 +5235,10 @@ std::string GCode::generate_object_brim(const Print &print, const PrintObject &o // geometry is already in plate coordinates. m_config.apply(print.default_region_config()); m_config.apply(object.config(), true); + // m_layer is not switched to this object until after brim emission, so name + // the belt-floor owner explicitly or the classification borrows whichever + // object was visited last. + BeltFloorObjectGuard floor_owner{ m_belt_floor_object, &object }; const Point &offset = object.instances()[instance_id].shift; this->set_origin(unscale(offset)); this->on_set_origin(&object, offset); @@ -6642,6 +6647,9 @@ LayerResult GCode::process_layer( // Speeds, flow and retraction all read m_config. m_config.apply(print.default_region_config()); m_config.apply(object.config(), true); + // Apron bands have no Layer at all (m_layer is null here), so the belt + // floor owner has to be named the same way the object brim names it. + BeltFloorObjectGuard floor_owner{ m_belt_floor_object, &object }; const size_t i_begin = single_object_instance_idx == size_t(-1) ? 0 : single_object_instance_idx; const size_t i_end = single_object_instance_idx == size_t(-1) ? object.instances().size() : single_object_instance_idx + 1; @@ -10307,6 +10315,48 @@ std::string GCode::set_object_info(Print *print) { return gcode.str(); } +bool GCode::belt_height_above_floor(const Vec3d &point_slicing_mm, double &height_mm) const +{ + // The owning object, which is what carries the belt description. During + // object-brim and coincident-apron emission m_layer still points at whichever + // object was visited last (or at nothing at all), so those paths publish the + // owner explicitly -- otherwise a brim's speed would depend on plate order. + const PrintObject *object = m_belt_floor_object != nullptr ? m_belt_floor_object + : (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 + // option move the model's first-layer speed band would be a surprising + // coupling -- a negative value would switch the slowdown off entirely. + // The belt surface itself is just shear * u + z_shift. + if (std::abs(sp.belt_floor_shear_factor) < EPSILON) + return false; + const double u = sp.belt_floor_from_axis == 0 ? point_slicing_mm.x() : point_slicing_mm.y(); + const double floor_z = sp.belt_floor_shear_factor * u + sp.belt_floor_z_shift; + // Measured along the slicing Z, not perpendicular to the belt: layers are + // horizontal slabs in the sliced frame, so the slab holding the material that + // rests on the belt at this point is the one within one layer height of it. + // A perpendicular measure would shrink the band by 1/cos(tilt). + height_mm = point_slicing_mm.z() - floor_z; + return true; +} + // convert a model-space scaled point into G-code coordinates Vec2d GCode::point_to_gcode(const Point &point) const { diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 26b8bc78a5..c78a9675e2 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -831,6 +831,18 @@ protected: // Counter standing in for Layer::id() on apron layers, which precede layer 0. size_t m_belt_brim_layer_idx{0}; + // Belt brim only. Brim and coincident apron bands are emitted before m_layer + // is switched to their object, so belt_height_above_floor() would otherwise + // read the previously visited object's belt description -- making a brim's + // classification depend on plate visiting order. Those paths publish the + // owner here for the duration of the emission. Never left set. + const PrintObject *m_belt_floor_object{nullptr}; + struct BeltFloorObjectGuard { + const PrintObject *&slot; + BeltFloorObjectGuard(const PrintObject *&s, const PrintObject *o) : slot(s) { slot = o; } + ~BeltFloorObjectGuard() { slot = nullptr; } + }; + std::set m_initial_layer_extruders; std::vector> m_sorted_layer_filaments; // BBS @@ -853,6 +865,12 @@ protected: // 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); @@ -863,10 +881,40 @@ protected: // perpendicular distance to the plane in band_thickness_mm units; // otherwise it returns 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. + 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; + return band > 0. ? band : 0.2; + } + + // Height of a slicing-frame point above the belt surface, or false when this + // is not a belt print. + // + // 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. + bool belt_height_above_floor(const Vec3d &point_slicing_mm, double &height_mm) const; int layer_id() const { if (m_layer == nullptr) return -1; From 1bcfe58064ea16387a3f4107d7ea782fc43c6e9a Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 12 Sep 2026 22:23:14 -0500 Subject: [PATCH 110/224] Belt: stop the purge prism printing plastic no toolchange needs Two independent leaks of filament on the belt purge prism, plus the replan safety net the second one needs. 1. The early-truncation scan bounded itself with the prism's own toolchanges. ToolOrdering covers the whole print and the prism is a printed object in it, so the "last toolchange" the scan found was on the prism's own top layers -- it runs past every model object by design -- and the truncation cancelled nothing. Bound the scan at the tallest non-prism object (support layers included; on a belt they can top the object). On MCTEST5 that was 197 toolchanges over 39.4 mm of tower that no swap ever needed. 2. On a layer with no toolchange, the prism's entire fill printed as solid infill in its own filament. Drop the fills no toolchange claimed, right after the purge marking and before ensure_perimeters_infills_order() force-overrides whatever is left. Perimeters stay so the bar keeps a continuous wall. An earlier version of this deleted the entities and had to be reverted: psWipeTower can rerun without regenerating infill, and a later tool ordering may claim what this one did not. The entities are now stashed with their layer, region and index and put back exactly, the same reversibility contract layer truncation already had. 3. Both stashes go stale if an object step reruns: make_fills() clears and regenerates fills over m_layers only, so a stale stash would put old fills back next to new ones, and truncated layers would keep old perimeters/fills. Undo the plan's edits at the top of Print::process() whenever psWipeTower is not done. Every object-step invalidation also invalidates psWipeTower, so that condition is exactly "some object step may rerun"; when it is done nothing regenerates and the edits must stay. This also covers a prism left behind after belt mode is turned off, which previously stayed truncated forever. WipingExtrusions::is_entity_overridden() becomes public so the prism can tell claimed fills from unclaimed ones. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01SsuY8Laiyh7q2zPVVKV3HZ --- src/libslic3r/BeltPurge.cpp | 109 +++++++++++++++++++++++++-- src/libslic3r/GCode/ToolOrdering.hpp | 8 +- src/libslic3r/Print.cpp | 13 ++++ src/libslic3r/Print.hpp | 32 ++++++++ src/libslic3r/PrintObject.cpp | 7 ++ 5 files changed, 162 insertions(+), 7 deletions(-) diff --git a/src/libslic3r/BeltPurge.cpp b/src/libslic3r/BeltPurge.cpp index b249f21bd2..37d89ca14e 100644 --- a/src/libslic3r/BeltPurge.cpp +++ b/src/libslic3r/BeltPurge.cpp @@ -149,7 +149,7 @@ void Print::_plan_belt_purge() // previous plan so a newly higher toolchange can use its original layers. for (PrintObject *po : m_objects) if (po->config().belt_purge_tower_object.value) - po->belt_restore_truncated_layers(); + po->belt_undo_purge_plan(); // Must run before ToolOrdering is built: LayerTools merge per-object layer // print_z values, and the prism only absorbs purge where its (snapped) @@ -191,11 +191,44 @@ void Print::_plan_belt_purge() // flush object), so truncating afterwards would leave dangling overrides // pointing into deleted layers. { + // The tool ordering covers the WHOLE print, and the prism is a printed + // object in it. Left unbounded, the scan below sees the prism's own + // toolchanges on layers above every model object -- the prism runs past + // them by design (ramp/height compensation at the tilted ends) -- so + // last_tc_z lands at the prism's own top and the truncation cancels + // nothing. The tower ends up justifying its own existence. + // + // Nothing above the tallest printed object can require a color change, + // so bound the scan there. On MCTEST5 that is 197 toolchanges spanning + // z=154.00..193.20 with the tallest object topping out at 153.80, i.e. + // 39.4 mm of tower that no swap ever needed. + // Support layers count too: on a belt they can extend above the object's + // own top, and a toolchange there is a real one. + double obj_top_z = -1.; + for (const PrintObject *po : m_objects) { + if (po->config().belt_purge_tower_object.value) + continue; + if (!po->layers().empty()) + obj_top_z = std::max(obj_top_z, po->layers().back()->print_z); + if (!po->support_layers().empty()) + obj_top_z = std::max(obj_top_z, po->support_layers().back()->print_z); + } + double last_tc_z = -1.; unsigned int cur_ext = m_wipe_tower_data.tool_ordering.first_extruder(); - for (const auto < : m_wipe_tower_data.tool_ordering.layer_tools()) + for (const auto < : m_wipe_tower_data.tool_ordering.layer_tools()) { + // layer_tools() is ordered by print_z ascending. + if (obj_top_z >= 0. && lt.print_z > obj_top_z + EPSILON) + break; for (const unsigned int e : lt.extruders) if (e != cur_ext) { last_tc_z = lt.print_z; cur_ext = e; } + } + // Deliberately NOT cancelling the prism outright when no object toolchange + // exists: belt_truncate_layers_above(0.) empties m_layers, and an object + // with zero layers is not something the rest of the pipeline expects. The + // GUI already declines to create a prism unless more than one filament is + // in use, so this case is a stale prism, not a hot path -- leave it whole + // rather than risk a zero-layer object. if (last_tc_z >= 0.) for (PrintObject *po : m_objects) if (po->config().belt_purge_tower_object.value && !po->layers().empty()) { @@ -251,9 +284,25 @@ void Print::_plan_belt_purge() current_extruder_id = extruder_id; } - // Do not destructively remove unclaimed fill entities here. psWipeTower - // can rerun without regenerating infill, and a later tool ordering may - // need entities that were unclaimed by the previous plan. + // Plastic saving: drop the prism's fills that no toolchange on this layer + // claimed. At this point the prism's OVERRIDDEN fills are exactly the + // purge; the rest would print as solid infill in the prism's own filament + // for nothing -- which is the whole prism on a layer with no toolchange + // (141 of 692 layers on MCTEST5 before the truncation fix). Perimeters are + // left alone so the bar keeps a continuous wall along the belt. + // + // Non-destructive: the entities are stashed with their positions and put + // back by belt_restore_dropped_fills() at the top of the next plan. An + // earlier version deleted them outright, which broke replanning when a + // later tool ordering needed what this one had not claimed -- that is why + // it was removed rather than kept. + if (prism_po != nullptr) { + const auto &we = layer_tools.wiping_extrusions(); + prism_po->belt_drop_unclaimed_fills( + prism_po->get_layer_at_printz(layer_tools.print_z, EPSILON), + [&we, prism_po](const ExtrusionEntity *e) { return we.is_entity_overridden(e, prism_po, 0); }); + } + layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this); if (layer_leftover > 0.f) { total_leftover += layer_leftover; @@ -330,6 +379,56 @@ size_t PrintObject::belt_truncate_layers_above(coordf_t z) return removed; } +// Plastic saving on the purge prism: keep only the fills a toolchange claimed. +// +// Called per layer from _plan_belt_purge(), after the real-purge marking and +// BEFORE ensure_perimeters_infills_order() -- that pass force-overrides every +// remaining fill on the prism (it is a dedicated flush object), so afterwards +// everything looks claimed and nothing could be distinguished. +size_t PrintObject::belt_drop_unclaimed_fills(Layer *layer, const std::function &claimed) +{ + if (layer == nullptr) + return 0; + size_t dropped = 0; + for (size_t ri = 0; ri < layer->regions().size(); ++ri) { + LayerRegion *lr = layer->get_region(ri); + auto &ents = lr->fills.entities; + ExtrusionEntitiesPtr keep; + keep.reserve(ents.size()); + for (size_t i = 0; i < ents.size(); ++i) { + if (claimed(ents[i])) { + keep.emplace_back(ents[i]); + } else { + // Stash with its original index so the restore is exact. + m_belt_dropped_fills.push_back(BeltDroppedFill{ layer, ri, i, ents[i] }); + ++dropped; + } + } + ents = std::move(keep); + } + return dropped; +} + +void PrintObject::belt_restore_dropped_fills() +{ + if (m_belt_dropped_fills.empty()) + return; + // Ascending index per (layer, region): inserting in that order lands every + // entity back at its original position, because each insertion shifts only + // the entries after it, which are themselves still to be inserted. + std::stable_sort(m_belt_dropped_fills.begin(), m_belt_dropped_fills.end(), + [](const BeltDroppedFill &a, const BeltDroppedFill &b) { + if (a.layer != b.layer) return a.layer < b.layer; + if (a.region_idx != b.region_idx) return a.region_idx < b.region_idx; + return a.index < b.index; + }); + for (const BeltDroppedFill &d : m_belt_dropped_fills) { + auto &ents = d.layer->get_region(d.region_idx)->fills.entities; + ents.insert(ents.begin() + std::min(d.index, ents.size()), d.entity); + } + m_belt_dropped_fills.clear(); +} + void PrintObject::belt_restore_truncated_layers() { if (m_belt_truncated_layers.empty()) diff --git a/src/libslic3r/GCode/ToolOrdering.hpp b/src/libslic3r/GCode/ToolOrdering.hpp index f38cb4bdc8..9ccf6e90fb 100644 --- a/src/libslic3r/GCode/ToolOrdering.hpp +++ b/src/libslic3r/GCode/ToolOrdering.hpp @@ -74,13 +74,17 @@ public: void set_layer_tools_ptr(const LayerTools* lt) { m_layer_tools = lt; } -private: - // Returns true if entity is not printed with its usual extruder for a given copy. + // Returns true if entity is not printed with its usual extruder for a given + // copy -- i.e. it was claimed as a wiping/purge extrusion. Public because the + // belt purge prism uses it to tell which of its fills actually carry purge + // from the ones that are unclaimed waste (Print::_plan_belt_purge()). bool is_entity_overridden(const ExtrusionEntity* entity, const PrintObject *object, size_t copy_id) const { auto it = entity_map.find(std::make_tuple(entity, object)); return it != entity_map.end() && copy_id < it->second.size() && it->second[copy_id] != -1; } +private: + int first_nonsoluble_extruder_on_layer(const PrintConfig& print_config) const; int last_nonsoluble_extruder_on_layer(const PrintConfig& print_config) const; diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 86272e612c..1fc92aa9fc 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -2490,6 +2490,19 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) if (m_objects.empty()) return; + // Belt purge prism: _plan_belt_purge() (psWipeTower) truncates the prism's + // layers and drops its unclaimed fills, stashing both so a replan can undo + // them. The object steps below regenerate per-layer content over m_layers + // ONLY, so if any of them is about to rerun the stashes must go back first; + // otherwise truncated layers keep stale perimeters/fills and dropped fills + // are re-inserted next to freshly generated ones. Every object-step + // invalidation also invalidates psWipeTower, so "psWipeTower not done" is + // exactly "some object step may rerun" -- and when it IS done nothing below + // regenerates, and the plan's edits have to stay. + if (!this->is_step_done(psWipeTower)) + for (PrintObject *obj : m_objects) + obj->belt_undo_purge_plan(); + for (PrintObject *obj : m_objects) obj->clear_shared_object(); diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index f3da337bb7..638bb93105 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -582,6 +582,28 @@ private: // Wipe-tower-only invalidations do not necessarily reslice the object, so // truncation must be reversible when later toolchanges move upward. void belt_restore_truncated_layers(); + // Belt purge prism, plastic saving: drop the fills on one layer that no + // toolchange claimed. `claimed` reports whether an entity was overridden as + // purge; everything else on that layer would otherwise print as solid infill + // in the prism's own filament for nothing. Perimeters are never touched, so + // the bar keeps a continuous wall along the belt. + // + // Entities are STASHED, not deleted, with their original positions -- the + // same reversibility contract belt_truncate_layers_above() has, and the + // reason the original version of this had to be removed: psWipeTower can + // rerun without regenerating infill, and a later tool ordering may claim what + // this one did not. Returns the number of entities dropped. + size_t belt_drop_unclaimed_fills(Layer *layer, const std::function &claimed); + // Put every stashed fill back at its original index. Must run before a replan. + void belt_restore_dropped_fills(); + // Undo every edit _plan_belt_purge() made to this object's layers, leaving + // m_layers exactly as the object steps produced it. Fills first: they point + // into layers that are still live, and truncated layers were stashed whole + // with their own fills untouched, so the two stashes never share an entity. + // Print::process() calls this before any object step may rerun (those steps + // regenerate per-layer content over m_layers only, so a stale stash would + // otherwise be restored on top of fresh content); the plan calls it too. + void belt_undo_purge_plan() { belt_restore_dropped_fills(); belt_restore_truncated_layers(); } //BBS ExPolygons _shrink_contour_holes(double contour_delta, double hole_delta, const ExPolygons& polys) const; // BBS @@ -625,6 +647,16 @@ private: SlicingParameters m_slicing_params; LayerPtrs m_layers; LayerPtrs m_belt_truncated_layers; + // Fills removed by belt_drop_unclaimed_fills(), owned by this vector until + // restored or until clear_layers() deletes them. An entity is in exactly one + // of the live collection or this stash, never both. + struct BeltDroppedFill { + Layer *layer { nullptr }; + size_t region_idx { 0 }; + size_t index { 0 }; // position in the original fills.entities + ExtrusionEntity *entity { nullptr }; + }; + std::vector m_belt_dropped_fills; SupportLayerPtrs m_support_layers; // Belt brim, generated in posSupportMaterial by BeltBrim.cpp. Object-local // slicing frame, one entry per object layer plus a prologue of brim-only diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 6d9bfba090..2f02588d37 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1119,6 +1119,13 @@ void PrintObject::clear_layers() for (Layer *l : m_belt_truncated_layers) delete l; m_belt_truncated_layers.clear(); + // Fills dropped for plastic saving are owned by the stash while they sit + // outside their layer's collection, so they are freed here too. Order + // matters only in that these point at layers deleted just above, and we + // never dereference the layer -- just the entity. + for (const BeltDroppedFill &d : m_belt_dropped_fills) + delete d.entity; + m_belt_dropped_fills.clear(); } } From a43043069075b56c4a1bcbdc9d8b22217b3eb936 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 12 Sep 2026 22:23:14 -0500 Subject: [PATCH 111/224] Belt: size the purge prism against physical filaments, not mixed slots A mixed filament slot is virtual: ToolOrdering::resolve_mixed_filaments() replaces it with its physical components before any G-code is emitted, so the toolchanges the prism has to absorb are between those components. ensure_belt_purge_tower() counted the slot as a filament of its own, provisioning one island per mixed slot that no swap can ever reach -- the "extra purge tower" on MCTEST5, where filament 5 is a 50/50 blend of 2 and 4 and the G-code reports 0.00 g of it used. Expand the assigned set with the same expand_mixed_filaments() the backend uses, so the GUI sizes the prism against the filament set the slicer actually produces. No-op when nothing is mixed. Test covers the MCTEST5 shape, a mixed slot whose components are otherwise unused, and the no-mixing case. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01SsuY8Laiyh7q2zPVVKV3HZ --- src/slic3r/GUI/BeltPurgeTower.cpp | 34 +++++++++++++++++++ tests/libslic3r/test_filament_mixer.cpp | 44 +++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/src/slic3r/GUI/BeltPurgeTower.cpp b/src/slic3r/GUI/BeltPurgeTower.cpp index 8c5764072e..1882363d27 100644 --- a/src/slic3r/GUI/BeltPurgeTower.cpp +++ b/src/slic3r/GUI/BeltPurgeTower.cpp @@ -9,6 +9,7 @@ #include "libslic3r/Preset.hpp" #include "libslic3r/PresetBundle.hpp" #include "libslic3r/PrintConfig.hpp" +#include "libslic3r/FilamentMixer.hpp" #include "libslic3r/TriangleMesh.hpp" #include "libslic3r/Geometry.hpp" #include "libslic3r/BoundingBox.hpp" @@ -131,6 +132,39 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object } } + // A mixed filament slot is VIRTUAL: it never reaches a nozzle. At slice time + // ToolOrdering::resolve_mixed_filaments() replaces it with its physical + // components, so the toolchanges the prism has to absorb are between those + // components, not to the mixed slot itself. Counting the slot as a filament + // of its own therefore over-provisions the prism by one island per mixed slot + // -- the "extra purge tower" -- and, when every component is already used by + // another object, by an island that can never be reached at all. + // + // Expand here with the same helper the backend uses (Print.cpp's sequential + // path), so the GUI sizes the prism against the same filament set the slicer + // will actually produce. No-op when no filament is mixed. + { + // Copies, not references: a ternary with an empty-vector fallback would bind + // a reference to a temporary. + std::vector is_mixed; + std::vector comp_strs; + if (const auto *o = full_cfg.option("filament_is_mixed")) + is_mixed = o->values; + if (const auto *o = full_cfg.option("filament_mixed_components")) + comp_strs = o->values; + if (has_any_mixed_filament(is_mixed)) { + std::vector zero_based; + zero_based.reserve(filaments.size()); + for (int f : filaments) + if (f > 0) + zero_based.push_back((unsigned int) (f - 1)); + zero_based = expand_mixed_filaments(zero_based, is_mixed, comp_strs); + filaments.clear(); + for (unsigned int f : zero_based) + filaments.insert((int) f + 1); + } + } + const bool wanted = belt && prime_tower_enabled && !by_object && have_objects && filaments.size() > 1; if (!wanted) { sig = BeltPurgeSignature{}; diff --git a/tests/libslic3r/test_filament_mixer.cpp b/tests/libslic3r/test_filament_mixer.cpp index ade0c910dc..31bcb09526 100644 --- a/tests/libslic3r/test_filament_mixer.cpp +++ b/tests/libslic3r/test_filament_mixer.cpp @@ -57,6 +57,50 @@ TEST_CASE("expand_mixed_filaments replaces mixed slots with their components", " } } +TEST_CASE("belt purge tower island count ignores virtual mixed slots", "[FilamentMixer][belt]") +{ + // Regression for the "extra purge tower" on a belt printer with a mixed + // filament (MCTEST5). The belt purge prism is sized as + // n_islands = used_filaments.size() - 1 + // and GUI::ensure_belt_purge_tower() collected those filaments straight off + // the model objects' extruder assignments. A mixed slot is VIRTUAL -- no + // nozzle carries it, and ToolOrdering::resolve_mixed_filaments() replaces it + // with its components before any G-code is emitted -- so counting it as a + // filament of its own provisions one island that can never be reached. + // + // MCTEST5: five cubes on extruders 1..5, where filament 5 is a 50/50 blend of + // filaments 2 and 4. The G-code uses only T0..T3 and reports + // "filament used [g] = 53.35, 141.11, 40.84, 107.23, 0.00" -- filament 5 + // consumes nothing, exactly as a virtual slot should. + const std::vector is_mixed = {0, 0, 0, 0, 1}; + const std::vector comp_strs = {"", "", "", "", "2,4"}; + + // The set the sizer used to see: slots 0..4 (filaments 1..5). + const std::vector assigned = {0, 1, 2, 3, 4}; + const auto physical = expand_mixed_filaments(assigned, is_mixed, comp_strs); + + // Slot 4 dissolves into 1 and 3, which are already present. + REQUIRE(physical == std::vector({0, 1, 2, 3})); + + // Four physical filaments => three transitions => three islands, not four. + REQUIRE(int(physical.size()) - 1 == 3); + REQUIRE(int(assigned.size()) - 1 == 4); // what it produced before the fix + + SECTION("A mixed slot whose components are otherwise unused still counts them") { + // Only the mixed slot is assigned: it must still yield its two components, + // i.e. one island, rather than collapsing to zero. + const auto only_mixed = expand_mixed_filaments({4}, is_mixed, comp_strs); + REQUIRE(only_mixed == std::vector({1, 3})); + REQUIRE(int(only_mixed.size()) - 1 == 1); + } + + SECTION("No mixed filaments anywhere leaves the set untouched") { + const std::vector none_mixed = {0, 0, 0, 0, 0}; + REQUIRE_FALSE(has_any_mixed_filament(none_mixed)); + REQUIRE(expand_mixed_filaments(assigned, none_mixed, {"", "", "", "", ""}) == assigned); + } +} + TEST_CASE("check_mixed_filament_integrity flags dangling component references", "[FilamentMixer]") { const std::vector is_mixed = {0, 0, 1}; From 9c83631d2061091eab5c2a0d70607a2195071ba8 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 12 Sep 2026 22:23:14 -0500 Subject: [PATCH 112/224] TreeSupport: drop the include left over from removed debug output Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01SsuY8Laiyh7q2zPVVKV3HZ --- src/libslic3r/Support/TreeSupport.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index d472681c41..0b8eed9f6b 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -13,7 +13,6 @@ #include "SVG.hpp" #include "TreeSupportCommon.hpp" #include "TreeSupport.hpp" -#include #include "TreeSupport3D.hpp" #include "BeltFloorContext.hpp" #include From 6873267a9cb79f0829048983f662a1fedf54d6a7 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 15:56:00 +0800 Subject: [PATCH 113/224] Remove Plate Tilt Keys from Per-Object Settings Tables build_plate_tilt_x/y are printer-preset keys; listing them in the per-object frequent-settings and object-table bundles stored ignored values in object configs and crashed the object table on the process config lookup. --- src/slic3r/GUI/GUI_Factories.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index 30fdfc0d04..6a47d5ad2f 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -63,7 +63,6 @@ static SettingsFactory::Bundle FREQ_SETTINGS_BUNDLE_FFF = { L("Support") , { "enable_support", "support_type", "support_threshold_angle", "support_threshold_overlap", "support_base_pattern", "support_on_build_plate_only","support_critical_regions_only", "support_remove_small_overhang", - "build_plate_tilt_x", "build_plate_tilt_y", "support_base_pattern_spacing", "support_expansion"}}, //BBS { L("Flush options") , { "flush_into_infill", "flush_into_objects", "flush_into_support"} } @@ -93,8 +92,7 @@ std::map> SettingsFactory::OBJECT_C {"support_bottom_z_distance", "",22},{"support_top_z_distance", "",23},{"support_base_pattern", "",24},{"support_base_pattern_spacing", "",25}, {"support_interface_top_layers", "",26},{"support_interface_bottom_layers", "",27},{"support_interface_spacing", "",28},{"support_bottom_interface_spacing", "",29}, {"support_object_xy_distance", "",30}, {"bridge_no_support", "",31},{"max_bridge_length", "",32},{"support_critical_regions_only", "",33},{"support_remove_small_overhang","",34}, - {"build_plate_tilt_x","",35},{"build_plate_tilt_y","",36}, - {"support_object_first_layer_gap","",37} + {"support_object_first_layer_gap","",35} }}, { L("Speed"), {{"support_speed", "",12}, {"support_interface_speed", "",13} }} From 9cca3093ee960ad6772cdb025ac5a9b9a7d2e504 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 15:59:19 +0800 Subject: [PATCH 114/224] Keep the Plate Offset When Swapping in the Belt Writer The belt writer replaced the plate-offset-carrying writer mid-export, so belt G-code for any plate but the first kept the plate origin and long-travel clipping used the wrong frame. GCode now remembers the offset and hands it to the new writer, and the writer's first-layer probes use the plate-local point it emits. --- src/libslic3r/BeltGCode.cpp | 1 + src/libslic3r/BeltGCodeWriter.cpp | 7 ++++--- src/libslic3r/GCode.hpp | 4 +++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/libslic3r/BeltGCode.cpp b/src/libslic3r/BeltGCode.cpp index 3ba2f79e55..b6108ea78e 100644 --- a/src/libslic3r/BeltGCode.cpp +++ b/src/libslic3r/BeltGCode.cpp @@ -15,6 +15,7 @@ void BeltGCode::init_belt_writer(Print &print, bool is_bbl_printers) // Axis remap and build volume max are set by base GCode after init_belt_writer returns. belt_writer->set_belt_back_transform(print.config()); belt_writer->set_machine_frame_transform(print.config()); + belt_writer->set_xy_offset(m_gcode_offset.x(), m_gcode_offset.y()); m_writer = std::move(belt_writer); } diff --git a/src/libslic3r/BeltGCodeWriter.cpp b/src/libslic3r/BeltGCodeWriter.cpp index 4824d76f69..a1902f2db4 100644 --- a/src/libslic3r/BeltGCodeWriter.cpp +++ b/src/libslic3r/BeltGCodeWriter.cpp @@ -85,7 +85,7 @@ std::string BeltGCodeWriter::travel_to_xy(const Vec2d &point, const std::string w.emit_xyz(machine); const bool first_layer_for_point = belt_point_on_first_layer( m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer, - Vec3d(point.x(), point.y(), m_pos.z())); + Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); auto speed = first_layer_for_point ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) : this->config.travel_speed.get_at(m_cached_extruder_idx); @@ -115,7 +115,7 @@ std::string BeltGCodeWriter::_travel_to_z(double z, const std::string &comment) if (speed == 0.) { const bool first_layer_for_point = belt_point_on_first_layer( m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer, - Vec3d(m_pos.x(), m_pos.y(), z)); + Vec3d(m_pos.x() - m_x_offset, m_pos.y() - m_y_offset, z)); speed = first_layer_for_point ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) : this->config.travel_speed.get_at(m_cached_extruder_idx); } @@ -181,7 +181,8 @@ std::string BeltGCodeWriter::travel_to_xyz(const Vec3d &point, const std::string Vec3d dest_point = point; const bool first_layer_for_point = belt_point_on_first_layer( - m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer, point); + m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer, + Vec3d(point.x() - m_x_offset, point.y() - m_y_offset, point.z())); auto travel_speed = first_layer_for_point ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) : this->config.travel_speed.get_at(m_cached_extruder_idx); diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 17a7ce4608..34b0774b34 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -228,7 +228,7 @@ public: void do_export(Print* print, const char* path, GCodeProcessorResult* result = nullptr, ThumbnailsGeneratorCallback thumbnail_cb = nullptr); void export_layer_filaments(GCodeProcessorResult* result); //BBS: set offset for gcode writer - void set_gcode_offset(double x, double y) { m_writer->set_xy_offset(x, y); m_processor.set_xy_offset(x, y);} + void set_gcode_offset(double x, double y) { m_gcode_offset = Vec2d(x, y); m_writer->set_xy_offset(x, y); m_processor.set_xy_offset(x, y);} // Exported for the helper classes (OozePrevention, Wipe) and for the Perl binding for unit tests. const Vec2d& origin() const { return m_origin; } @@ -767,6 +767,8 @@ protected: // 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 m_first_layer_plane; + // Plate origin, kept so a writer replaced during export can be given it again. + Vec2d m_gcode_offset{ Vec2d::Zero() }; std::unique_ptr m_pressure_equalizer; From 509283100709d8653e801a0e95331a50b8ab613b Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Sun, 13 Sep 2026 16:28:09 +0800 Subject: [PATCH 115/224] Apply the Belt Slicing Transform to Painted Supports and Seams Painted support/seam facets, support volumes, seam occlusion, MMU and fuzzy skin painting (top/bottom and side facets) and the adaptive infill octree used trafo_centered(), or trafo() with a centre-offset shift, while the layers were sliced with the belt rotation, remap and Z lift; they now share PrintObject::trafo_sliced(). --- src/libslic3r/GCode/SeamPlacer.cpp | 4 ++-- src/libslic3r/MultiMaterialSegmentation.cpp | 11 ++++++----- src/libslic3r/Print.hpp | 3 +++ src/libslic3r/PrintObject.cpp | 10 +++++++--- src/libslic3r/PrintObjectSlice.cpp | 9 ++++++++- 5 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/libslic3r/GCode/SeamPlacer.cpp b/src/libslic3r/GCode/SeamPlacer.cpp index 35bc436d5a..7562c7745d 100644 --- a/src/libslic3r/GCode/SeamPlacer.cpp +++ b/src/libslic3r/GCode/SeamPlacer.cpp @@ -627,7 +627,7 @@ void compute_global_occlusion(GlobalModelInfo &result, const PrintObject *po, SeamPosition seam_position = spAligned) { BOOST_LOG_TRIVIAL(debug) << "SeamPlacer: gather occlusion meshes: start"; - auto obj_transform = po->trafo_centered(); + auto obj_transform = po->trafo_sliced(); indexed_triangle_set triangle_set; indexed_triangle_set negative_volumes_set; //add all parts @@ -712,7 +712,7 @@ void gather_enforcers_blockers(GlobalModelInfo &result, const PrintObject *po) { BOOST_LOG_TRIVIAL(debug) << "SeamPlacer: build AABB trees for raycasting enforcers/blockers: start"; - auto obj_transform = po->trafo_centered(); + auto obj_transform = po->trafo_sliced(); for (const ModelVolume *mv : po->model_object()->volumes) { if (mv->is_seam_painted()) { diff --git a/src/libslic3r/MultiMaterialSegmentation.cpp b/src/libslic3r/MultiMaterialSegmentation.cpp index 6f80f7b759..0016d96ac2 100644 --- a/src/libslic3r/MultiMaterialSegmentation.cpp +++ b/src/libslic3r/MultiMaterialSegmentation.cpp @@ -1215,7 +1215,7 @@ static inline std::vector> segmentation_top_and_bottom_l // project downards pointing painted triangles over bottom surfaces. std::vector> top_raw(num_facets_states), bottom_raw(num_facets_states); std::vector zs = zs_from_layers(layers); - Transform3d object_trafo = print_object.trafo_centered(); + Transform3d object_trafo = print_object.trafo_sliced(); #ifdef MM_SEGMENTATION_DEBUG_TOP_BOTTOM static int iRun = 0; @@ -2039,17 +2039,19 @@ std::vector> segmentation_by_painting(const PrintObject } BOOST_LOG_TRIVIAL(debug) << "Print object segmentation - Projection of painted triangles - Begin"; + // The layers were sliced in this frame (belt rotation, remap and Z lift included), and it already centers the object. + const Transform3d object_trafo = print_object.trafo_sliced(); for (const ModelVolume *mv : print_object.model_object()->volumes) { const ModelVolumeFacetsInfo facets_info = extract_facets_info(*mv); - tbb::parallel_for(tbb::blocked_range(1, num_facets_states), [&mv, &print_object, &facets_info, &layers, &edge_grids, &painted_lines, &painted_lines_mutex, &input_expolygons, &throw_on_cancel_callback](const tbb::blocked_range &range) { + tbb::parallel_for(tbb::blocked_range(1, num_facets_states), [&mv, &object_trafo, &facets_info, &layers, &edge_grids, &painted_lines, &painted_lines_mutex, &input_expolygons, &throw_on_cancel_callback](const tbb::blocked_range &range) { for (size_t extruder_idx = range.begin(); extruder_idx < range.end(); ++extruder_idx) { throw_on_cancel_callback(); const indexed_triangle_set custom_facets = facets_info.facets_annotation.get_facets(*mv, EnforcerBlockerType(extruder_idx)); if (!mv->is_model_part() || custom_facets.indices.empty()) continue; - const Transform3f tr = print_object.trafo().cast() * mv->get_matrix().cast(); - tbb::parallel_for(tbb::blocked_range(0, custom_facets.indices.size()), [&tr, &custom_facets, &print_object, &layers, &edge_grids, &input_expolygons, &painted_lines, &painted_lines_mutex, &extruder_idx](const tbb::blocked_range &range) { + const Transform3f tr = (object_trafo * mv->get_matrix()).cast(); + tbb::parallel_for(tbb::blocked_range(0, custom_facets.indices.size()), [&tr, &custom_facets, &layers, &edge_grids, &input_expolygons, &painted_lines, &painted_lines_mutex, &extruder_idx](const tbb::blocked_range &range) { for (size_t facet_idx = range.begin(); facet_idx < range.end(); ++facet_idx) { float min_z = std::numeric_limits::max(); float max_z = std::numeric_limits::lowest(); @@ -2102,7 +2104,6 @@ std::vector> segmentation_by_painting(const PrintObject Line line_to_test(Point(scale_(line_start_f.x()), scale_(line_start_f.y())), Point(scale_(line_end_f.x()), scale_(line_end_f.y()))); - line_to_test.translate(-print_object.center_offset()); // BoundingBoxes for EdgeGrids are computed from printable regions. It is possible that the painted line (line_to_test) could // be outside EdgeGrid's BoundingBox, for example, when the negative volume is used on the painted area (GH #7618). diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index c387dcd552..a801e0688f 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -359,6 +359,9 @@ public: // Trafo with the center_offset() applied after the transformation, to center the object in XY before slicing. Transform3d trafo_centered() const { Transform3d t = this->trafo(); t.pretranslate(Vec3d(- unscale(m_center_offset.x()), - unscale(m_center_offset.y()), 0)); return t; } + // trafo_centered() with the belt pre-slice transforms applied: the frame the layers were sliced in (Layer::slice_z). + // Equal to trafo_centered() unless a belt rotation or pre-slice remap is active. + Transform3d trafo_sliced() const; const PrintInstances& instances() const { return m_instances; } PrintInstances &instances() { return m_instances; } diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index ac7f34baa7..275998018d 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1101,7 +1101,10 @@ std::pair PrintObject::prepare indexed_triangle_set mesh = this->model_object()->raw_indexed_triangle_set(); // Rotate mesh and build octree on it with axis-aligned (standart base) cubes. auto to_octree = transform_to_octree().toRotationMatrix(); - its_transform(mesh, to_octree * this->trafo_centered(), true); + // Overhangs below are placed at Layer::bottom_z(), which includes the belt global Z offset. + Transform3d object_trafo = this->trafo_sliced(); + object_trafo.translation().z() += m_belt_global_z_offset; + its_transform(mesh, to_octree * object_trafo, true); // Triangulate internal bridging surfaces. std::vector> overhangs(std::max(surfaces_w_bottom_z.size(), size_t(1))); @@ -5124,6 +5127,7 @@ static void project_triangles_to_slabs(ConstLayerPtrsAdaptor layers, const index void PrintObject::project_and_append_custom_facets( bool seam, EnforcerBlockerType type, std::vector& out, std::vector>* vertical_points) const { + const Transform3d object_trafo = this->trafo_sliced(); for (const ModelVolume* mv : this->model_object()->volumes) if (mv->is_model_part()) { const indexed_triangle_set custom_facets = seam @@ -5132,12 +5136,12 @@ void PrintObject::project_and_append_custom_facets( if (! custom_facets.indices.empty()) { if (seam) project_triangles_to_slabs(this->layers(), custom_facets, - (this->trafo_centered() * mv->get_matrix()).cast(), + (object_trafo * mv->get_matrix()).cast(), seam, out); else { std::vector projected; // Support blockers or enforcers. Project downward facing painted areas upwards to their respective slicing plane. - slice_mesh_slabs(custom_facets, zs_from_layers(this->layers()), this->trafo_centered() * mv->get_matrix(), nullptr, &projected, vertical_points, [](){}); + slice_mesh_slabs(custom_facets, zs_from_layers(this->layers()), object_trafo * mv->get_matrix(), nullptr, &projected, vertical_points, [](){}); // Merge these projections with the output, layer by layer. assert(! projected.empty()); assert(out.empty() || out.size() == projected.size()); diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 2b6c4ffcff..8360a9562f 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -1806,6 +1806,13 @@ ExPolygons PrintObject::_shrink_contour_holes(double contour_delta, double hole_ return union_ex(new_ex_polys); } +Transform3d PrintObject::trafo_sliced() const +{ + Transform3d trafo = this->trafo_centered(); + BeltSliceStrategy::apply_preslice_transforms(trafo, this->print()->config(), this->model_object()->volumes); + return trafo; +} + std::vector PrintObject::slice_support_volumes(const ModelVolumeType model_volume_type) const { auto it_volume = this->model_object()->volumes.begin(); @@ -1820,7 +1827,7 @@ std::vector PrintObject::slice_support_volumes(const ModelVolumeType m const Print *print = this->print(); auto throw_on_cancel_callback = std::function([print](){ print->throw_if_canceled(); }); MeshSlicingParamsEx params; - params.trafo = this->trafo_centered(); + params.trafo = this->trafo_sliced(); for (; it_volume != it_volume_end; ++ it_volume) if ((*it_volume)->type() == model_volume_type) { std::vector slices2 = slice_volume(*(*it_volume), zs, params, throw_on_cancel_callback); From 6e33f3f5dd11dee5250188b6acc540442fb1295c Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 15:58:45 +0800 Subject: [PATCH 116/224] Share and Clamp the Build Plate Tilt Shift in Support Generators The three support generators each computed lh * tan(tilt), which overflows coord_t at 90 degrees and flips sign beyond it (belt sync can write up to 180). One helper now returns the tilt slope with the tilt capped at 89 degrees. --- src/libslic3r/Support/SupportCommon.cpp | 6 ++++++ src/libslic3r/Support/SupportCommon.hpp | 4 ++++ src/libslic3r/Support/SupportMaterial.cpp | 10 +++------- src/libslic3r/Support/TreeSupport.cpp | 10 +++------- src/libslic3r/Support/TreeSupport3D.cpp | 12 ++++-------- 5 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/libslic3r/Support/SupportCommon.cpp b/src/libslic3r/Support/SupportCommon.cpp index 27ac9b24d1..0c3d570b1d 100644 --- a/src/libslic3r/Support/SupportCommon.cpp +++ b/src/libslic3r/Support/SupportCommon.cpp @@ -2069,4 +2069,10 @@ sub clip_with_shape { } */ +Vec2d build_plate_tilt_slope(const PrintConfig &print_config) +{ + auto slope = [](double tilt_deg) { return std::tan(Geometry::deg2rad(std::clamp(tilt_deg, -89., 89.))); }; + return { slope(print_config.build_plate_tilt_y.value), slope(print_config.build_plate_tilt_x.value) }; +} + } // namespace Slic3r diff --git a/src/libslic3r/Support/SupportCommon.hpp b/src/libslic3r/Support/SupportCommon.hpp index f15c7d417a..4374986a3e 100644 --- a/src/libslic3r/Support/SupportCommon.hpp +++ b/src/libslic3r/Support/SupportCommon.hpp @@ -150,6 +150,10 @@ Polygons belt_floor_surface_polygon( const SlicingParameters &slicing_params, const PrintConfig &print_config, const PrintObject &object, coordf_t print_z); +// Build plate tilt: XY drift of gravity per unit of layer height, zero on a level plate. +// The tilt is capped below 90 degrees to keep the drift finite. +Vec2d build_plate_tilt_slope(const PrintConfig &print_config); + } // namespace Slic3r #endif /* slic3r_SupportCommon_hpp_ */ diff --git a/src/libslic3r/Support/SupportMaterial.cpp b/src/libslic3r/Support/SupportMaterial.cpp index c8ac217af2..5d886bcdf0 100644 --- a/src/libslic3r/Support/SupportMaterial.cpp +++ b/src/libslic3r/Support/SupportMaterial.cpp @@ -1438,9 +1438,8 @@ static inline ExPolygons detect_overhangs( const bool bridge_no_support = object_config.bridge_no_support.value; const coordf_t xy_expansion = scale_(object_config.support_expansion.value); // Build plate tilt: compute per-layer XY shift for tilted gravity direction - const double tilt_x_rad = Geometry::deg2rad(print_config.build_plate_tilt_x.value); - const double tilt_y_rad = Geometry::deg2rad(print_config.build_plate_tilt_y.value); - const bool has_tilt = std::abs(tilt_x_rad) > EPSILON || std::abs(tilt_y_rad) > EPSILON; + const Vec2d tilt_slope = build_plate_tilt_slope(print_config); + const bool has_tilt = tilt_slope.cwiseAbs().maxCoeff() > EPSILON; float lower_layer_offset = 0; if (layer_id == 0) @@ -1480,10 +1479,7 @@ static inline ExPolygons detect_overhangs( Polygons tilted_lower; if (has_tilt) { tilted_lower = lower_layer_polygons; - const double lh = lower_layer.height; - Point tilt_shift(coord_t(scale_(lh * tan(tilt_y_rad))), - coord_t(scale_(lh * tan(tilt_x_rad)))); - translate(tilted_lower, tilt_shift); + translate(tilted_lower, Point::new_scale(tilt_slope * lower_layer.height)); effective_lower = &tilted_lower; } diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 84ae229130..a86aea3536 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -709,9 +709,8 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) const double threshold_rad = Geometry::deg2rad(thresh_angle); // Build plate tilt: compute per-layer XY shift for tilted gravity direction const PrintConfig& print_cfg = m_object->print()->config(); - const double tilt_x_rad = Geometry::deg2rad(print_cfg.build_plate_tilt_x.value); - const double tilt_y_rad = Geometry::deg2rad(print_cfg.build_plate_tilt_y.value); - const bool has_tilt = std::abs(tilt_x_rad) > EPSILON || std::abs(tilt_y_rad) > EPSILON; + const Vec2d tilt_slope = build_plate_tilt_slope(print_cfg); + const bool has_tilt = tilt_slope.cwiseAbs().maxCoeff() > EPSILON; // FIXME this is a fudge constant! double support_tree_tip_diameter = 0.8; auto enforcer_overhang_offset = scaled(support_tree_tip_diameter); @@ -858,10 +857,7 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) ExPolygons shifted_lower; if (has_tilt) { shifted_lower = lower_polys; // copy - const double lh = lower_layer->height; - Point tilt_shift(coord_t(scale_(lh * tan(tilt_y_rad))), - coord_t(scale_(lh * tan(tilt_x_rad)))); - translate(shifted_lower, tilt_shift); + translate(shifted_lower, Point::new_scale(tilt_slope * lower_layer->height)); } const ExPolygons &effective_lower = has_tilt ? shifted_lower : lower_polys; diff --git a/src/libslic3r/Support/TreeSupport3D.cpp b/src/libslic3r/Support/TreeSupport3D.cpp index e02f474420..b5c35bfd6b 100644 --- a/src/libslic3r/Support/TreeSupport3D.cpp +++ b/src/libslic3r/Support/TreeSupport3D.cpp @@ -211,9 +211,8 @@ static std::vector>> group_me // +1 makes the threshold inclusive double tan_threshold = support_threshold_auto ? 0. : tan(M_PI * double(support_threshold + 1) / 180.); // Build plate tilt: compute per-layer XY shift for tilted gravity direction - const double tilt_x_rad = Geometry::deg2rad(print_config.build_plate_tilt_x.value); - const double tilt_y_rad = Geometry::deg2rad(print_config.build_plate_tilt_y.value); - const bool has_tilt = std::abs(tilt_x_rad) > EPSILON || std::abs(tilt_y_rad) > EPSILON; + const Vec2d tilt_slope = build_plate_tilt_slope(print_config); + const bool has_tilt = tilt_slope.cwiseAbs().maxCoeff() > EPSILON; //FIXME this is a fudge constant! auto enforcer_overhang_offset = scaled(config.tree_support_tip_diameter.value); const coordf_t radius_sample_resolution = g_config_tree_support_collision_resolution; @@ -235,7 +234,7 @@ static std::vector>> group_me size_t num_overhang_layers = support_auto ? num_object_layers : std::min(num_object_layers, std::max(size_t(support_enforce_layers), enforcers_layers.size())); tbb::parallel_for(tbb::blocked_range(1, num_overhang_layers), [&print_object, &config, &print_config, &enforcers_layers, &blockers_layers, - support_auto, support_enforce_layers, support_threshold_auto, tan_threshold, enforcer_overhang_offset, num_raft_layers, radius_sample_resolution, has_tilt, tilt_x_rad, tilt_y_rad, &throw_on_cancel, &out] + support_auto, support_enforce_layers, support_threshold_auto, tan_threshold, enforcer_overhang_offset, num_raft_layers, radius_sample_resolution, has_tilt, tilt_slope, &throw_on_cancel, &out] (const tbb::blocked_range &range) { for (LayerIndex layer_id = range.begin(); layer_id < range.end(); ++ layer_id) { const Layer ¤t_layer = *print_object.get_layer(layer_id); @@ -263,10 +262,7 @@ static std::vector>> group_me Polygons lower_layer_offseted; if (has_tilt) { Polygons lower_src = to_polygons(lower_layer.lslices_extrudable); - const double lh = lower_layer.height; - Point tilt_shift(coord_t(scale_(lh * tan(tilt_y_rad))), - coord_t(scale_(lh * tan(tilt_x_rad)))); - translate(lower_src, tilt_shift); + translate(lower_src, Point::new_scale(tilt_slope * lower_layer.height)); lower_layer_offseted = offset(lower_src, lower_layer_offset); } else { lower_layer_offseted = offset(lower_layer.lslices_extrudable, lower_layer_offset); From 4ed56954e8be7e1d9e5fb355628816454d51985b Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 15:59:05 +0800 Subject: [PATCH 117/224] Limit Build Plate Tilt Range Below 90 Degrees A 90 degree tilt has no finite gravity drift per layer, so the option range now stops at 89 degrees, matching the cap applied by the support generators. --- src/libslic3r/PrintConfig.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index ceeb60af92..a6a419a15e 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -7163,8 +7163,8 @@ void PrintConfigDef::init_fff_params() "A negative value tilts the -X side higher. Set to 0 for no X-axis tilt. " "In belt printer mode, this is automatically synced to the belt angle."); def->sidetext = u8"\u00B0"; - def->min = -90; - def->max = 90; + def->min = -89; + def->max = 89; def->mode = comExpert; def->set_default_value(new ConfigOptionFloat(0.)); @@ -7175,8 +7175,8 @@ void PrintConfigDef::init_fff_params() "A positive value tilts the plate so the +Y side is higher, shifting gravity toward -Y and increasing overhangs on the +Y side. " "A negative value tilts the -Y side higher. Set to 0 for no Y-axis tilt."); def->sidetext = u8"\u00B0"; - def->min = -90; - def->max = 90; + def->min = -89; + def->max = 89; def->mode = comExpert; def->set_default_value(new ConfigOptionFloat(0.)); From f2a11928f63f371c92ad8f4a33ae35379e06d3bb Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 20:25:08 +0800 Subject: [PATCH 118/224] Read the Belt Tilt Only from the Belt G-code Header Every printer's config block lists belt_slice_rotation_angle (default 45), so the processor marked all G-code as belt G-code: imported flat G-code got the belt view on a belt printer, and the belt-only Z handling in the processor ran for non-belt prints whose config block precedes the body. Take the angle only from outside the config block, where only the belt header writes it. --- src/libslic3r/GCode/GCodeProcessor.cpp | 16 +++++++-- src/libslic3r/GCode/GCodeProcessor.hpp | 1 + tests/fff_print/CMakeLists.txt | 1 + tests/fff_print/test_gcode_processor.cpp | 46 ++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 tests/fff_print/test_gcode_processor.cpp diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index b2477437b0..33d5b96876 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -3586,6 +3586,7 @@ void GCodeProcessor::reset() m_zero_layer_height = 0.0f; m_first_layer_height = 0.0f; m_processing_start_custom_gcode = false; + m_in_config_block = false; m_g1_line_id = 0; m_layer_id = 0; m_cp_color.reset(); @@ -4191,9 +4192,20 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers return; } + if (boost::starts_with(comment, " CONFIG_BLOCK_START")) { + m_in_config_block = true; + return; + } + if (boost::starts_with(comment, " CONFIG_BLOCK_END")) { + m_in_config_block = false; + return; + } + // Belt printer: derive the physical tilt magnitude from the slicing-rotation - // angle header comment (used to enable the preview's belt view). - if (boost::starts_with(comment, " belt_slice_rotation_angle = ")) { + // angle header comment (used to enable the preview's belt view). Only the belt + // header carries it outside the config block; the config block lists the key + // for every printer, belt or not. + if (!m_in_config_block && boost::starts_with(comment, " belt_slice_rotation_angle = ")) { try { m_result.belt_tilt_angle = std::abs(std::stof(std::string(comment.substr(29)))); } catch (...) {} diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index 0a8a35a88b..85cc5a7544 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -1194,6 +1194,7 @@ class Print; float m_first_layer_height; // mm float m_zero_layer_height; // mm bool m_processing_start_custom_gcode; + bool m_in_config_block; unsigned int m_g1_line_id; unsigned int m_layer_id; CpColor m_cp_color; diff --git a/tests/fff_print/CMakeLists.txt b/tests/fff_print/CMakeLists.txt index 3247bfda66..59e065ecb8 100644 --- a/tests/fff_print/CMakeLists.txt +++ b/tests/fff_print/CMakeLists.txt @@ -8,6 +8,7 @@ add_executable(${_TEST_NAME}_tests test_extrusion_processor.cpp test_fill.cpp test_flow.cpp + test_gcode_processor.cpp test_gcode_timing.cpp test_gcodewriter.cpp test_model.cpp diff --git a/tests/fff_print/test_gcode_processor.cpp b/tests/fff_print/test_gcode_processor.cpp new file mode 100644 index 0000000000..3998b65547 --- /dev/null +++ b/tests/fff_print/test_gcode_processor.cpp @@ -0,0 +1,46 @@ +#include + +#include "libslic3r/libslic3r.h" +#include "libslic3r/GCode/GCodeProcessor.hpp" +#include "libslic3r/PrintConfig.hpp" + +#include "test_utils.hpp" + +#include +#include + +using namespace Slic3r; +using Catch::Matchers::WithinAbs; + +namespace { + +float processed_belt_tilt(const std::string &gcode) +{ + ScopedTemporaryFile temp(".gcode"); + { + std::ofstream os(temp.string()); + os << gcode; + } + GCodeProcessor proc; + proc.apply_config(FullPrintConfig{}); + proc.process_file(temp.string()); + return proc.get_result().belt_tilt_angle; +} + +constexpr const char *body = "G1 X10 Y10 Z0.2 F3000\nG1 X20 Y10 E1 F1200\n"; + +} // namespace + +TEST_CASE("The config block's belt angle does not mark G-code as belt G-code", "[GCodeProcessor][belt]") +{ + // Every printer's config block lists belt_slice_rotation_angle (default 45), belt or not. + const std::string gcode = std::string("; CONFIG_BLOCK_START\n; belt_printer = 0\n; belt_slice_rotation_angle = 45\n; CONFIG_BLOCK_END\n") + body; + CHECK_THAT(processed_belt_tilt(gcode), WithinAbs(0., 1e-6)); +} + +TEST_CASE("The belt header's angle marks G-code as belt G-code", "[GCodeProcessor][belt]") +{ + const std::string gcode = std::string("; belt_slice_rotation_angle = -45.0\n") + body + + "; CONFIG_BLOCK_START\n; belt_printer = 1\n; belt_slice_rotation_angle = -45\n; CONFIG_BLOCK_END\n"; + CHECK_THAT(processed_belt_tilt(gcode), WithinAbs(45., 1e-6)); +} From 4a7311bf0160e54aa9d912a1d92f06beda8ccb61 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 15:57:58 +0800 Subject: [PATCH 119/224] Rebuild the Brim Type Combobox Only When Its Entries Change toggle_options() now runs on every value change and mode switch; rebuild the brim_type choices only when the leading-edge entry has to be added or removed. --- src/slic3r/GUI/Tab.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 1c03ddf88a..2c1247edb5 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -3230,16 +3230,17 @@ void TabPrint::toggle_options() const auto current = m_config->opt_enum("brim_type"); auto &opt = const_cast(field->m_opt); auto cb = dynamic_cast(choice->window); - if (cb != nullptr) { + // Keep the entry if it is already selected, so switching to a non-belt + // printer cannot leave the control showing a value it does not offer. + const bool offer_leading_edge = is_belt_printer || current == btLeadingEdgeOnly; + const bool offered = std::find(opt.enum_values.begin(), opt.enum_values.end(), "leading_edge_only") != opt.enum_values.end(); + if (cb != nullptr && offer_leading_edge != offered) { auto n = cb->GetValue(); opt.enum_values.clear(); opt.enum_labels.clear(); cb->Clear(); for (size_t i = 0; i < def->enum_values.size(); ++ i) { - // Keep the entry if it is already selected, so switching to a non-belt - // printer cannot leave the control showing a value it does not offer. - if (def->enum_values[i] == "leading_edge_only" && ! is_belt_printer - && current != btLeadingEdgeOnly) + if (def->enum_values[i] == "leading_edge_only" && ! offer_leading_edge) continue; opt.enum_values.push_back(def->enum_values[i]); opt.enum_labels.push_back(def->enum_labels[i]); From 3b302b466617e336b6723e294317b06c138b62a6 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 16:04:54 +0800 Subject: [PATCH 120/224] Derive Belt Support Tilt From Slicing Rotation in Print::apply build_plate_tilt_x/y was synced from belt_slice_rotation* only by the printer Tab, so CLI or 3MF edits of the rotation left the support tilt stale. --- src/libslic3r/PrintApply.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index aa5c6aa2bd..0ed85f7fbc 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1241,6 +1241,17 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", i=%1%, key=%2%")%i %changed_keys[i]; } } + // On belt printers the support tilt follows the slicing rotation. The GUI keeps the two in + // sync, but a CLI or 3MF edit of the rotation alone would otherwise leave supports on a stale tilt. + if (const auto *belt_opt = new_full_config.option("belt_printer"); belt_opt && belt_opt->value) { + const auto *axis_opt = new_full_config.option>("belt_slice_rotation"); + const auto *angle_opt = new_full_config.option("belt_slice_rotation_angle"); + if (axis_opt && angle_opt) { + const auto tilt = BeltTransformPipeline::physical_tilt(axis_opt->value, angle_opt->value); + new_full_config.set_key_value("build_plate_tilt_x", new ConfigOptionFloat(tilt.tilt_x_deg)); + new_full_config.set_key_value("build_plate_tilt_y", new ConfigOptionFloat(tilt.tilt_y_deg)); + } + } const ConfigOption* enable_support_option = new_full_config.option("enable_support"); if (enable_support_option && enable_support_option->getBool()) m_support_used = true; From 708212a306f6f39dd051be81249340572e301047 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Sun, 13 Sep 2026 03:02:21 +0800 Subject: [PATCH 121/224] Share One Belt Brim Band Loop Between Apron-Only and Ordinary Layers The ordinary-layer path kept its own copy of the apron band loop. Give emit_belt_brim_bands() an optional brim filament filter and call it from the per-extruder lambda; without a filter it still prints every band, so apron-only layers are unchanged. --- src/libslic3r/GCode.cpp | 40 ++++++++-------------------------------- src/libslic3r/GCode.hpp | 10 ++++++---- 2 files changed, 14 insertions(+), 36 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 284c815f2d..4c742ed8df 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -5388,7 +5388,8 @@ LayerResult GCode::process_belt_brim_layer( // object layer, takes the ordinary path, and the band would be silently dropped. std::string GCode::emit_belt_brim_bands(const Print &print, const std::vector &layers, - const size_t single_object_instance_idx) + const size_t single_object_instance_idx, + const std::optional extruder_id) { std::string gcode; for (const LayerToPrint <p : layers) { @@ -5396,6 +5397,9 @@ std::string GCode::emit_belt_brim_bands(const Print &print, if (band == nullptr || band->fills.empty() || ltp.original_object == nullptr) continue; const PrintObject &object = *ltp.original_object; + // No filter prints every band; belt_brim_filament() is 1-based. + if (extruder_id && (! object.has_belt_brim() || static_cast(object.belt_brim_filament() - 1) != *extruder_id)) + continue; // Speeds, flow and retraction all read m_config. m_config.apply(print.default_region_config()); m_config.apply(object.config(), true); @@ -6623,39 +6627,11 @@ LayerResult GCode::process_layer( std::set> belt_brim_emitted; // Emit every ORDINARY-layer apron band (belt_brim_prologue band coinciding with an - // object/support layer) whose brim filament is this pass's extruder. Mirrors - // emit_belt_brim_bands() per band, but filtered to one brim filament so each band + // object/support layer) whose brim filament is this pass's extruder, so each band // prints in the correct tool's pass (Finding B). extruder_id is 0-based (the - // reindexed tool domain); belt_brim_filament() is 1-based, so subtract one. + // reindexed tool domain). auto emit_belt_brim_for_extruder = [this, &print, &layers, single_object_instance_idx](unsigned int extruder_id) -> std::string { - std::string gc; - for (const LayerToPrint <p : layers) { - const BeltBrimBand *band = ltp.belt_brim_band; - if (band == nullptr || band->fills.empty() || ltp.original_object == nullptr) - continue; - const PrintObject &object = *ltp.original_object; - if (! object.has_belt_brim() || (unsigned int)(object.belt_brim_filament() - 1) != extruder_id) - continue; - // Speeds, flow and retraction all read m_config. - m_config.apply(print.default_region_config()); - m_config.apply(object.config(), true); - const size_t i_begin = single_object_instance_idx == size_t(-1) ? 0 : single_object_instance_idx; - const size_t i_end = single_object_instance_idx == size_t(-1) ? object.instances().size() - : single_object_instance_idx + 1; - for (size_t i = i_begin; i < i_end && i < object.instances().size(); ++ i) { - // Band geometry is object-local, like the object's own extrusions. - const Point &offset = object.instances()[i].shift; - this->set_origin(unscale(offset)); - this->on_set_origin(&object, offset); - m_avoid_crossing_perimeters.use_external_mp(); - for (const ExtrusionEntity *ee : band->fills.entities) - if (ee != nullptr) - gc += this->extrude_entity(*ee, "brim", NOZZLE_CONFIG(support_speed)); - m_avoid_crossing_perimeters.use_external_mp(false); - m_avoid_crossing_perimeters.disable_once(); - } - } - return gc; + return this->emit_belt_brim_bands(print, layers, single_object_instance_idx, extruder_id); }; for (unsigned int extruder_id : layer_tools.extruders) diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 34b0774b34..23fb563711 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -418,13 +418,15 @@ protected: const bool last_layer, const size_t single_object_instance_idx); - // Emit the apron bands carried by these layers. Called from both the brim-only - // branch and the ordinary path, since a band's print_z can coincide with another - // object's layer on a multi-object belt. + // Emit the apron bands carried by these layers, only those whose brim filament is + // extruder_id (0-based) when given. Called from both the brim-only branch and the + // ordinary path, since a band's print_z can coincide with another object's layer on a + // multi-object belt. std::string emit_belt_brim_bands( const Print &print, const std::vector &layers, - const size_t single_object_instance_idx); + const size_t single_object_instance_idx, + const std::optional extruder_id = std::nullopt); LayerResult process_layer( const Print &print, From 19a085206ca5e7594d295c153e4afb9e113b6aa1 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Sun, 13 Sep 2026 03:02:36 +0800 Subject: [PATCH 122/224] Print Belt Brim Aprons in Each Object's Brim Filament Apron-only layers printed every band with the first tool, so objects with different brim filaments at the same apron Z shared one filament. Emit each brim filament's bands with its own toolchange. --- src/libslic3r/GCode.cpp | 13 +++++++++---- src/libslic3r/GCode.hpp | 9 ++++----- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 4c742ed8df..bc117f5d1b 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -5374,7 +5374,12 @@ LayerResult GCode::process_belt_brim_layer( print.config().layer_change_gcode.value, m_writer->filament()->id(), &config) + "\n"; } - gcode += this->emit_belt_brim_bands(print, layers, single_object_instance_idx); + // Objects sharing this apron Z may use different brim filaments; print each in its own tool. + for (const unsigned int brim_extruder : layer_tools.extruders) { + if (m_writer->filament() == nullptr || m_writer->filament()->id() != brim_extruder) + gcode += this->set_extruder(brim_extruder, print_z); + gcode += this->emit_belt_brim_bands(print, layers, single_object_instance_idx, brim_extruder); + } result.gcode = std::move(gcode); return result; @@ -5389,7 +5394,7 @@ LayerResult GCode::process_belt_brim_layer( std::string GCode::emit_belt_brim_bands(const Print &print, const std::vector &layers, const size_t single_object_instance_idx, - const std::optional extruder_id) + const unsigned int extruder_id) { std::string gcode; for (const LayerToPrint <p : layers) { @@ -5397,8 +5402,8 @@ std::string GCode::emit_belt_brim_bands(const Print &print, if (band == nullptr || band->fills.empty() || ltp.original_object == nullptr) continue; const PrintObject &object = *ltp.original_object; - // No filter prints every band; belt_brim_filament() is 1-based. - if (extruder_id && (! object.has_belt_brim() || static_cast(object.belt_brim_filament() - 1) != *extruder_id)) + // belt_brim_filament() is 1-based. + if (! object.has_belt_brim() || static_cast(object.belt_brim_filament() - 1) != extruder_id) continue; // Speeds, flow and retraction all read m_config. m_config.apply(print.default_region_config()); diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 23fb563711..afa7151a5c 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -418,15 +418,14 @@ protected: const bool last_layer, const size_t single_object_instance_idx); - // Emit the apron bands carried by these layers, only those whose brim filament is - // extruder_id (0-based) when given. Called from both the brim-only branch and the - // ordinary path, since a band's print_z can coincide with another object's layer on a - // multi-object belt. + // Emit the apron bands carried by these layers whose brim filament is extruder_id + // (0-based). Called from both the brim-only branch and the ordinary path, since a + // band's print_z can coincide with another object's layer on a multi-object belt. std::string emit_belt_brim_bands( const Print &print, const std::vector &layers, const size_t single_object_instance_idx, - const std::optional extruder_id = std::nullopt); + const unsigned int extruder_id); LayerResult process_layer( const Print &print, From cb5b489e9010c3d0a978749714786d3336a159a3 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 15:57:27 +0800 Subject: [PATCH 123/224] Invalidate Only G-code Export for Belt Output Options gcode_back_transform, first_layer_plane* and belt_printer_infinite_y fell through to invalidate_all_steps(), which re-ran tool ordering, skirt/brim and G-code export on toggles that only affect G-code export. --- src/libslic3r/Print.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index ba838003ea..b97ffed3a2 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -121,6 +121,10 @@ 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 "additional_cooling_fan_speed", "reduce_crossing_wall", From 76aba24ddfeea6249f8902061db1bea3e66f5b12 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 15:59:34 +0800 Subject: [PATCH 124/224] Number Belt Extension Support Layers Sequentially Extension layers were all created with id 0, so every one of them could be taken for the first layer by id-only checks (ooze-prevention standby temperature, cached layer ids). Renumber the support layers after inserting them. --- src/libslic3r/Support/TreeSupport.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index a86aea3536..1f39a56163 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -1913,6 +1913,8 @@ void TreeSupport::generate() 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); } } } From 33aec258da17c4988d35ada0445a13d48a9531eb Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Mon, 14 Sep 2026 16:42:33 +0800 Subject: [PATCH 125/224] Skip CLI Wipe Tower Reservation on Belt Printers Print::has_wipe_tower() is always false for belt printers, but CLI arrange, plate checks and the pre-slice tower clamp still reserved a phantom tower footprint and wrote a clamped wipe_tower_x/y into the config. --- src/OrcaSlicer.cpp | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index b75c653eda..15c5677485 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -4014,6 +4014,10 @@ int CLI::run(int argc, char **argv) BOOST_LOG_TRIVIAL(info) << boost::format("%1%, set disable_wipe_tower_after_mapping back to false due to wrapping detect")%__LINE__; } + // Belt printers never get the classic wipe tower (see Print::has_wipe_tower()), so reserve no space for it. + const ConfigOptionBool* belt_printer_opt = m_print_config.option("belt_printer"); + const bool is_belt_printer = belt_printer_opt && belt_printer_opt->value; + auto timelapse_type_opt = m_print_config.option("timelapse_type"); bool is_smooth_timelapse = false; if (enable_timelapse && timelapse_type_opt && (timelapse_type_opt->getInt() == TimelapseType::tlSmooth)) @@ -4251,11 +4255,11 @@ int CLI::run(int argc, char **argv) } }; - auto check_plate_wipe_tower = [get_print_sequence, is_smooth_timelapse](Slic3r::GUI::PartPlate* plate, int plate_index, DynamicPrintConfig& print_config, plate_obj_size_info_t &plate_obj_size_info) { + auto check_plate_wipe_tower = [get_print_sequence, is_smooth_timelapse, is_belt_printer](Slic3r::GUI::PartPlate* plate, int plate_index, DynamicPrintConfig& print_config, plate_obj_size_info_t &plate_obj_size_info) { plate_obj_size_info.obj_bbox= plate->get_objects_bounding_box(); BOOST_LOG_TRIVIAL(info) << boost::format("plate %1%, object bbox: min {%2%, %3%, %4%} - max {%5%, %6%, %7%}") %(plate_index+1) %plate_obj_size_info.obj_bbox.min.x() % plate_obj_size_info.obj_bbox.min.y() % plate_obj_size_info.obj_bbox.min.z() %plate_obj_size_info.obj_bbox.max.x() % plate_obj_size_info.obj_bbox.max.y() % plate_obj_size_info.obj_bbox.max.z(); - if (!print_config.has("wipe_tower_x")) { + if (is_belt_printer || !print_config.has("wipe_tower_x")) { plate_obj_size_info.has_wipe_tower = false; BOOST_LOG_TRIVIAL(info) << boost::format("can not found wipe_tower_x in config, set to no wipe tower"); return; @@ -5062,7 +5066,7 @@ int CLI::run(int argc, char **argv) } } - if ((!arrange_cfg.is_seq_print && (assemble_plate.filaments_count > 1))||(enable_wrapping_detect && !current_wrapping_exclude_area.empty())) + if (!is_belt_printer && ((!arrange_cfg.is_seq_print && (assemble_plate.filaments_count > 1)) || (enable_wrapping_detect && !current_wrapping_exclude_area.empty()))) { //prepare the wipe tower int plate_count = partplate_list.get_plate_count(); @@ -5212,7 +5216,7 @@ int CLI::run(int argc, char **argv) bool is_seq_print = false; get_print_sequence(cur_plate, m_print_config, is_seq_print); - if (!is_seq_print && (assemble_plate.filaments_count > 1) && !has_wipe_tower_position) + if (!is_belt_printer && !is_seq_print && (assemble_plate.filaments_count > 1) && !has_wipe_tower_position) { //prepare the wipe tower auto printer_structure_opt = m_print_config.option>("printer_structure"); @@ -5361,7 +5365,7 @@ int CLI::run(int argc, char **argv) //add the virtual object into unselect list if has partplate_list.preprocess_exclude_areas(unselected, enable_wrapping_detect); - if (used_filament_set.size() > 0) + if (!is_belt_printer && used_filament_set.size() > 0) { //prepare the wipe tower int plate_count = partplate_list.get_plate_count(); @@ -5467,7 +5471,7 @@ int CLI::run(int argc, char **argv) BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": found single object mode"); } - if (m_print_config.has("wipe_tower_x") && (is_smooth_timelapse || !arrange_cfg.is_seq_print || (selected.size() <= 1))) { + if (!is_belt_printer && m_print_config.has("wipe_tower_x") && (is_smooth_timelapse || !arrange_cfg.is_seq_print || (selected.size() <= 1))) { float x; float y; if (duplicate_count > 0) { @@ -6037,7 +6041,7 @@ int CLI::run(int argc, char **argv) // The stored (or default) tower position may not fit the tower these plates // need, and no CLI placement site runs on a plain slice - mirror the GUI's // reload clamp and fit every plate's tower into the printable area first. - if (m_print_config.option("enable_prime_tower", true)->value) { + if (!is_belt_printer && m_print_config.option("enable_prime_tower", true)->value) { for (int index = 0; index < partplate_list.get_plate_count(); index++) { if ((plate_to_slice != 0) && (plate_to_slice != (index + 1))) continue; From 5cd66612801e6c851bc09352af05ee9acbc7faec Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 15:59:38 +0800 Subject: [PATCH 126/224] Check the Belt Temperature Tower Model Load Bail out like the other calibration paths when add_model() fails instead of indexing the empty model. --- src/slic3r/GUI/Plater.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 25084e9a9c..b6274ab7a2 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -16556,7 +16556,8 @@ void Plater::calib_temp(const Calib_Params& params) { << ", falling back to 230_190 (embossed numbers will not match)"; asset = calib_dir + "belt_temp_tower_230_190.stl"; } - add_model(false, asset); + if (!add_model(false, asset) || model().objects.empty()) + return; // Place keel-first asset at the belt entry (designed Y = 0) so Z_gcode // starts at 0, centered laterally on the bed, resting on the conveyor. From a8a45439db350cf1f026e70dd017a8b39996f570 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 16:05:40 +0800 Subject: [PATCH 127/224] Share One Build Plate Tilt Up-Direction Helper Across the GUI The bed gravity arrow, volume rendering and the painter/support gizmos each rebuilt the tilt up-vector from build_plate_tilt_x/y; use one helper that also tolerates presets without the keys. --- src/slic3r/GUI/3DBed.cpp | 12 +++--------- src/slic3r/GUI/3DScene.cpp | 12 +----------- src/slic3r/GUI/GUI_App.cpp | 13 +++++++++++++ src/slic3r/GUI/GUI_App.hpp | 2 ++ src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp | 20 +++----------------- src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp | 1 - src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp | 9 +-------- 7 files changed, 23 insertions(+), 46 deletions(-) diff --git a/src/slic3r/GUI/3DBed.cpp b/src/slic3r/GUI/3DBed.cpp index fa9acffb50..3032059678 100644 --- a/src/slic3r/GUI/3DBed.cpp +++ b/src/slic3r/GUI/3DBed.cpp @@ -746,20 +746,14 @@ void Bed3D::render_custom(GLCanvas3D& canvas, const Transform3d& view_matrix, co void Bed3D::render_gravity_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix) { - const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; // build_plate_tilt_{x,y} are kept in sync with the belt tilt (see TabPrinter), so // reading them here covers both belt and non-belt tilted printers. - double tilt_x_deg = cfg.opt_float("build_plate_tilt_x"); - double tilt_y_deg = cfg.opt_float("build_plate_tilt_y"); - if (tilt_x_deg == 0. && tilt_y_deg == 0.) { + const Vec3d up_dir = build_plate_tilt_up_direction(); + if (up_dir == Vec3d::UnitZ()) { m_gravity_arrow.reset(); return; } - - // Gravity direction (matching the slicer's tilt convention) - double tilt_x_rad = Geometry::deg2rad(tilt_x_deg); - double tilt_y_rad = Geometry::deg2rad(tilt_y_deg); - Vec3d gravity_dir = Vec3d(-tan(tilt_y_rad), -tan(tilt_x_rad), -1.0).normalized(); + const Vec3d gravity_dir = -up_dir; // Build the arrow model (same dimensions as the axis arrows) if (!m_gravity_arrow.is_initialized()) { diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index 81b0ee8028..b5c5c4fa16 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -1155,17 +1155,7 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type, // Compute up direction accounting for build plate tilt. This is frame-invariant // (config cannot change mid-render), so compute it once before the volume loop. - Vec3f up_direction = Vec3f::UnitZ(); - { - const DynamicPrintConfig& prt_cfg = GUI::wxGetApp().preset_bundle->printers.get_edited_preset().config; - double tilt_x_deg = prt_cfg.opt_float("build_plate_tilt_x"); - double tilt_y_deg = prt_cfg.opt_float("build_plate_tilt_y"); - if (tilt_x_deg != 0. || tilt_y_deg != 0.) { - double tilt_x_rad = Geometry::deg2rad(tilt_x_deg); - double tilt_y_rad = Geometry::deg2rad(tilt_y_deg); - up_direction = Vec3f(float(tan(tilt_y_rad)), float(tan(tilt_x_rad)), 1.f).normalized(); - } - } + const Vec3f up_direction = GUI::build_plate_tilt_up_direction().cast(); for (GLVolumeWithIdAndZ& volume : to_render) { #if ENABLE_MODIFIERS_ALWAYS_TRANSPARENT diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 966cf49013..512ddd315e 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -81,6 +81,7 @@ #include #include "libslic3r/Utils.hpp" +#include "libslic3r/Geometry.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/I18N.hpp" #include "libslic3r/PresetBundle.hpp" @@ -9812,5 +9813,17 @@ bool is_support_filament(int extruder_id, bool strict_check) return support_option->get_at(0); }; +Vec3d build_plate_tilt_up_direction() +{ + const DynamicPrintConfig &cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + const auto *opt_x = cfg.option("build_plate_tilt_x"); + const auto *opt_y = cfg.option("build_plate_tilt_y"); + const double tilt_x = opt_x != nullptr ? opt_x->value : 0.; + const double tilt_y = opt_y != nullptr ? opt_y->value : 0.; + if (tilt_x == 0. && tilt_y == 0.) + return Vec3d::UnitZ(); + return Vec3d(std::tan(Geometry::deg2rad(tilt_y)), std::tan(Geometry::deg2rad(tilt_x)), 1.).normalized(); +} + } // GUI } //Slic3r diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index f6f0b81c92..1355d7728d 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -831,6 +831,8 @@ bool is_support_filament(int extruder_id, bool strict_check = true); bool is_soluble_filament(int extruder_id); // check if the filament for model is in the list bool has_filaments(const std::vector& model_filaments); +// Up direction of the edited printer's tilted build plate (+Z when untilted). +Vec3d build_plate_tilt_up_direction(); } // namespace GUI } // Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp index 468e2e9b6c..78a3d4a834 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp @@ -548,14 +548,6 @@ int GLGizmoFdmSupports::get_selection_support_threshold_angle() return auto_support ? support_threshold_angle : 0; } -std::pair GLGizmoFdmSupports::get_build_plate_tilt() -{ - const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; - double tilt_x = cfg.opt_float("build_plate_tilt_x"); - double tilt_y = cfg.opt_float("build_plate_tilt_y"); - return {tilt_x, tilt_y}; -} - void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block) { float threshold = (float(M_PI)/180.f)*threshold_deg; @@ -564,15 +556,9 @@ void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block) const ModelInstance* mi = mo->instances[selection.get_instance_idx()]; // Compute gravity direction accounting for build plate tilt - auto [tilt_x_deg, tilt_y_deg] = get_build_plate_tilt(); - double tilt_x_rad = tilt_x_deg * M_PI / 180.0; - double tilt_y_rad = tilt_y_deg * M_PI / 180.0; - const bool has_tilt = (tilt_x_deg != 0. || tilt_y_deg != 0.); - // NB: use an if, not a ?:, so each branch converts to Vec3d independently - // (the two Eigen expression types don't unify in a ternary). - Vec3d gravity_dir = -Vec3d::UnitZ(); - if (has_tilt) - gravity_dir = Vec3d(-tan(tilt_y_rad), -tan(tilt_x_rad), -1.0).normalized(); + const Vec3d up_dir = build_plate_tilt_up_direction(); + const bool has_tilt = up_dir != Vec3d::UnitZ(); + const Vec3d gravity_dir = -up_dir; int mesh_id = -1; for (const ModelVolume* mv : mo->volumes) { diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp index b313ca04aa..57a6cc1981 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp @@ -60,7 +60,6 @@ private: void select_facets_by_angle(float threshold, bool block); // BBS int get_selection_support_threshold_angle(); - std::pair get_build_plate_tilt(); int m_support_threshold_angle = -1; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp index ac0f46df34..ad9f69eb9b 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp @@ -75,14 +75,7 @@ GLGizmoPainterBase::ClippingPlaneDataWrapper GLGizmoPainterBase::get_clipping_pl Vec3f GLGizmoPainterBase::get_tilt_up_direction() const { - const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; - double tilt_x_deg = cfg.opt_float("build_plate_tilt_x"); - double tilt_y_deg = cfg.opt_float("build_plate_tilt_y"); - if (tilt_x_deg == 0. && tilt_y_deg == 0.) - return Vec3f::UnitZ(); - double tilt_x_rad = Geometry::deg2rad(tilt_x_deg); - double tilt_y_rad = Geometry::deg2rad(tilt_y_deg); - return Vec3f(float(tan(tilt_y_rad)), float(tan(tilt_x_rad)), 1.f).normalized(); + return build_plate_tilt_up_direction().cast(); } void GLGizmoPainterBase::render_triangles(const Selection& selection) const From 62ef0fa4f8f2d6af9ef9c88cb3baf57724f0ae00 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Sun, 13 Sep 2026 03:03:00 +0800 Subject: [PATCH 128/224] Write the Standard Layer Change Tag on Belt Brim Apron Layers Apron layers appended print_z to the layer change tag, unlike every other layer; write the plain tag line. --- src/libslic3r/GCode.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index bc117f5d1b..0640860ec8 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -5342,8 +5342,7 @@ LayerResult GCode::process_belt_brim_layer( // skip these layers entirely. { char buf[64]; - sprintf(buf, ";%s%g\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Layer_Change).c_str(), print_z); - gcode += buf; + gcode += ";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Layer_Change) + "\n"; sprintf(buf, ";Z:%g\n", print_z); gcode += buf; const float band_height = float(height); From 18c08862a80b0e4bc2550addca69aa975e88d1d6 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 16:04:12 +0800 Subject: [PATCH 129/224] Align up_direction Position in TriangleSelector Fill Calls seed_fill_select_triangles() now takes up_direction right after highlight_by_angle_deg, as select_patch() does. --- src/libslic3r/TriangleSelector.cpp | 2 +- src/libslic3r/TriangleSelector.hpp | 4 ++-- src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp index 7e911cc565..840d3c672b 100644 --- a/src/libslic3r/TriangleSelector.cpp +++ b/src/libslic3r/TriangleSelector.cpp @@ -367,7 +367,7 @@ bool TriangleSelector::is_facet_clipped(int facet_idx, const ClippingPlane &clp) void TriangleSelector::seed_fill_select_triangles(const Vec3f &hit, int facet_start, const Transform3d& trafo_no_translate, const ClippingPlane &clp, float seed_fill_angle, float highlight_by_angle_deg, - bool force_reselection, const Vec3f &up_direction) + const Vec3f &up_direction, bool force_reselection) { assert(facet_start < m_orig_size_indices); diff --git a/src/libslic3r/TriangleSelector.hpp b/src/libslic3r/TriangleSelector.hpp index 6154f7d7f3..e715d976ab 100644 --- a/src/libslic3r/TriangleSelector.hpp +++ b/src/libslic3r/TriangleSelector.hpp @@ -336,8 +336,8 @@ public: const ClippingPlane &clp, // Clipping plane to limit painting to not clipped facets only float seed_fill_angle, // the maximal angle between two facets to be painted by the same color float highlight_by_angle_deg = 0.f, // The maximal angle of overhang. If it is set to a non-zero value, it is possible to paint only the triangles of overhang defined by this angle in degrees. - bool force_reselection = false, // force reselection of the triangle mesh even in cases that mouse is pointing on the selected triangle - const Vec3f &up_direction = Vec3f::UnitZ()); // Up direction for overhang detection (accounts for build plate tilt) + const Vec3f &up_direction = Vec3f::UnitZ(), // Up direction for overhang detection (accounts for build plate tilt) + bool force_reselection = false); // force reselection of the triangle mesh even in cases that mouse is pointing on the selected triangle void bucket_fill_select_triangles(const Vec3f &hit, // point where to start int facet_start, // facet of the original mesh (unsplit) that the hit point belongs to diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp index ad9f69eb9b..d85c635b5a 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp @@ -700,7 +700,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous mi->get_assemble_transformation().get_matrix() * mo->volumes[m_rr.mesh_id]->get_matrix() : mi->get_transformation().get_matrix() * mo->volumes[m_rr.mesh_id]->get_matrix(); m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, int(m_rr.facet), trafo_matrix_not_translate, this->get_clipping_plane_in_volume_coordinates(trafo_matrix), m_smart_fill_angle, - m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, true, get_tilt_up_direction()); + m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, get_tilt_up_direction(), true); m_triangle_selectors[m_rr.mesh_id]->request_update_render_data(); m_seed_fill_last_mesh_id = m_rr.mesh_id; } @@ -864,7 +864,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous m_triangle_selectors[mesh_idx]->seed_fill_apply_on_triangles(new_state); if (m_tool_type == ToolType::SMART_FILL) m_triangle_selectors[mesh_idx]->seed_fill_select_triangles(mesh_hit, facet_idx, trafo_matrix_not_translate, clp, m_smart_fill_angle, - m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, true, get_tilt_up_direction()); + m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, get_tilt_up_direction(), true); else if (m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER) // BBS: add infill_angle parameter m_triangle_selectors[mesh_idx]->bucket_fill_select_triangles(mesh_hit, facet_idx, clp, -1.f, false, true); @@ -962,7 +962,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous const TriangleSelector::ClippingPlane &clp = this->get_clipping_plane_in_volume_coordinates(trafo_matrix); if (m_tool_type == ToolType::SMART_FILL) m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, int(m_rr.facet), trafo_matrix_not_translate, clp, m_smart_fill_angle, - m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, false, get_tilt_up_direction()); + m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, get_tilt_up_direction(), false); else if (m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER) // BBS: add infill_angle parameter m_triangle_selectors[m_rr.mesh_id]->bucket_fill_select_triangles(m_rr.hit, int(m_rr.facet), clp, -1.f, false); From 14cf7861e063ae9c3d41b0cb7c4444517c525865 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 16:01:26 +0800 Subject: [PATCH 130/224] Fix Belt Tooltip Spacing and Legend Casing Drop double spaces in the belt tilt tooltips and match the preview legend header to the "Belt printer" settings group. --- src/slic3r/GUI/GCodeViewer.cpp | 2 +- src/slic3r/GUI/Tab.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index ae504d3746..16e7262470 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -4923,7 +4923,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv 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::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 diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 2c1247edb5..9905a203ba 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -5105,7 +5105,7 @@ void TabPrinter::build_fff() { 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. " + "slicing. Also drives bed rendering and support gravity tilt. " "Isometric (no distortion); the back-transform inverts it before " "the machine-frame remap.") }; line.append_option(belt_og->get_option("belt_slice_rotation")); @@ -5160,7 +5160,7 @@ void TabPrinter::build_fff() { 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 " + "the belt tilt angle. Enable 'Decouple' to set an independent " "machine-frame angle when the physical gantry tilt differs from " "the slicing rotation.") }; line.append_option(mf->get_option("belt_frame_tilt_decouple")); From b4052ec99f796047871fae17e799d69ffc1091c0 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 16:01:48 +0800 Subject: [PATCH 131/224] Drop the Redundant Lift Type Alias in eager_lift effective_type was a plain copy of the parameter. --- src/libslic3r/GCodeWriter.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index 67dd6cf138..a2d0a26b6e 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -836,7 +836,6 @@ std::string GCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase) // BBS: immediately execute an undelayed lift move with a spiral lift pattern // designed specifically for subsequent gcode injection (e.g. timelapse) std::string GCodeWriter::eager_lift(const LiftType type) { - const LiftType effective_type = type; std::string lift_move; double target_lift = 0; { @@ -850,7 +849,7 @@ std::string GCodeWriter::eager_lift(const LiftType type) { } // BBS: spiral lift only safe with known position - if (effective_type == LiftType::SpiralLift && this->is_current_position_clear()) { + if (type == LiftType::SpiralLift && this->is_current_position_clear()) { double radius = target_lift / (2 * PI * atan(filament()->travel_slope())); // static spiral alignment when no move in x,y plane. // spiral centra is a radius distance to the right (y=0) From 2b1a7e38dfd6bab9ad76295b37339a1a926a55cb Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 16:03:14 +0800 Subject: [PATCH 132/224] Drop Redundant Belt Checks in BeltGCode BeltGCode is only created for belt printers, so its hooks no longer re-check belt_printer, and the BBL-machine flag is set once on whichever writer survives init_belt_writer instead of on one about to be discarded. --- src/libslic3r/BeltGCode.cpp | 11 ++--------- src/libslic3r/BeltGCode.hpp | 2 +- src/libslic3r/GCode.cpp | 5 ++--- src/libslic3r/GCode.hpp | 2 +- 4 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/libslic3r/BeltGCode.cpp b/src/libslic3r/BeltGCode.cpp index b6108ea78e..1b2d76ff76 100644 --- a/src/libslic3r/BeltGCode.cpp +++ b/src/libslic3r/BeltGCode.cpp @@ -5,13 +5,9 @@ namespace Slic3r { -void BeltGCode::init_belt_writer(Print &print, bool is_bbl_printers) +void BeltGCode::init_belt_writer(Print &print) { - if (!print.config().belt_printer.value) - return; - auto belt_writer = std::make_unique(); - belt_writer->set_is_bbl_machine(is_bbl_printers); // Axis remap and build volume max are set by base GCode after init_belt_writer returns. belt_writer->set_belt_back_transform(print.config()); belt_writer->set_machine_frame_transform(print.config()); @@ -21,9 +17,6 @@ void BeltGCode::init_belt_writer(Print &print, bool is_bbl_printers) void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print) { - if (!print.config().belt_printer.value) - return; - const auto &full_cfg = print.full_print_config(); // Slicing rotation: the belt tilt (axis + angle) and the single source of truth // for the physical tilt the G-code viewer uses to enable belt view. @@ -62,7 +55,7 @@ void BeltGCode::on_set_origin(const PrintObject * /*obj*/, const Point & /*inst_ || (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 || !m_config.belt_printer.value) + if (!use_global) return; // Adjust origin: transform through belt forward pipeline so that diff --git a/src/libslic3r/BeltGCode.hpp b/src/libslic3r/BeltGCode.hpp index 2a3718b49f..b68bb1066f 100644 --- a/src/libslic3r/BeltGCode.hpp +++ b/src/libslic3r/BeltGCode.hpp @@ -14,7 +14,7 @@ namespace Slic3r { class BeltGCode : public GCode { protected: - void init_belt_writer(Print &print, bool is_bbl_printers) override; + void init_belt_writer(Print &print) override; void write_belt_header(GCodeOutputStream &file, const Print &print) override; void on_set_origin(const PrintObject *obj, const Point &inst_shift) override; bool should_disable_arc_fitting() const override { return true; } diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 0640860ec8..05ec6ebc78 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2961,10 +2961,9 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_fan_mover.release(); m_ordering_cache.clear(); - m_writer->set_is_bbl_machine(is_bbl_printers); - // Belt printer: initialize belt-specific writer via virtual hook. - this->init_belt_writer(print, is_bbl_printers); + this->init_belt_writer(print); + m_writer->set_is_bbl_machine(is_bbl_printers); // Standalone axis remap (works with or without belt mode). // Sync the writer's remap state to the current export UNCONDITIONALLY — even at diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index afa7151a5c..18cb7868a2 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -376,7 +376,7 @@ protected: // Virtual hooks for belt printer subclass (BeltGCode). // No-ops in base GCode; overridden in BeltGCode. - virtual void init_belt_writer(Print &print, bool is_bbl_printers) {} + virtual void init_belt_writer(Print &print) {} virtual void write_belt_header(GCodeOutputStream &file, const Print &print) {} virtual void on_set_origin(const PrintObject *obj, const Point &inst_shift) {} virtual bool should_disable_arc_fitting() const { return false; } From 2b2c710626330fca12570bf32bcf538b0f521c4c Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 16:03:41 +0800 Subject: [PATCH 133/224] Order GCodeWriter Initializers Like the Members Moved to Protected The lift, speed and cached-extruder members now live in the protected section ahead of the private ones; list their initializers first so the list reads in construction order. No behaviour change. --- src/libslic3r/GCodeWriter.hpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index 43e869910e..c73ccc3e07 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -18,17 +18,18 @@ public: bool multiple_extruders; GCodeWriter() : - multiple_extruders(false), m_curr_filament_extruder(MAXIMUM_EXTRUDER_NUMBER, nullptr), - m_curr_extruder_id (-1), - m_cached_extruder_idx(0), - m_single_extruder_multi_material(false), - m_last_acceleration(0), m_max_acceleration(0),m_last_travel_acceleration(0), m_max_travel_acceleration(0), - m_last_jerk(0), m_max_jerk_x(0), m_max_jerk_y(0), - m_last_bed_temperature(0), m_last_bed_temperature_reached(true), + multiple_extruders(false), m_lifted(0), m_to_lift(0), m_to_lift_type(LiftType::NormalLift), - m_current_speed(3600), m_is_first_layer(true) + m_is_first_layer(true), m_current_speed(3600), + m_cached_extruder_idx(0), + m_curr_filament_extruder(MAXIMUM_EXTRUDER_NUMBER, nullptr), + m_curr_extruder_id (-1), + m_single_extruder_multi_material(false), + m_last_acceleration(0), m_max_acceleration(0),m_last_travel_acceleration(0), m_max_travel_acceleration(0), + m_last_jerk(0), m_max_jerk_x(0), m_max_jerk_y(0), + m_last_bed_temperature(0), m_last_bed_temperature_reached(true) {} Extruder* filament(size_t extruder_id) { assert(extruder_id < m_curr_filament_extruder.size()); return m_curr_filament_extruder[extruder_id]; } const Extruder* filament(size_t extruder_id) const { assert(extruder_id < m_curr_filament_extruder.size()); return m_curr_filament_extruder[extruder_id]; } From 6cf747808c79c913665e8c03a7d811329d3d8290 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 30 Sep 2026 15:44:18 -0500 Subject: [PATCH 134/224] Belt: never start a scarf joint seam below the layer A scarf joint begins one layer height below the current layer and ramps up along the wall. On a tilted belt that start is a step backwards along the belt axis, into the previous layer's wall at the seam: 0.283 mm per 0.2 mm layer at 45 degrees. With an aligned seam the nozzle rams the same spot on every layer. A BabyBelt Pro benchy with seam_slope_type=external showed 601 such back-steps from layer 107 on, and in the field the belt "jumped backwards" and the head knocked the part loose. Belt printers now skip the scarf in GCode::extrude_loop, and the process tab greys the scarf controls out for them, as it already does for arc fitting. The regression test slices a cube on a belt with the scarf enabled and checks the belt axis never steps back by a layer pitch. --- src/libslic3r/GCode.cpp | 7 +++- src/slic3r/GUI/ConfigManipulation.cpp | 6 ++-- tests/fff_print/test_print.cpp | 51 +++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index ed2dd400d4..fae8860b7b 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -7661,8 +7661,13 @@ std::string GCode::extrude_loop(const ExtrusionLoop& loop_ref, loop.split_at(last_pos, false); const auto seam_scarf_type = m_config.seam_slope_type.value; + // Belt printers never get a scarf joint. The scarf starts one layer height + // below the layer, which on a tilted belt is a step backwards along the belt + // axis into the previous layer's wall at the seam (0.28 mm at 45 degrees per + // 0.2 mm layer); with an aligned seam that ram repeats at the same spot on + // every layer and knocks the part loose. bool enable_seam_slope = ((seam_scarf_type == SeamScarfType::External && !is_hole) || seam_scarf_type == SeamScarfType::All) && - !m_config.spiral_mode && + !m_config.spiral_mode && !m_config.belt_printer.value && (loop.role() == erExternalPerimeter || (loop.role() == erPerimeter && m_config.seam_slope_inner_walls)) && layer_id() > 0; const auto nozzle_diameter = EXTRUDER_CONFIG(nozzle_diameter); diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 65c2df092c..ac3ffa9006 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -1193,8 +1193,10 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in toggle_line("small_area_infill_flow_compensation_model", have_small_area_infill_flow_compensation); - toggle_field("seam_slope_type", !has_spiral_vase); - bool has_seam_slope = !has_spiral_vase && config->opt_enum("seam_slope_type") != SeamScarfType::None; + // Belt printers: the scarf would start one layer back along the belt, inside the + // previous layer (GCode::extrude_loop skips it there too). + toggle_field("seam_slope_type", !has_spiral_vase && !is_belt_printer); + bool has_seam_slope = !has_spiral_vase && !is_belt_printer && config->opt_enum("seam_slope_type") != SeamScarfType::None; toggle_line("seam_slope_conditional", has_seam_slope); toggle_line("seam_slope_start_height", has_seam_slope); toggle_line("seam_slope_entire_loop", has_seam_slope); diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 78d8e0fd72..511a627fc1 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -20,6 +20,7 @@ #include "test_utils.hpp" #include +#include #include #include @@ -508,3 +509,53 @@ TEST_CASE("Sequential printing publishes the nozzle group result", "[Print][Mult CHECK(gcode.find("; SEQ-ND-OK") != std::string::npos); } } + +// A scarf joint starts one layer height below the layer and ramps up along the +// wall. On a tilted belt that start is a step backwards along the belt axis, into +// the previous layer's wall at the seam: 0.283 mm per 0.2 mm layer at 45 degrees. +// With an aligned seam the nozzle rams the same spot on every layer (field report +// from a BabyBelt Pro: the belt "jumped backwards" and knocked the part loose). +// Belt printers therefore never get a scarf, whatever the process preset says. +TEST_CASE("Belt printers never start a scarf seam below the layer", "[Print][belt][Seam]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "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" }, + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "skirt_loops", 0 }, + { "top_shell_layers", 0 }, + { "bottom_shell_layers", 1 }, + { "wall_loops", 2 }, + { "seam_position", "back" }, + { "seam_slope_type", "external" }, + { "seam_slope_inner_walls", 1 }, + { "seam_slope_start_height", 0 }, + { "machine_start_gcode", "T[initial_tool]\n" }, + { "layer_change_gcode", "G92 E0\n" }, + }); + const std::string gcode = slice({ cube(20) }, config); + REQUIRE(! gcode.empty()); + + // The belt axis is machine Z. Within a layer it only drifts by the frame + // coupling (well under 0.1 mm across a 20 mm cube); a scarf start is a full + // layer pitch (0.283 mm) backwards. + double last_z = std::numeric_limits::lowest(); + double worst_backstep = 0.; + GCodeReader parser; + parser.parse_buffer(gcode, [&](GCodeReader &, const GCodeReader::GCodeLine &line) { + if (! line.cmd_is("G1") || ! line.has_z()) + return; + const double z = line.z(); + if (last_z != std::numeric_limits::lowest()) + worst_backstep = std::max(worst_backstep, last_z - z); + last_z = z; + }); + CHECK(worst_backstep < 0.2); +} From 48af5b5d9725281983dfcf9dc60e0e15dd754212 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 30 Sep 2026 15:44:18 -0500 Subject: [PATCH 135/224] Read the pre-slice remap header tags at their real length The header tags lost their belt_ prefix in the Part 3.2 rename (20 characters now), but the parser still skipped 25, so every axis read as pos_x. Found in Hanif Koh's review of #14394. --- src/libslic3r/GCode/GCodeProcessor.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index efb4a45ed0..88ce95195f 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -4232,13 +4232,13 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers return RemapAxis::PosX; }; if (boost::starts_with(comment, " preslice_remap_x = ")) { - m_result.preslice_remap_x = parse_remap_axis(trim(std::string(comment.substr(25)))); return; + m_result.preslice_remap_x = parse_remap_axis(trim(std::string(comment.substr(20)))); return; } if (boost::starts_with(comment, " preslice_remap_y = ")) { - m_result.preslice_remap_y = parse_remap_axis(trim(std::string(comment.substr(25)))); return; + m_result.preslice_remap_y = parse_remap_axis(trim(std::string(comment.substr(20)))); return; } if (boost::starts_with(comment, " preslice_remap_z = ")) { - m_result.preslice_remap_z = parse_remap_axis(trim(std::string(comment.substr(25)))); return; + m_result.preslice_remap_z = parse_remap_axis(trim(std::string(comment.substr(20)))); return; } } // wipe start tag From 2c0570c97d5ad6aaec27226dcc38649a099994c0 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 30 Sep 2026 15:44:49 -0500 Subject: [PATCH 136/224] Drop references to planning docs that are not in the tree The MachineKinematics comments pointed at docs/superpowers plan files, which are gitignored working notes. --- src/libslic3r/GCode/MachineKinematics.hpp | 2 +- tests/fff_print/test_gcodewriter.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/GCode/MachineKinematics.hpp b/src/libslic3r/GCode/MachineKinematics.hpp index e046dd9476..5c53bd9f58 100644 --- a/src/libslic3r/GCode/MachineKinematics.hpp +++ b/src/libslic3r/GCode/MachineKinematics.hpp @@ -15,7 +15,7 @@ namespace Slic3r { // // This is a seam for writer-generated movement only. Start/end/custom G-code, // classic wipe-tower output and GCodeWriter::extrude_arc_to_xy() do NOT pass -// through it; see doc in 09-orca-machinekinematics-split-plan.md section 6. +// through it: they write machine coordinates directly. class MachineKinematics { public: diff --git a/tests/fff_print/test_gcodewriter.cpp b/tests/fff_print/test_gcodewriter.cpp index a37a4947ae..b8f2b890e5 100644 --- a/tests/fff_print/test_gcodewriter.cpp +++ b/tests/fff_print/test_gcodewriter.cpp @@ -1038,7 +1038,7 @@ SCENARIO("Belt: start-gcode prepare-stage moves keep their real Z", "[GCode][bel // --------------------------------------------------------------------------- // Regression tests for the two latent bugs the MachineKinematics refactor -// preserved deliberately (see 09b-latent-bug-fix-plan.md). +// preserved deliberately and the follow-up commit fixed. // --------------------------------------------------------------------------- // Bug 1. _travel_to_z() emits full XYZ whenever the mapping must emit every From 69a08915d10044d1ceae00fa0d1d59be64b08837 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 30 Sep 2026 17:29:33 -0500 Subject: [PATCH 137/224] Belt: do not fail the G-code height check against belt travel check_multi_extruder_gcode_valid() compares each object's max Z with printable_height. On a belt printer machine Z is belt travel (a 3DBenchy on the BabyBelt Pro runs from Z=197 to Z=309 on a 69 mm printable_height), so every belt export set the over-height error bit and the CLI refused the plate with -102 "G-code in unprintable area". The preview already skips its ToolHeightOutside warning for the same reason; the export check now does too. The XY printable-area check is unchanged. --- src/libslic3r/GCode/GCodeProcessor.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 88ce95195f..c2aedd8262 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -2860,7 +2860,11 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int valid = false; } } - if ( iter->second.max_print_z > plate_printable_height ) { //over height + // Belt printers: machine Z is belt travel, which grows without bound over a + // print, while printable_height is the clearance above the belt; the two are + // not comparable, so the over-height check is skipped, as the preview's + // ToolHeightOutside warning already is. + if ( !machine_frame_active && iter->second.max_print_z > plate_printable_height ) { //over height m_result.gcode_check_result.error_code |= (1 << 3); std::pair filament_to_object_id; filament_to_object_id.first = iter->first; @@ -2901,7 +2905,7 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int } // check printable height - if ((extruder_id < printable_heights.size()) && (iter->second.max_print_z > printable_heights[extruder_id])) { + if (!machine_frame_active && (extruder_id < printable_heights.size()) && (iter->second.max_print_z > printable_heights[extruder_id])) { m_result.gcode_check_result.error_code |= (1 << 1); std::pair filament_to_object_id; filament_to_object_id.first = iter->first; From 4f110bc26133516508e54bf4842762267a43e22e Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 30 Sep 2026 17:42:16 -0500 Subject: [PATCH 138/224] Belt scarf test: slice without a z-hop The default 0.4 mm z-hop is a 0.57 mm move along the belt axis and its return tripped the back-step check. Shipped belt profiles print without a z-hop, so the test does too. --- tests/fff_print/test_print.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 511a627fc1..8b342fde68 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -537,6 +537,9 @@ TEST_CASE("Belt printers never start a scarf seam below the layer", "[Print][bel { "seam_slope_type", "external" }, { "seam_slope_inner_walls", 1 }, { "seam_slope_start_height", 0 }, + // No z-hop: on a belt a lift is a move along the belt axis (0.4 mm / sin 45 = 0.57 mm) + // and its return would read as a back-step. The shipped belt profiles print without one. + { "z_hop", 0 }, { "machine_start_gcode", "T[initial_tool]\n" }, { "layer_change_gcode", "G92 E0\n" }, }); From d57549159dda3cf727ec46f09d576c39cf4f458f Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 25 Sep 2026 01:54:17 -0500 Subject: [PATCH 139/224] 3MF loader: refuse non-finite vertex coordinates A 3MF vertex with a nan/inf coordinate was accepted by both parsers and crashed qhull in ModelVolume's convex hull while the file was still loading. Both vertex handlers refuse it, and volume generation checks again whichever parser produced the geometry. The main parser's _stop_object_xml_parser keeps a message a handler already set. --- src/libslic3r/Format/bbs_3mf.cpp | 43 +++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index e2091da1db..366104bfa8 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -978,10 +978,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) void _stop_object_xml_parser(const std::string& msg = std::string()) { assert(! obj_parse_error); - assert(obj_parse_error_message.empty()); assert(object_xml_parser != nullptr); obj_parse_error = true; - obj_parse_error_message = msg; + if (! msg.empty() || obj_parse_error_message.empty()) // a handler may have set the message already + obj_parse_error_message = msg; XML_StopParser(object_xml_parser, false); } @@ -3815,11 +3815,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) { // appends the vertex coordinates // missing values are set equal to ZERO - if (m_curr_object) - m_curr_object->geometry.vertices.emplace_back( - m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, X_ATTR), - m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Y_ATTR), - m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Z_ATTR)); + if (m_curr_object) { + const Vec3f v(m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, X_ATTR), + m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Y_ATTR), + m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Z_ATTR)); + // A non-finite coordinate ("nan", "inf") used to be accepted and crashed + // qhull in ModelVolume's convex hull while the file was still loading. Refuse the file. + if (! v.allFinite()) { + _stop_xml_parser("Invalid vertex coordinate: not a finite number"); + return true; // the parser is stopped; returning false would overwrite the message + } + m_curr_object->geometry.vertices.emplace_back(v); + } return true; } @@ -5109,6 +5116,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) } } + for (const Vec3f &v : sub_object->geometry.vertices) + if (! v.allFinite()) { // Qhull cannot take a NaN vertex + add_error("invalid (non-finite) vertex in object " + std::to_string(sub_object->id)); + return false; + } its.vertices.assign(sub_object->geometry.vertices.begin(), sub_object->geometry.vertices.end()); // BBS @@ -5600,11 +5612,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) { // appends the vertex coordinates // missing values are set equal to ZERO - if (current_object) - current_object->geometry.vertices.emplace_back( - object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, X_ATTR), - object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Y_ATTR), - object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Z_ATTR)); + if (current_object) { + const Vec3f v(object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, X_ATTR), + object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Y_ATTR), + object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Z_ATTR)); + // See _BBS_3MF_Importer::_handle_start_vertex: a non-finite coordinate + // crashed qhull while the file loaded. The dispatcher stops this parser on `false`. + if (! v.allFinite()) { + obj_parse_error_message = "Invalid vertex coordinate: not a finite number"; + return false; + } + current_object->geometry.vertices.emplace_back(v); + } return true; } From 6be7911e40564290e0c6a62617f95776a4c6dfd6 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 25 Sep 2026 01:54:17 -0500 Subject: [PATCH 140/224] CLI: survive a project with missing printable_height or fewer filaments than loaded Found by fuzzing the headless slicer: - A BBS-style 3MF without Metadata/project_settings.config segfaulted the CLI silently on the missing printable_height option. - A project saved with fewer filaments (or filament groups) than --load-filaments overran the filament variant tables (segfault in the variant match) and then hit an uncaught ConfigurationError from set_with_restore_2 (std::terminate). The tables are regenerated for the filaments the project did not know about, the destination vectors grown first (only from a non-empty source), the match bounded, and a failure becomes a CLI config error. --- src/OrcaSlicer.cpp | 46 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index 15c5677485..1abd1627d8 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -1809,7 +1809,10 @@ int CLI::run(int argc, char **argv) old_printable_width = static_cast(old_printable_bbox.size().x()); old_printable_depth = static_cast(old_printable_bbox.size().y()); } - old_printable_height = (int)(config.opt_float("printable_height")); + // A BBS-style 3mf without Metadata/project_settings.config has no + // printable_height (found by fuzzing: this was a silent segfault). + if (const auto *ph = config.option("printable_height")) + old_printable_height = (int) ph->value; if (config.option("extruder_clearance_height_to_rod")) old_height_to_rod = config.opt_float("extruder_clearance_height_to_rod"); @@ -3341,9 +3344,14 @@ int CLI::run(int argc, char **argv) max_self_index = std::max(max_self_index, v); min_self_index = std::min(min_self_index, v); } - if (max_self_index > filament_count || min_self_index < 1) { - BOOST_LOG_TRIVIAL(warning) << boost::format("filament_self_index range [%1%, %2%] is invalid for filament_count %3%, regenerating") - % min_self_index % max_self_index % filament_count; + // And a project saved with FEWER filaments than are now loaded (a + // one-filament project sliced with two --load-filaments) leaves the tables half filled: + // the variant matching below then reads past filament_extruder_variant and + // set_with_restore_2 throws an uncaught size error. Regenerate in that case too. + if (max_self_index > filament_count || min_self_index < 1 || max_self_index < filament_count + || (int) filament_self_index_opt->values.size() < filament_count) { + BOOST_LOG_TRIVIAL(warning) << boost::format("filament_self_index range [%1%, %2%] (size %4%) is invalid for filament_count %3%, regenerating") + % min_self_index % max_self_index % filament_count % filament_self_index_opt->values.size(); need_regenerate_self_index = true; } } @@ -3424,6 +3432,10 @@ int CLI::run(int argc, char **argv) std::vector& filament_variants = curr_variant_opt->values; filament_variants.resize(filament_count, get_extruder_variant_string(etDirectDrive, nvtStandard)); } + // See the filament_self_index note above: one variant per filament for + // the filaments the project did not know about. + if ((int) curr_variant_opt->values.size() < filament_count) + curr_variant_opt->values.resize(filament_count, get_extruder_variant_string(etDirectDrive, nvtStandard)); const ConfigOptionStrings *new_variant_opt = dynamic_cast(config.option("filament_extruder_variant", true)); std::vector new_variant_indice; @@ -3432,7 +3444,7 @@ int CLI::run(int argc, char **argv) for (int i = 0; i < new_variant_count; i++) { - for (int j = old_start_indice[filament_index - 1]; j < old_start_indice[filament_index - 1] + old_variant_count; j++) + for (int j = old_start_indice[filament_index - 1]; j < old_start_indice[filament_index - 1] + old_variant_count && j < (int) curr_variant_opt->values.size(); j++) { if (curr_variant_opt->values[j] == new_variant_opt->values[i]) { new_variant_indice[i] = j; @@ -3484,7 +3496,18 @@ int CLI::run(int argc, char **argv) ConfigOptionVectorBase* opt_vec_dst = static_cast(opt); const ConfigOptionVectorBase* opt_vec_src = static_cast(source_opt); //set with index - opt_vec_dst->set_with_restore_2(opt_vec_src, new_variant_indice, old_start_indice[filament_index - 1], old_variant_count); + try { + // A project with fewer filaments than are loaded: grow the + // destination to the filament's slot first (set_with_restore_2 only restores). + if (opt_vec_src->size() > 0 && opt_vec_dst->size() < size_t(old_start_indice[filament_index - 1] + old_variant_count)) + opt_vec_dst->resize(size_t(old_start_indice[filament_index - 1] + old_variant_count), opt_vec_src); + opt_vec_dst->set_with_restore_2(opt_vec_src, new_variant_indice, old_start_indice[filament_index - 1], old_variant_count); + } catch (const std::exception &ex) { // Was an uncaught abort + BOOST_LOG_TRIVIAL(error) << boost::format("filament %1%: option %2% could not be applied: %3%") % filament_index % opt_key % ex.what(); + boost::nowide::cerr << "filament " << filament_index << ": option " << opt_key << " could not be applied: " << ex.what() << std::endl; + record_exit_reson(outfile_dir, CLI_CONFIG_FILE_ERROR, 0, cli_errors[CLI_CONFIG_FILE_ERROR], sliced_info); + flush_and_exit(CLI_CONFIG_FILE_ERROR); + } } continue; @@ -3530,7 +3553,16 @@ int CLI::run(int argc, char **argv) if (filament_options_with_variant.find(opt_key) != filament_options_with_variant.end()) { std::vector temp_variant_indice; temp_variant_indice.resize(new_variant_count, -1); - opt_vec_dst->set_with_restore_2(opt_vec_src, temp_variant_indice, old_start_indice[filament_index - 1], old_variant_count, true); + try { + if (opt_vec_src->size() > 0 && opt_vec_dst->size() < size_t(old_start_indice[filament_index - 1] + old_variant_count)) // See above + opt_vec_dst->resize(size_t(old_start_indice[filament_index - 1] + old_variant_count), opt_vec_src); + opt_vec_dst->set_with_restore_2(opt_vec_src, temp_variant_indice, old_start_indice[filament_index - 1], old_variant_count, true); + } catch (const std::exception &ex) { // Was an uncaught abort + BOOST_LOG_TRIVIAL(error) << boost::format("filament %1%: option %2% could not be applied: %3%") % filament_index % opt_key % ex.what(); + boost::nowide::cerr << "filament " << filament_index << ": option " << opt_key << " could not be applied: " << ex.what() << std::endl; + record_exit_reson(outfile_dir, CLI_CONFIG_FILE_ERROR, 0, cli_errors[CLI_CONFIG_FILE_ERROR], sliced_info); + flush_and_exit(CLI_CONFIG_FILE_ERROR); + } if (opt_key == "filament_extruder_variant") new_variant_counts[filament_index - 1] = opt_vec_src->size(); From 6a6ceb2612211321711984ba6e57e9131a7823c3 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Thu, 24 Sep 2026 23:39:13 -0500 Subject: [PATCH 141/224] G-code: key the island tour cache on the island layout, never index past the islands The per-filament island tour was cached by island centroids only. A later layer with the same centroids but fewer islands (thin walls, negative volumes) reused the stale visit list, whose catch-all index pointed past the layer's islands, and extrude_perimeters read freed memory (three fuzz crashes, planar and belt). The per-instance island layout is part of the cache key and the use site never indexes past the islands. --- src/libslic3r/GCode.cpp | 21 +++++++++++++++------ src/libslic3r/GCode.hpp | 18 +++++++++++++++--- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index fae8860b7b..75b14e06bc 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -6491,6 +6491,7 @@ LayerResult GCode::process_layer( std::vector &objects_by_extruder = objects_by_extruder_it->second; std::vector &instances = filament_plan.first; std::vector nodes; + std::vector> layout; // Per instance, see IslandOrderCacheEntry std::vector node_instances; auto quantize_to_mm = [](const Point &pt) -> Point { const coord_t grid = coord_t(scale_(1.)); @@ -6515,6 +6516,7 @@ LayerResult GCode::process_layer( const size_t instance_idx = instances.size(); instances.emplace_back(object_by_extruder, layer_id, *print_object, instance_id, print_object->instances()[instance_id].model_instance->get_labeled_id()); + layout.emplace_back(islands.size(), ! islands.empty() && ! islands.back().by_region.empty()); const Point &shift = print_object->instances()[instance_id].shift; const size_t first_node = nodes.size(); if (islands_chainable) @@ -6534,8 +6536,9 @@ LayerResult GCode::process_layer( // Reuse the cached tour while this filament's island layout is unchanged. auto &cache_entry = m_ordering_cache[filament_id]; - if (!(cache_entry.first == nodes)) { - cache_entry.first = nodes; + if (! (cache_entry.nodes == nodes && cache_entry.layout == layout)) { + cache_entry.nodes = nodes; + cache_entry.layout = layout; Points node_points; node_points.reserve(nodes.size()); for (const IslandOrderNode &node : nodes) @@ -6568,12 +6571,12 @@ LayerResult GCode::process_layer( // A visit without explicit islands already prints everything. continue; std::vector &islands = instances[i].object_by_extruder.islands; - if (!islands.back().by_region.empty()) + if (! islands.empty() && ! islands.back().by_region.empty()) last_visit.islands.emplace_back(islands.size() - 1); } - cache_entry.second = std::move(visits); + cache_entry.visits = std::move(visits); } - filament_plan.second = cache_entry.second; + filament_plan.second = cache_entry.visits; } } @@ -6896,7 +6899,13 @@ LayerResult GCode::process_layer( // in this instance's frame after set_origin() above). Empty islands are skipped; // the trailing catch-all island has no centroid to chain by and always goes last. std::vector &islands = instance_to_print.object_by_extruder.islands; - std::vector island_order = visit.islands; + std::vector island_order; + island_order.reserve(visit.islands.size()); + for (size_t idx : visit.islands) // Never index past the islands (see IslandOrderCacheEntry) + if (idx < islands.size()) + island_order.emplace_back(idx); + else + BOOST_LOG_TRIVIAL(error) << "island tour refers to island " << idx << " of " << islands.size() << ", skipped"; if (island_order.empty()) { island_order.reserve(islands.size()); if (layer_to_print.object_layer != nullptr && islands.size() == layer_to_print.object_layer->lslices.size() + 1) { diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index e62d9cee41..5d99c219c6 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -632,9 +632,21 @@ protected: }; // Cache the per-filament island tour to avoid recomputing while the layer's island layout is - // unchanged. Key: filament_id. Value: {nodes the tour was computed from, resulting visits}. - std::map, std::vector>> - m_ordering_cache; + // unchanged. Key: filament_id. Value: the nodes the tour was computed from, the per-instance + // island layout (count and whether the trailing catch-all island has anything to print), and + // the resulting visits. + // The layout is part of the key. Nodes only cover the chainable islands, so two + // layers with the same centroids but a different number of islands (thin walls, negative + // volumes come and go) matched the cache and the visit's catch-all index -- islands.size() - 1 + // of the OLD layer -- ran past the new layer's islands (found by fuzzing: segfault in + // extrude_perimeters on multi-part objects). + struct IslandOrderCacheEntry + { + std::vector nodes; + std::vector> layout; + std::vector visits; + }; + std::map m_ordering_cache; ExtrusionQualityEstimator m_extrusion_quality_estimator; From 2ae41bb0efe1bd2c421987f32270cac5dc7df238 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Thu, 24 Sep 2026 23:39:13 -0500 Subject: [PATCH 142/224] Painting: guard the top/bottom projection erase when no shell layers are requested With top_shell_layers = 0 the `top` vector is never filled and erasing its begin() was undefined (found by fuzzing on a painted object dropped below the plate). --- src/libslic3r/MultiMaterialSegmentation.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/libslic3r/MultiMaterialSegmentation.cpp b/src/libslic3r/MultiMaterialSegmentation.cpp index 0016d96ac2..67b431790e 100644 --- a/src/libslic3r/MultiMaterialSegmentation.cpp +++ b/src/libslic3r/MultiMaterialSegmentation.cpp @@ -1244,10 +1244,16 @@ static inline std::vector> segmentation_top_and_bottom_l slicing_params.trafo = volume_trafo; Polygons bottom_slice = slice_mesh(painted, zs[0], slicing_params); - top.erase(top.begin()); - bottom.erase(bottom.begin()); - - bottom[0] = union_(bottom[0], bottom_slice); + // Only the requested projections exist: with + // top_shell_layers = 0 `top` is empty and erasing its begin() was + // undefined (found by fuzzing: a sunk, painted object crashed here). + if (! top.empty()) + top.erase(top.begin()); + if (! bottom.empty()) { + bottom.erase(bottom.begin()); + if (! bottom.empty()) + bottom[0] = union_(bottom[0], bottom_slice); + } } else slice_mesh_slabs(painted, zs, volume_trafo, max_top_layers > 0 ? &top : nullptr, max_bottom_layers > 0 ? &bottom : nullptr, nullptr, throw_on_cancel_callback); auto merge = [](std::vector &&src, std::vector &dst) { From e8926efbe33430680ce1d33c511c8029720036ef Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Thu, 24 Sep 2026 19:42:44 -0500 Subject: [PATCH 143/224] Belt purge: detect filament changes by scanning the ordering, not the first layer's flag ToolOrdering::has_wipe_tower() reads the first layer's flag. On a belt the first layer may be a brim apron band, which carries neither object nor support and never gets the flag, so with a brim the purge plan returned early and nothing was purged. Scan the layers for a change. --- src/libslic3r/BeltPurge.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/libslic3r/BeltPurge.cpp b/src/libslic3r/BeltPurge.cpp index 37d89ca14e..bba338b878 100644 --- a/src/libslic3r/BeltPurge.cpp +++ b/src/libslic3r/BeltPurge.cpp @@ -165,9 +165,18 @@ void Print::_plan_belt_purge() if (m_wipe_tower_data.tool_ordering.empty() || m_wipe_tower_data.tool_ordering.last_extruder() == unsigned(-1)) throw Slic3r::SlicingError("The print is empty. The model is not printable with current print settings."); - if (!m_wipe_tower_data.tool_ordering.has_wipe_tower()) - // No toolchanges anywhere, nothing to purge. - return; + // Is there any filament change at all? Not ToolOrdering::has_wipe_tower(): that reads the + // FIRST layer's flag, and on a belt the first layer may be a brim apron band, which carries + // neither object nor support and so never gets the flag even when the print changes filament. + { + bool any_change = false; + unsigned int cur = m_wipe_tower_data.tool_ordering.first_extruder(); + for (const auto < : m_wipe_tower_data.tool_ordering.layer_tools()) + for (const unsigned int e : lt.extruders) + if (e != cur) { any_change = true; cur = e; } + if (! any_change) + return; + } this->throw_if_canceled(); From d755580c994a41fe2f40ecf360182a9a2a3750bb Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Thu, 24 Sep 2026 19:39:01 -0500 Subject: [PATCH 144/224] Belt brim: the purge prism never gets a brim, whatever its config says The prism's generator already sets no_brim; PrintObject::has_belt_brim() now also ignores any brim setting on the prism (belt_purge_tower_object), so a brim on the parts beside it never blocks purging. --- src/libslic3r/PrintObject.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 81b8a58d6b..58ce53df5a 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1209,6 +1209,10 @@ bool PrintObject::has_belt_brim() const { if (! m_print->has_tilted_belt()) return false; + // The purge prism is sacrificial and sits at the plate's edge; its generator sets no_brim, and + // 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) From b8a8e38ceb5898191db2d61fdb4e89e878635fd2 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 25 Sep 2026 01:54:17 -0500 Subject: [PATCH 145/224] G-code: no crossing-perimeter avoidance while travelling on a brim apron layer Crossing-perimeter avoidance dereferenced the (null) layer while travelling on a brim apron layer. --- src/libslic3r/GCode.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 75b14e06bc..9947e40b8d 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -9427,6 +9427,7 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string // multi-hop travel path inside the configuration space if (m_config.reduce_crossing_wall && !m_avoid_crossing_perimeters.disabled_once() + && m_layer != nullptr // A brim apron layer has no Layer to avoid crossing && m_writer->is_current_position_clear()) //BBS: don't generate detour travel paths when current position is unclea { From f219431935cf95c499cf3bdda1b1e7a40d83c913 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Thu, 24 Sep 2026 07:39:43 -0500 Subject: [PATCH 146/224] GUI: the plate is open along Y for containment tests on an infinite-Y belt printer PartPlate's containment tests treat the plate as open along Y on a belt printer with belt_printer_infinite_y, so a long part is no longer flagged outside the plate in Prepare while the slicer and the G-code checks accept it. The check reads the printer preset through the app object, which does not exist headlessly, so it is guarded on the plater. --- src/slic3r/GUI/PartPlate.cpp | 16 ++++++++++++++++ src/slic3r/GUI/PartPlate.hpp | 4 ++++ 2 files changed, 20 insertions(+) diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 893260f934..12f4e423e5 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -2783,6 +2783,7 @@ bool PartPlate::check_outside(int obj_id, int instance_id, BoundingBoxf3* boundi BoundingBoxf3 instance_box = bounding_box? *bounding_box: object->instance_convex_hull_bounding_box(instance_id); Polygon hull = instance->convex_hull_2d(); BoundingBoxf3 plate_box = get_plate_box(); + this->open_belt_y(plate_box); if (instance_box.max.z() > plate_box.min.z()) plate_box.min.z() += instance_box.min.z(); // not considering outsize if sinking @@ -3467,6 +3468,19 @@ Polygon PartPlate::get_shared_printable_polygon() const return m_extruder_areas.empty() ? Polygon::new_scale(m_shape) : get_shared_poly(m_extruder_areas); } + +bool PartPlate::belt_open_y() const +{ + // Headless (CLI) plates have no plater and no wxApp behind wxGetApp(); the CLI's own belt + // handling lives in Print::validate(). + if (m_plater == nullptr || wxGetApp().preset_bundle == nullptr) + return false; + const DynamicPrintConfig &printer = wxGetApp().preset_bundle->printers.get_edited_preset().config; + const auto *belt = printer.option("belt_printer"); + const auto *infinite_y = printer.option("belt_printer_infinite_y"); + return belt != nullptr && belt->value && infinite_y != nullptr && infinite_y->value; +} + bool PartPlate::contains(const Vec3d& point) const { return m_bounding_box.contains(point); @@ -3486,6 +3500,7 @@ bool PartPlate::contains(const BoundingBoxf3& bb) const print_volume.min(1) -= Slic3r::BuildVolume::BedEpsilon; print_volume.max(0) += Slic3r::BuildVolume::BedEpsilon; print_volume.max(1) += Slic3r::BuildVolume::BedEpsilon; + this->open_belt_y(print_volume); return print_volume.contains(bb); } @@ -3498,6 +3513,7 @@ bool PartPlate::intersects(const BoundingBoxf3& bb) const print_volume.min(1) -= Slic3r::BuildVolume::BedEpsilon; print_volume.max(0) += Slic3r::BuildVolume::BedEpsilon; print_volume.max(1) += Slic3r::BuildVolume::BedEpsilon; + this->open_belt_y(print_volume); return print_volume.intersects(bb); } diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index e913ebaabf..babc88f364 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -427,6 +427,10 @@ public: bool contains(const GLVolume& v) const; bool contains(const BoundingBoxf3& bb) const; bool intersects(const BoundingBoxf3& bb) const; + // A belt printer with belt_printer_infinite_y: the plate is open along Y for the + // containment tests (the drawn plate keeps its shape). + bool belt_open_y() const; + void open_belt_y(BoundingBoxf3 &box) const { if (this->belt_open_y()) { box.min.y() = -1e5; box.max.y() = 1e5; } } void render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_body = false, bool force_background_color = false, HeightLimitMode mode = HEIGHT_LIMIT_NONE, int hover_id = -1, bool render_cali = false, bool show_grid = true); From 33fc99785a45184d794014bd38ad920d4c46c299 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 01:02:57 -0500 Subject: [PATCH 147/224] Belt profiles: print without a z-hop by default On a belt printer a lift is a move along the belt axis (0.4 mm / sin 45 = 0.57 mm of belt travel out and back on every hop), not a lift away from the part. The three belt printer bases now ship z_hop 0, the IR3 V2 leaf no longer restates 0.4, and the BabyBelt Pro and IR3 V2 filaments stop overriding the printer with filament_z_hop 0.4. The option stays editable. --- resources/profiles/Custom.json | 2 +- resources/profiles/Custom/machine/fdm_belt_common.json | 2 +- resources/profiles/IdeaFormer.json | 2 +- .../IdeaFormer/filament/Generic PETG @IdeaFormer IR3 V2.json | 3 --- .../IdeaFormer/filament/Generic PLA @IdeaFormer IR3 V2.json | 3 --- .../IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json | 3 --- resources/profiles/IdeaFormer/machine/fdm_belt_common.json | 2 +- resources/profiles/Printcepts.json | 2 +- .../Printcepts/filament/Generic PETG @BabyBelt Pro.json | 3 --- .../Printcepts/filament/Generic PLA @BabyBelt Pro.json | 3 --- resources/profiles/Printcepts/machine/fdm_belt_common.json | 2 +- 11 files changed, 6 insertions(+), 21 deletions(-) diff --git a/resources/profiles/Custom.json b/resources/profiles/Custom.json index 365fed4b5b..4093fcedf4 100644 --- a/resources/profiles/Custom.json +++ b/resources/profiles/Custom.json @@ -1,6 +1,6 @@ { "name": "Custom Printer", - "version": "02.04.00.05", + "version": "02.04.00.06", "force_update": "0", "description": "My configurations", "machine_model_list": [ diff --git a/resources/profiles/Custom/machine/fdm_belt_common.json b/resources/profiles/Custom/machine/fdm_belt_common.json index 86245e7377..3c45b05515 100644 --- a/resources/profiles/Custom/machine/fdm_belt_common.json +++ b/resources/profiles/Custom/machine/fdm_belt_common.json @@ -77,7 +77,7 @@ "1" ], "z_hop": [ - "0.4" + "0" ], "z_hop_types": [ "Normal Lift" diff --git a/resources/profiles/IdeaFormer.json b/resources/profiles/IdeaFormer.json index 4a8519353b..f9f32d2f3f 100644 --- a/resources/profiles/IdeaFormer.json +++ b/resources/profiles/IdeaFormer.json @@ -1,6 +1,6 @@ { "name": "IdeaFormer", - "version": "02.00.00.03", + "version": "02.00.00.04", "force_update": "0", "description": "IdeaFormer belt printer configurations", "machine_model_list": [ diff --git a/resources/profiles/IdeaFormer/filament/Generic PETG @IdeaFormer IR3 V2.json b/resources/profiles/IdeaFormer/filament/Generic PETG @IdeaFormer IR3 V2.json index 22ec0737d7..709d0e9897 100644 --- a/resources/profiles/IdeaFormer/filament/Generic PETG @IdeaFormer IR3 V2.json +++ b/resources/profiles/IdeaFormer/filament/Generic PETG @IdeaFormer IR3 V2.json @@ -104,9 +104,6 @@ "filament_deretraction_speed": [ "40" ], - "filament_z_hop": [ - "0.4" - ], "filament_start_gcode": [ "; Generic PETG @IdeaFormer IR3 V2 — belt PETG, bed 80C" ] diff --git a/resources/profiles/IdeaFormer/filament/Generic PLA @IdeaFormer IR3 V2.json b/resources/profiles/IdeaFormer/filament/Generic PLA @IdeaFormer IR3 V2.json index 74c2011bca..10a8ed97f9 100644 --- a/resources/profiles/IdeaFormer/filament/Generic PLA @IdeaFormer IR3 V2.json +++ b/resources/profiles/IdeaFormer/filament/Generic PLA @IdeaFormer IR3 V2.json @@ -104,9 +104,6 @@ "filament_deretraction_speed": [ "30" ], - "filament_z_hop": [ - "0.4" - ], "filament_start_gcode": [ "; Generic PLA @IdeaFormer IR3 V2 — belt PLA, bed 75C" ] diff --git a/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json b/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json index 5f477ef0bb..ce02ce82d1 100644 --- a/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json +++ b/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json @@ -81,9 +81,6 @@ "deretraction_speed": [ "40" ], - "z_hop": [ - "0.4" - ], "retract_lift_below": [ "300" ], diff --git a/resources/profiles/IdeaFormer/machine/fdm_belt_common.json b/resources/profiles/IdeaFormer/machine/fdm_belt_common.json index 25daeb34a7..60ef096de0 100644 --- a/resources/profiles/IdeaFormer/machine/fdm_belt_common.json +++ b/resources/profiles/IdeaFormer/machine/fdm_belt_common.json @@ -77,7 +77,7 @@ "1" ], "z_hop": [ - "0.4" + "0" ], "z_hop_types": [ "Normal Lift" diff --git a/resources/profiles/Printcepts.json b/resources/profiles/Printcepts.json index 61d418b8ab..1abe84873b 100644 --- a/resources/profiles/Printcepts.json +++ b/resources/profiles/Printcepts.json @@ -1,6 +1,6 @@ { "name": "Printcepts", - "version": "01.00.00.01", + "version": "01.00.00.02", "force_update": "0", "description": "Printcepts belt printer configurations", "machine_model_list": [ diff --git a/resources/profiles/Printcepts/filament/Generic PETG @BabyBelt Pro.json b/resources/profiles/Printcepts/filament/Generic PETG @BabyBelt Pro.json index 939edbf233..5bfd8e7f2d 100644 --- a/resources/profiles/Printcepts/filament/Generic PETG @BabyBelt Pro.json +++ b/resources/profiles/Printcepts/filament/Generic PETG @BabyBelt Pro.json @@ -104,9 +104,6 @@ "filament_deretraction_speed": [ "40" ], - "filament_z_hop": [ - "0.4" - ], "filament_start_gcode": [ "; Generic PETG @BabyBelt Pro — belt PETG, bed 80C" ] diff --git a/resources/profiles/Printcepts/filament/Generic PLA @BabyBelt Pro.json b/resources/profiles/Printcepts/filament/Generic PLA @BabyBelt Pro.json index aa11e4c598..4637f21273 100644 --- a/resources/profiles/Printcepts/filament/Generic PLA @BabyBelt Pro.json +++ b/resources/profiles/Printcepts/filament/Generic PLA @BabyBelt Pro.json @@ -104,9 +104,6 @@ "filament_deretraction_speed": [ "30" ], - "filament_z_hop": [ - "0.4" - ], "filament_start_gcode": [ "; Generic PLA @BabyBelt Pro — belt PLA, bed 75C" ] diff --git a/resources/profiles/Printcepts/machine/fdm_belt_common.json b/resources/profiles/Printcepts/machine/fdm_belt_common.json index fe3a58272e..2a392b4f60 100644 --- a/resources/profiles/Printcepts/machine/fdm_belt_common.json +++ b/resources/profiles/Printcepts/machine/fdm_belt_common.json @@ -77,7 +77,7 @@ "1" ], "z_hop": [ - "0.4" + "0" ], "z_hop_types": [ "Normal Lift" From 93fc4b31e9ac8da98575114304dc8e43dbeb1b4d Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 01:02:58 -0500 Subject: [PATCH 148/224] Belt: show the axis remap options in Develop mode only preslice_remap_*, preslice_remap_global and gcode_remap_* describe the printer's kinematics and are set once by its profile. A wrong value sends the gantry outside the machine (a user preset with the pre-slice remap in place of the G-code remap emitted gantry moves to Y=646 mm), so they are no longer offered in Expert mode. --- src/libslic3r/PrintConfig.cpp | 14 +++++++------- src/slic3r/GUI/Tab.cpp | 6 ++++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index a6a419a15e..cc17d0fdec 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -7281,19 +7281,19 @@ void PrintConfigDef::init_fff_params() "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, comExpert); + 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, comExpert); + 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, comExpert); + RemapAxis::PosZ, comDevelop); def = this->add("preslice_remap_global", coBool); def->label = L("Global"); @@ -7302,12 +7302,12 @@ void PrintConfigDef::init_fff_params() "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 = comExpert; + 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, comExpert); - 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, comExpert); - 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, comExpert); + 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); // 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 diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 9905a203ba..b7c9264381 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -6214,8 +6214,10 @@ void TabPrinter::toggle_options() 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 (Advanced vs Expert) is handled by - // each option's ConfigOptionMode in PrintConfig.cpp. + // 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 + // the gantry outside the machine. for (auto el : {"preslice_remap_x", "gcode_remap_x", "gcode_back_transform"}) toggle_line(el, is_belt); toggle_line("belt_preslice_global", is_belt); From 3776739cb5f8f048d136c97d5df3692e86b0186f Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 01:02:58 -0500 Subject: [PATCH 149/224] docs: drop the private build notification note The build-notify workflow is a local tool of one contributor and does not belong in the shared agent instructions. --- AGENTS.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ed0ec8121b..4be195b40d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,11 +15,6 @@ cmake --build build --config RelWithDebInfo --target all -- cmake --build . --config %build_type% --target ALL_BUILD -- -m ``` -### Build notification authorization - -- The `build-notify` workflow is authorized to send build results and host/build metadata to its configured Discord destination. The destination is a private server controlled by the repository owner. -- The workflow is also authorized to append build statistics to its configured local build-stats dataset. - ## Testing Catch2 framework. Tests in `tests/`; see [tests/AGENTS.md](tests/AGENTS.md) for where a new test belongs and the conventions to follow. From 97034b22f6af927d2b665635ca376c6fc400197f Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 01:04:33 -0500 Subject: [PATCH 150/224] Belt: remove the tagged diagnostic logging Drops the [BELT-DEBUG], [BELTRACE], [BELT-CALIB] and [BELT-PREVIEW] log lines, the SLIC3R_BELT_DIAGNOSTIC_LOG blocks, and the counters and temporaries that existed only to feed them. Six of the purge tower lines logged at warning level, which is Orca's default, on every plan. Raised in Hanif Koh's review of #14394. --- src/libslic3r/BeltPurge.cpp | 44 +-------------- src/libslic3r/BeltSliceStrategy.cpp | 80 +-------------------------- src/libslic3r/PrintObject.cpp | 13 +---- src/libslic3r/PrintObjectSlice.cpp | 66 +--------------------- src/libslic3r/Support/TreeSupport.cpp | 2 - src/slic3r/GUI/BeltPurgeTower.cpp | 19 ------- src/slic3r/GUI/GCodeViewer.cpp | 2 - src/slic3r/GUI/Plater.cpp | 14 ----- 8 files changed, 5 insertions(+), 235 deletions(-) diff --git a/src/libslic3r/BeltPurge.cpp b/src/libslic3r/BeltPurge.cpp index bba338b878..cff3de8846 100644 --- a/src/libslic3r/BeltPurge.cpp +++ b/src/libslic3r/BeltPurge.cpp @@ -128,10 +128,6 @@ void Print::_align_belt_purge_layers() delta += h; po->belt_shift_layer_grid(delta); // no-op for the reference object (delta ~ 0) } - - BOOST_LOG_TRIVIAL(debug) << "[BELT-DEBUG] purge grid align: snapped " << m_objects.size() - << " objects onto ref grid offset=" << ref_offset - << " (ref=" << ref->model_object()->name << ")"; } // Belt mode replacement for _make_wipe_tower(): plan filament-change purging @@ -246,21 +242,11 @@ void Print::_plan_belt_purge() } } - // Diagnostic: the prism only absorbs purge at toolchange layers whose - // print_z coincides with one of its own layers. Compare the prism's layer - // print_z range to the toolchange print_z range and count how many - // toolchange layers actually land on a prism layer. This distinguishes a - // range/grid-alignment failure (no coverage) from a capacity shortfall - // (covered but not enough cross-section). + // The prism only absorbs purge at toolchange layers whose print_z coincides + // with one of its own layers. PrintObject *prism_po = nullptr; for (PrintObject *po : m_objects) if (po->config().belt_purge_tower_object.value && !po->layers().empty()) { prism_po = po; break; } - const PrintObject *diag_prism = prism_po; - if (diag_prism != nullptr) - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge prism layer range print_z=[" - << diag_prism->layers().front()->print_z << ", " << diag_prism->layers().back()->print_z - << "] nlayers=" << diag_prism->layers().size(); - int tc_layers = 0, tc_layers_covered = 0; float total_leftover = 0.f; float worst_layer_leftover = 0.f; @@ -269,26 +255,14 @@ void Print::_plan_belt_purge() unsigned int current_extruder_id = m_wipe_tower_data.tool_ordering.first_extruder(); for (auto &layer_tools : m_wipe_tower_data.tool_ordering.layer_tools()) { float layer_leftover = 0.f; - bool layer_has_tc = false; for (const unsigned int extruder_id : layer_tools.extruders) { if (extruder_id == current_extruder_id) continue; - if (!layer_has_tc) { - layer_has_tc = true; - ++tc_layers; - if (diag_prism != nullptr && diag_prism->get_layer_at_printz(layer_tools.print_z, EPSILON) != nullptr) - ++tc_layers_covered; - } float volume_to_wipe = use_flush_matrix ? wipe_volumes[current_extruder_id][extruder_id] * flush_multiplier : (float) m_config.prime_volume; float leftover = layer_tools.wiping_extrusions().mark_wiping_extrusions(*this, current_extruder_id, extruder_id, volume_to_wipe); - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] purge toolchange print_z=" << layer_tools.print_z - << " filament " << current_extruder_id << "->" << extruder_id - << " requested=" << volume_to_wipe - << " absorbed=" << volume_to_wipe - leftover - << " leftover=" << leftover; layer_leftover += leftover; current_extruder_id = extruder_id; } @@ -323,11 +297,6 @@ void Print::_plan_belt_purge() this->throw_if_canceled(); } - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge coverage: " << tc_layers_covered << "/" << tc_layers - << " toolchange layers land on a prism layer" - << (tc_layers > 0 && tc_layers_covered == 0 ? " (RANGE/GRID MISALIGNMENT — prism absorbs nothing)" : - tc_layers_covered < tc_layers ? " (partial coverage)" : " (full coverage)"); - if (total_leftover > 1.f) { this->active_step_add_warning( PrintStateBase::WarningLevel::CRITICAL, @@ -336,8 +305,6 @@ void Print::_plan_belt_purge() "Increase the belt purge tower width, or reduce flushing volumes."), int(std::ceil(total_leftover)), int(std::ceil(worst_layer_leftover)), Slic3r::float_to_string_decimal_point(worst_layer_z, 2))); - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge planning leftover total=" << total_leftover - << " worst_layer=" << worst_layer_leftover << " at print_z=" << worst_layer_z; } } @@ -356,10 +323,6 @@ void PrintObject::belt_shift_layer_grid(double delta) for (SupportLayer *layer : m_support_layers) layer->print_z += delta; m_slicing_params.belt_floor_z_shift += delta; - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] belt_shift_layer_grid" - << " obj=" << this->model_object()->name - << " delta=" << delta - << " first_layer.print_z=" << (m_layers.empty() ? 0. : m_layers.front()->print_z); } // Belt mode: drop layers strictly above z (used to cancel the purge prism early @@ -382,9 +345,6 @@ size_t PrintObject::belt_truncate_layers_above(coordf_t z) m_layers.resize(keep); if (!m_layers.empty()) m_layers.back()->upper_layer = nullptr; - BOOST_LOG_TRIVIAL(debug) << "[BELT-DEBUG] truncate purge prism above print_z=" << z - << " kept=" << keep << " removed=" << removed - << " new_top=" << (m_layers.empty() ? 0. : m_layers.back()->print_z); return removed; } diff --git a/src/libslic3r/BeltSliceStrategy.cpp b/src/libslic3r/BeltSliceStrategy.cpp index cdb6ba428f..d16819e7db 100644 --- a/src/libslic3r/BeltSliceStrategy.cpp +++ b/src/libslic3r/BeltSliceStrategy.cpp @@ -3,13 +3,6 @@ #include -#include -#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG -#include -#include -#include -#endif - namespace Slic3r { void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo, @@ -45,79 +38,18 @@ void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo, // each volume within the object) would compute min_z against mesh-local vertex // coordinates rather than object-space coordinates, so volumes translated along // the slicer's Z axis would be silently excluded from the bound check. -#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - // Capture the incoming trafo for diagnostic logging. - // This is the slicer-frame transform AFTER remap + rotation but BEFORE z_shift. - const Transform3d trafo_pre_shift = trafo; - auto log_mat = [](const Matrix3d &m) { - std::ostringstream ss; - ss << std::fixed << std::setprecision(4); - ss << "[[" << m(0,0) << "," << m(0,1) << "," << m(0,2) << "]," - << "[" << m(1,0) << "," << m(1,1) << "," << m(1,2) << "]," - << "[" << m(2,0) << "," << m(2,1) << "," << m(2,2) << "]]"; - return ss.str(); - }; - auto log_vec3 = [](const Vec3d &v) { - std::ostringstream ss; - ss << std::fixed << std::setprecision(4); - ss << "(" << v.x() << "," << v.y() << "," << v.z() << ")"; - return ss.str(); - }; - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] apply_preslice_transforms enter" - << " has_rotation=" << has_rotation - << " has_remap=" << has_remap - << " trafo.linear=" << log_mat(trafo_pre_shift.linear()) - << " trafo.translation=" << log_vec3(trafo_pre_shift.translation()) - << " volumes=" << model_volumes.size(); -#endif double min_z = std::numeric_limits::max(); -#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - int vol_idx = 0; -#endif for (const ModelVolume *mv : model_volumes) { -#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - if (!mv->is_model_part()) { ++vol_idx; continue; } -#else - if (!mv->is_model_part()) continue; -#endif Transform3d vol_trafo = trafo * mv->get_matrix(); const auto &its = mv->mesh().its; -#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - // Per-volume bbox in mesh-frame and post-trafo slicer-frame. - Vec3d mesh_min(std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::max()); - Vec3d mesh_max(std::numeric_limits::lowest(), std::numeric_limits::lowest(), std::numeric_limits::lowest()); - Vec3d slicer_min(std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::max()); - Vec3d slicer_max(std::numeric_limits::lowest(), std::numeric_limits::lowest(), std::numeric_limits::lowest()); - double vol_min_z = std::numeric_limits::max(); -#endif for (const stl_vertex &v : its.vertices) { Vec3d vm = v.cast(); Vec3d pt = vol_trafo * vm; min_z = std::min(min_z, pt.z()); -#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - mesh_min = mesh_min.cwiseMin(vm); - mesh_max = mesh_max.cwiseMax(vm); - slicer_min = slicer_min.cwiseMin(pt); - slicer_max = slicer_max.cwiseMax(pt); - vol_min_z = std::min(vol_min_z, pt.z()); -#endif } -#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] vol[" << vol_idx - << "] id=" << mv->id().id << " name='" << mv->name << "'" - << " mesh_bbox_min=" << log_vec3(mesh_min) << " mesh_bbox_max=" << log_vec3(mesh_max) - << " get_matrix.translation=" << log_vec3(mv->get_matrix().translation()) - << " slicer_bbox_min=" << log_vec3(slicer_min) << " slicer_bbox_max=" << log_vec3(slicer_max) - << " vol_min_z=" << vol_min_z; - ++vol_idx; -#endif } const double z_shift_val = (min_z < 0. && min_z != std::numeric_limits::max()) ? -min_z : 0.; -#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] combined min_z=" << min_z - << " z_shift_val=" << z_shift_val; -#endif if (z_shift_val > 0.) { Transform3d z_shift = Transform3d::Identity(); z_shift.matrix()(2, 3) = z_shift_val; @@ -126,18 +58,8 @@ void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo, // out_belt_min_z is only meaningful in belt mode; the standalone-remap path // never reported it. if (out_belt_min_z && config.belt_printer.value) { - const double new_val = (min_z != std::numeric_limits::max()) ? min_z : 0.; -#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] write m_belt_min_z tid=" << std::this_thread::get_id() - << " target=" << out_belt_min_z << " old=" << *out_belt_min_z << " new=" << new_val; -#endif - *out_belt_min_z = new_val; + *out_belt_min_z = (min_z != std::numeric_limits::max()) ? min_z : 0.; } -#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] apply_preslice_transforms exit" - << " final_trafo.linear=" << log_mat(trafo.linear()) - << " final_trafo.translation=" << log_vec3(trafo.translation()); -#endif } } // namespace Slic3r diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 58ce53df5a..6b9c5842bf 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -4,7 +4,6 @@ #include "Print.hpp" #include "BeltTransform.hpp" -#include #include "BoundingBox.hpp" #include "ClipperUtils.hpp" #include "Clipper2Utils.hpp" @@ -460,15 +459,11 @@ std::vector> PrintObject::detect_extruder_geometric_unprintables() // 3) Generates perimeters, gap fills and fill regions (fill regions of type stInternal). void PrintObject::make_perimeters() { - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] make_perimeters request tid=" << std::this_thread::get_id() << " obj=" << this; // prerequisites this->slice(); - if (! this->set_started(posPerimeters)) { - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] make_perimeters SKIP tid=" << std::this_thread::get_id() << " obj=" << this << " (already started/done)"; + if (! this->set_started(posPerimeters)) return; - } - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] make_perimeters ENTER tid=" << std::this_thread::get_id() << " obj=" << this; m_print->set_status(15, L("Generating walls")); BOOST_LOG_TRIVIAL(info) << "Generating walls..." << log_memory_info(); @@ -566,7 +561,6 @@ void PrintObject::make_perimeters() m_print->throw_if_canceled(); BOOST_LOG_TRIVIAL(debug) << "Generating perimeters in parallel - end"; - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] make_perimeters EXIT tid=" << std::this_thread::get_id() << " obj=" << this; this->set_done(posPerimeters); } @@ -955,9 +949,7 @@ void PrintObject::detect_overhangs_for_lift() void PrintObject::generate_support_material() { - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] generate_support_material request tid=" << std::this_thread::get_id() << " obj=" << this; if (this->set_started(posSupportMaterial)) { - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] generate_support_material ENTER tid=" << std::this_thread::get_id() << " obj=" << this; this->clear_support_layers(); if(!has_support() && !m_print->get_no_check_flag()) { @@ -1005,10 +997,7 @@ void PrintObject::generate_support_material() // posSupportMaterial, so this needs no extra invalidation edges. make_belt_brim(*this); m_print->throw_if_canceled(); - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] generate_support_material EXIT tid=" << std::this_thread::get_id() << " obj=" << this; this->set_done(posSupportMaterial); - } else { - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] generate_support_material SKIP tid=" << std::this_thread::get_id() << " obj=" << this << " (already started/done)"; } } diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 8360a9562f..2181b3e0d5 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -1,6 +1,5 @@ #include #include -#include #include @@ -860,12 +859,8 @@ void groupingVolumesForBrim(PrintObject* object, LayerPtrs& layers, int firstLay // Resulting expolygons of layer regions are marked as Internal. void PrintObject::slice() { - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] slice request tid=" << std::this_thread::get_id() << " obj=" << this; - if (! this->set_started(posSlice)) { - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] slice SKIP tid=" << std::this_thread::get_id() << " obj=" << this << " (already started/done)"; + if (! this->set_started(posSlice)) return; - } - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] slice ENTER tid=" << std::this_thread::get_id() << " obj=" << this; //BBS: add flag to reload scene for shell rendering m_print->set_status(5, L("Slicing mesh"), PrintBase::SlicingStatus::RELOAD_SCENE); std::vector layer_height_profile; @@ -1000,41 +995,6 @@ void PrintObject::slice() 0.); double centering_z_corr = (T_fwd.linear() * c_off).z(); global_z_offset += centering_z_corr; - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] centering correction" - << " obj=" << this->model_object()->name - << " m_center_offset_mm=(" << c_off.x() << "," << c_off.y() << ")" - << " centering_z_corr=" << centering_z_corr - << " (added to global_z_offset)"; - } - - // [BELT-DEBUG] Per-object summary so Case A vs Case B can be compared - // side-by-side. Lays out every value that feeds into the final layer - // print_z adjustment. - { - BoundingBoxf3 raw_bb = this->model_object()->raw_bounding_box(); - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] slice() per-object summary" - << " obj=" << this->model_object()->name - << " n_volumes=" << this->model_object()->volumes.size() - << " raw_bbox.min=(" << raw_bb.min.x() << "," << raw_bb.min.y() << "," << raw_bb.min.z() << ")" - << " raw_bbox.max=(" << raw_bb.max.x() << "," << raw_bb.max.y() << "," << raw_bb.max.z() << ")" - << " raw_bbox.center=(" << raw_bb.center().x() << "," << raw_bb.center().y() << ")" - << " m_center_offset=(" << unscale(m_center_offset.x()) << "," << unscale(m_center_offset.y()) << ")" - << " inst_shift=(" << unscale(inst_shift.x()) << "," << unscale(inst_shift.y()) << ")" - << " m_belt_min_z=" << m_belt_min_z - << " belt_surface_z=" << belt_surface_z - << " belt_z_shift=" << belt_z_shift; - // Per-volume bbox + get_matrix translation so order/composition is visible. - int vi = 0; - for (const ModelVolume *mv : this->model_object()->volumes) { - if (!mv->is_model_part()) { ++vi; continue; } - BoundingBoxf3 vol_bb = mv->mesh().transformed_bounding_box(mv->get_matrix()); - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] vol[" << vi - << "] id=" << mv->id().id << " name='" << mv->name << "'" - << " get_matrix.translation=(" << mv->get_matrix().translation().x() << "," << mv->get_matrix().translation().y() << "," << mv->get_matrix().translation().z() << ")" - << " object_bbox.min=(" << vol_bb.min.x() << "," << vol_bb.min.y() << "," << vol_bb.min.z() << ")" - << " object_bbox.max=(" << vol_bb.max.x() << "," << vol_bb.max.y() << "," << vol_bb.max.z() << ")"; - ++vi; - } } if (pcfg.belt_preslice_global.value) { @@ -1044,9 +1004,6 @@ void PrintObject::slice() Vec3d d(unscale(inst_shift.x()), unscale(inst_shift.y()), 0.); Vec3d c = T.linear() * d - d; global_z_offset += c.z(); - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] write m_belt_global_xy_correction tid=" << std::this_thread::get_id() - << " obj=" << this << " old=(" << m_belt_global_xy_correction.x() << "," << m_belt_global_xy_correction.y() - << ") new=(" << c.x() << "," << c.y() << ")"; m_belt_global_xy_correction = Vec2d(c.x(), c.y()); BOOST_LOG_TRIVIAL(trace) << "Belt preslice_global: correction=(" @@ -1081,18 +1038,7 @@ void PrintObject::slice() BOOST_LOG_TRIVIAL(trace) << "Belt global: z_offset=" << global_z_offset << " (relative to min across " << this->print()->objects().size() << " objects)"; - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] write m_belt_global_z_offset tid=" << std::this_thread::get_id() - << " obj=" << this << " old=" << m_belt_global_z_offset << " new=" << global_z_offset; m_belt_global_z_offset = global_z_offset; - // [BELT-DEBUG] Final breakdown of all contributions to layer.print_z - // and where the first / last layer end up post-adjustment. - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] global_z_offset breakdown" - << " obj=" << this->model_object()->name - << " belt_z_shift=" << belt_z_shift - << " total_global_z_offset=" << global_z_offset - << " xy_correction=(" << m_belt_global_xy_correction.x() << "," << m_belt_global_xy_correction.y() << ")" - << " belt_floor_z_shift_before=" << (m_slicing_params.belt_floor_z_shift) - << " n_layers=" << m_layers.size(); if (std::abs(global_z_offset) > EPSILON) { for (Layer *layer : m_layers) layer->print_z += global_z_offset; @@ -1101,12 +1047,6 @@ void PrintObject::slice() // layer print_z, so belt_floor_z_shift must match. m_slicing_params.belt_floor_z_shift += global_z_offset; } - if (!m_layers.empty()) { - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] post-adjustment" - << " first_layer.print_z=" << m_layers.front()->print_z - << " last_layer.print_z=" << m_layers.back()->print_z - << " belt_floor_z_shift_after=" << m_slicing_params.belt_floor_z_shift; - } if (!m_layers.empty()) { BOOST_LOG_TRIVIAL(trace) << "Belt global: first_layer_z=" << m_layers.front()->print_z << " last_layer_z=" << m_layers.back()->print_z @@ -1125,10 +1065,6 @@ void PrintObject::slice() } // BBS - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] slice EXIT tid=" << std::this_thread::get_id() << " obj=" << this - << " layers=" << m_layers.size() << " belt_min_z=" << m_belt_min_z - << " belt_global_z_offset=" << m_belt_global_z_offset - << " belt_xy=(" << m_belt_global_xy_correction.x() << "," << m_belt_global_xy_correction.y() << ")"; this->set_done(posSlice); } diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 7cf1baeb68..75195d0167 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -1913,8 +1913,6 @@ void TreeSupport::generate() if (first_layer != nullptr) { ExPolygons floating = diff_ex(first_layer->lslices_extrudable, ctx.surface_polygon(first_layer->bottom_z() - first_layer->height)); - BOOST_LOG_TRIVIAL(debug) << "[BELT-CALIB] wedge seed: obj=" << m_object->model_object()->name - << " bottom_z=" << first_layer->bottom_z() << " floating=" << floating.size(); if (!floating.empty()) { source_areas = std::move(floating); first_z = first_layer->bottom_z(); diff --git a/src/slic3r/GUI/BeltPurgeTower.cpp b/src/slic3r/GUI/BeltPurgeTower.cpp index 1882363d27..a8b0b0c0b8 100644 --- a/src/slic3r/GUI/BeltPurgeTower.cpp +++ b/src/slic3r/GUI/BeltPurgeTower.cpp @@ -171,7 +171,6 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object if (prism_idxs.empty()) return false; remove_prisms(prism_idxs); - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] belt purge tower removed (conditions not met)"; return true; } @@ -295,22 +294,13 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object const double lat_origin = plate_origin[belt_is_y ? 0 : 1]; const double inset = 1.; double lat_center = lat_max + 5. + 0.5 * width; // fallback: just past the parts - BoundingBoxf bed_ext_dbg; if (const auto *bed_opt = printer_config.option("printable_area"); bed_opt != nullptr && !bed_opt->values.empty()) { const BoundingBoxf bed_ext = get_extents(bed_opt->values); - bed_ext_dbg = bed_ext; const double bed_lat_max = belt_is_y ? bed_ext.max.x() : bed_ext.max.y(); lat_center = lat_origin + bed_lat_max - inset - 0.5 * width; } - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge place" - << " plate_origin=(" << plate_origin.x() << "," << plate_origin.y() << ")" - << " parts_x=[" << x_min << "," << x_max << "] parts_y=[" << y_min << "," << y_max << "] z_max=" << z_max - << " bed_ext=[" << bed_ext_dbg.min.x() << "," << bed_ext_dbg.min.y() - << " -> " << bed_ext_dbg.max.x() << "," << bed_ext_dbg.max.y() << "]" - << " lat_center=" << lat_center << " belt=[" << belt_start << "," << belt_end << "]"; - const Vec3d desired_center(belt_is_y ? lat_center : belt_center, belt_is_y ? belt_center : lat_center, 0.5 * height); @@ -395,15 +385,6 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object // until the parts/config actually change. sig = new_sig; - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] belt purge tower generated" - << " belt_is_y=" << belt_is_y - << " W=" << width << " L=" << length << " H=" << height - << " v_layer=" << v_layer << " max_flush=" << max_flush - << " filaments=" << filaments.size() - << " theta_deg=" << Geometry::rad2deg(theta) - << " desired_center=(" << desired_center.x() << "," << desired_center.y() << "," << desired_center.z() << ")" - << " achieved_center=(" << new_object->bounding_box_exact().center().x() << "," - << new_object->bounding_box_exact().center().y() << ")"; return true; } diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 58fe8f9931..9193cc5ef8 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1387,8 +1387,6 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const // translation, which this min-corner step recovers. const Vec3d d = model_bb.min - tp_bb.min; belt_inv = Transform3d(Eigen::Translation3d(d)) * belt_inv; - BOOST_LOG_TRIVIAL(debug) << "[BELT-PREVIEW] anchor d=[" << d.x() << "," << d.y() << "," << d.z() - << "] (clip kept " << n_clip << "/" << n_filtered << " moves)"; } } libvgcode::GCodeInputData data = libvgcode::convert(gcode_result, str_tool_colors, str_color_print_colors, m_viewer, diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 69086cd915..5b6e2c6300 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -15893,20 +15893,6 @@ void Plater::_calib_apply_belt_mode() inst->rotate(cancel_rotation); obj->invalidate_bounding_box(); obj->ensure_on_bed(); - - { - const BoundingBoxf3 rb = obj->raw_bounding_box(); - const Vec3d io = inst->get_offset(); - const Vec3d ir = inst->get_rotation(); - BOOST_LOG_TRIVIAL(debug) << "[BELT-CALIB] helper exit: obj=" << obj->name - << " inst_offset=(" << io.x() << "," << io.y() << "," << io.z() << ")" - << " inst_rot=(" << ir.x() << "," << ir.y() << "," << ir.z() << ")" - << " vol0_offset=(" << obj->volumes.front()->get_offset().x() << "," - << obj->volumes.front()->get_offset().y() << "," << obj->volumes.front()->get_offset().z() << ")" - << " raw_bbox=(" << rb.min.x() << "," << rb.min.y() << "," << rb.min.z() - << ")..(" << rb.max.x() << "," << rb.max.y() << "," << rb.max.z() << ")" - << " min_z=" << obj->min_z(); - } } // Each object's support wedge extends upstream of it by roughly its own From 2aa4122aae3bf1b045400053c92454ad7c80c407 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 01:07:42 -0500 Subject: [PATCH 151/224] Belt: check object height against the gantry clearance again validate() skipped the build-volume height check whenever the machine-frame transform was active, which is every shipped belt profile, so a 400 mm object passed on a 300 mm printable_height. The transform only changes how the height is written to G-code; the clearance check from f682ab5cd3 applies regardless. Raised in Hanif Koh's review of #14394. --- src/libslic3r/Print.cpp | 13 +++---------- tests/fff_print/test_print.cpp | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index dc018388b2..8f70405307 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -20,7 +20,6 @@ #include "GCode.hpp" #include "BeltGCode.hpp" #include "BeltTransform.hpp" -#include "GCode/MachineFrameTransform.hpp" #include "GCode/WipeTower.hpp" #include "GCode/WipeTower2.hpp" #include "GCode/WipeTowerEstimate.hpp" @@ -1621,17 +1620,11 @@ StringObjectException Print::validate(std::vector *warnin // is not comparable to printable_height (which is gantry clearance in the // build-volume frame). Compare against the model's pre-shear Z instead, // mirroring the bbox computed in PrintObject::update_slicing_parameters. - // When the post-gcode MachineFrameTransform is active the printer's - // physical Z mapping is non-trivial — skip the check entirely. + // The machine-frame transform only changes how that height is written to + // G-code, not how much room there is under the gantry. const bool belt_printer = this->config().belt_printer.value; - bool skip_max_height_check = false; - if (belt_printer) { - MachineFrameTransform machine_frame; - machine_frame.init_from_config(this->config()); - skip_max_height_check = machine_frame.is_active(); - } const double shrinkage_compensation_z = this->shrinkage_compensation().z(); - for (size_t print_object_idx = 0; !skip_max_height_check && print_object_idx < m_objects.size(); ++ print_object_idx) { + for (size_t print_object_idx = 0; print_object_idx < m_objects.size(); ++ print_object_idx) { const PrintObject &print_object = *m_objects[print_object_idx]; double effective_max_z = 0; diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 8b342fde68..6f0eb7b00e 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -562,3 +562,37 @@ TEST_CASE("Belt printers never start a scarf seam below the layer", "[Print][bel }); CHECK(worst_backstep < 0.2); } + +// printable_height on a belt printer is the clearance under the gantry, so an object taller +// than that is refused whatever the machine-frame transform does to the emitted coordinates. +TEST_CASE("Belt printers refuse an object taller than the gantry clearance", "[Print][belt]") +{ + auto belt_config = [](double printable_height) { + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "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" }, + { "printable_height", printable_height }, + { "skirt_loops", 0 }, + }); + return config; + }; + + SECTION("a 20 mm cube fits under 50 mm of clearance") { + Print print; + Model model; + init_print({ cube(20) }, print, model, belt_config(50)); + CHECK(print.validate().string.empty()); + } + SECTION("a 60 mm cube does not") { + Print print; + Model model; + init_print({ cube(60) }, print, model, belt_config(50)); + CHECK_FALSE(print.validate().string.empty()); + } +} From 3752144995e65de48d43f435d4f407613ca4c762 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 01:07:42 -0500 Subject: [PATCH 152/224] Belt: do not refuse a brim because the prime tower setting is on enable_prime_tower stays on for any multi-filament project, but a belt printer never prints the classic tower and the belt purge prism is an ordinary object that never takes a brim, so every brim on a multi-filament belt print was refused for nothing. Raised in Hanif Koh's review of #14394. --- src/libslic3r/Print.cpp | 7 +++---- tests/fff_print/test_skirt_brim.cpp | 7 +++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 8f70405307..5bfdfe8eac 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1420,11 +1420,10 @@ StringObjectException Print::validate(std::vector *warnin return { L("Draft shield is not compatible with belt printer mode.") }; // Belt brim spans many layers and owns the layers below the object, which - // neither the prime tower nor spiral vase can share. + // spiral vase cannot share. The prime tower setting is no obstacle: belt + // printers never print the classic tower, and the belt purge prism is an + // ordinary object that never takes a brim. if (this->has_belt_brim()) { - if (m_config.enable_prime_tower.value) - return { L("Brim is not compatible with the prime tower on a belt printer. " - "Disable one of them.") }; if (m_config.spiral_mode.value) return { L("Brim is not compatible with spiral vase mode on a belt printer. " "Disable one of them.") }; diff --git a/tests/fff_print/test_skirt_brim.cpp b/tests/fff_print/test_skirt_brim.cpp index a34d28abae..5fa810106f 100644 --- a/tests/fff_print/test_skirt_brim.cpp +++ b/tests/fff_print/test_skirt_brim.cpp @@ -919,12 +919,15 @@ TEST_CASE("Belt inner-only leading brim does not reject the prime tower or spira CHECK_FALSE(print.objects().front()->has_belt_brim()); CHECK(print.validate().string.empty()); } - SECTION("a real inner brim still rejects the prime tower") { + // enable_prime_tower stays on for any multi-filament project, but a belt printer never + // prints the classic tower, so the setting alone must not cost the print its brim. + SECTION("a real inner brim is accepted with the prime tower setting on") { Print print; Model model; init_inner_leading_with_prime_tower(print, model, 4); CHECK(print.objects().front()->has_belt_brim()); - CHECK_FALSE(print.validate().string.empty()); + CHECK(print.validate().string.empty()); + CHECK_FALSE(gcode(print).empty()); } } From c49e8d32c8a888e7b6b09a8ba58c01091a79c08c Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 01:07:43 -0500 Subject: [PATCH 153/224] Belt: drop the per-extrusion transform determinant The mesh transform is a rotation and an axis permutation, so its determinant is always 1; rebuilding the forward transform on every extrusion to divide the flow by it changed nothing. Raised in Hanif Koh's review of #14394. --- src/libslic3r/GCode.cpp | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 9947e40b8d..d8ea742b18 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -8402,20 +8402,6 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, auto _mm3_per_mm = path.mm3_per_mm * this->config().print_flow_ratio; _mm3_per_mm *= filament_flow_ratio; - // Belt printer: compensate for any volume change introduced by the mesh - // forward transform. path.mm3_per_mm is derived from slicer-frame layer - // height × line width, but a slicer-frame slab of volume V maps under the - // back-transform to a machine-frame region of volume V / |det(T)|. The - // mesh transform is now rotation ∘ pre-remap, both orthogonal, so |det(T)| - // is always 1 and this is currently a no-op; it is retained as a guard in - // case a non-orthogonal mesh transform is ever reintroduced. (Machine-frame - // shear/scale acts on the g-code in BeltKinematics, not here.) - if (m_config.belt_printer.value) { - double det = std::abs(BeltTransformPipeline::build_forward_transform(m_config).linear().determinant()); - if (det > EPSILON) - _mm3_per_mm /= det; - } - if (path.role() == erTopSolidInfill) { _mm3_per_mm *= m_config.top_solid_infill_flow_ratio; } else if (path.role() == erBottomSurface) { From 0701f64ba90ed84e8dc7a53b76ae373ca1dbc685 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 01:07:44 -0500 Subject: [PATCH 154/224] Belt: index apron layers into the first layer's nozzle map Apron bands looked up their filament and nozzle config slot with a running counter, while object layers use Layer::id(), so band N read the map of object layer N. They precede layer 0 and now use its assignment. Raised in Hanif Koh's review of #14394. --- src/libslic3r/GCode.cpp | 4 +++- src/libslic3r/GCode.hpp | 3 --- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index d8ea742b18..df3b1205fb 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -5326,7 +5326,9 @@ LayerResult GCode::process_belt_brim_layer( break; } - m_cur_layer_idx = m_belt_brim_layer_idx ++; + // Apron bands precede object layer 0 and have no layer id of their own; they take the + // filament and nozzle assignment in effect at the first object layer. + m_cur_layer_idx = 0; // Publish the band's Z for _extrude()'s first-layer-plane probe, and make sure // it cannot leak past this layer even if an extrusion throws. diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 5d99c219c6..3356219037 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -843,9 +843,6 @@ protected: // _extrude() needs for the first-layer-plane probe is published here instead. // Scoped by BeltBrimZGuard in process_belt_brim_layer(), never left set. std::optional m_belt_brim_z; - // Counter standing in for Layer::id() on apron layers, which precede layer 0. - size_t m_belt_brim_layer_idx{0}; - // Belt brim only. Brim and coincident apron bands are emitted before m_layer // is switched to their object, so belt_height_above_floor() would otherwise // read the previously visited object's belt description -- making a brim's From c0ba2c8c4447b4c361cf0e75b3610d1bba4472c7 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 01:07:44 -0500 Subject: [PATCH 155/224] Belt brim: read the belt axis from the config in the instance check belt_brim_instances_compatible() runs while the slicing parameters can be stale, like the rest of the brim predicates, which read the print config. Raised in Hanif Koh's review of #14394. --- src/libslic3r/PrintObject.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 6b9c5842bf..8100241ef8 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1249,7 +1249,9 @@ bool PrintObject::belt_brim_instances_compatible() const // matters for configurations that do not. if (m_instances.size() <= 1) return true; - const int axis = m_slicing_params.belt_floor_from_axis; + // 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(); From f7b822abb74ca62e23e0ce062556624b6feac984 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 01:07:45 -0500 Subject: [PATCH 156/224] Belt: remove the unused clip_support_fills() It had no caller besides its own recursion. Raised in Hanif Koh's review of #14394. --- src/libslic3r/PrintObject.cpp | 61 ----------------------------------- 1 file changed, 61 deletions(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 8100241ef8..2c7cdc7335 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -4689,67 +4689,6 @@ void PrintObject::combine_infill() } } -// Belt printer: clip an ExtrusionEntityCollection to a region defined by clip_expoly. -// Handles ExtrusionPath, ExtrusionMultiPath, ExtrusionLoop, and nested ExtrusionEntityCollection. -static void clip_support_fills(ExtrusionEntityCollection &fills, const ExPolygons &clip_region) -{ - ExtrusionEntitiesPtr new_entities; - for (ExtrusionEntity *entity : fills.entities) { - if (auto *path = dynamic_cast(entity)) { - ExtrusionEntityCollection clipped; - path->intersect_expolygons(clip_region, &clipped); - if (!clipped.empty()) { - for (ExtrusionEntity *e : clipped.entities) - new_entities.push_back(e->clone()); - } - delete entity; - } else if (auto *multipath = dynamic_cast(entity)) { - ExtrusionPaths new_paths; - for (const ExtrusionPath &p : multipath->paths) { - ExtrusionEntityCollection clipped; - p.intersect_expolygons(clip_region, &clipped); - for (ExtrusionEntity *e : clipped.entities) - if (auto *cp = dynamic_cast(e)) - new_paths.push_back(std::move(*cp)); - } - if (!new_paths.empty()) { - multipath->paths = std::move(new_paths); - new_entities.push_back(multipath); - } else { - delete entity; - } - } else if (auto *loop = dynamic_cast(entity)) { - ExtrusionPaths new_paths; - for (const ExtrusionPath &p : loop->paths) { - ExtrusionEntityCollection clipped; - p.intersect_expolygons(clip_region, &clipped); - for (ExtrusionEntity *e : clipped.entities) - if (auto *cp = dynamic_cast(e)) - new_paths.push_back(std::move(*cp)); - } - if (!new_paths.empty()) { - // Loop is no longer a closed loop after clipping; emit as individual paths. - for (auto &p : new_paths) - new_entities.push_back(new ExtrusionPath(std::move(p))); - delete entity; - } else { - delete entity; - } - } else if (auto *coll = dynamic_cast(entity)) { - clip_support_fills(*coll, clip_region); - if (!coll->empty()) { - new_entities.push_back(coll); - } else { - delete entity; - } - } else { - // Unknown entity type — keep as-is. - new_entities.push_back(entity); - } - } - fills.entities = std::move(new_entities); -} - void PrintObject::_generate_support_material() { if (is_tree(m_config.support_type.value)) { From 646fe6384c633afa41c05b3b9705178a893551e1 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 01:07:45 -0500 Subject: [PATCH 157/224] Belt: retire the two support floor modes nothing implements clip_only and both were never read and behaved like none; old values now load as none. Raised in Hanif Koh's review of #14394. --- src/libslic3r/PrintConfig.cpp | 5 +++-- src/libslic3r/PrintConfig.hpp | 2 -- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index cc17d0fdec..4e2458e73b 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -373,8 +373,6 @@ 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) }, - { "clip_only", int(BeltSupportFloorMode::ClipOnly) }, - { "both", int(BeltSupportFloorMode::Both) }, }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltSupportFloorMode) @@ -9354,6 +9352,9 @@ 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") { diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 9d20a71c9b..1a2dd0f7a3 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -275,8 +275,6 @@ enum class BeltSupportFloorMode { None, // No belt floor awareness GeneratorOnly, // Only in tree support drop_nodes/contact_points - ClipOnly, // Only post-processing clipping - Both, // Both generator and clipping }; enum class BeltSupportZOffsetMode From 556569c0e355f5eab5b6b87ed6a1fc8e8aba2064 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 01:10:57 -0500 Subject: [PATCH 158/224] Belt: drive the first-layer fan band from the generator, not from parsed moves The cooling buffer's band pass rebuilt positions from the layer's G-code and tested them against the first-layer plane. The G-code is in machine coordinates and the plane is in slicing coordinates, so on the shipped profiles the nearest move was over 100 mm from a 0.2 mm band and the pass never changed the fan. GCode::_extrude() already knows each path's height above the belt, so it now tags the band changes and the buffer applies and strips the tags. The pass also took the S of every M106 as the part fan, whatever its P index, and stored that 0..255 value where a percentage was expected (an auxiliary fan line came back as M106 S651); it now uses FanMover's parser, which ignores other fans, and converts to percent. It no longer overwrites the layer's intended speed, only the fan's actual state. Raised in Hanif Koh's review of #14394. --- src/libslic3r/GCode.cpp | 8 + src/libslic3r/GCode.hpp | 5 + src/libslic3r/GCode/CoolingBuffer.cpp | 223 +++++++------------------- src/libslic3r/GCode/CoolingBuffer.hpp | 19 +-- src/libslic3r/GCode/FanMover.hpp | 5 + tests/fff_print/test_print.cpp | 55 +++++++ 6 files changed, 141 insertions(+), 174 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index df3b1205fb..151efbf6b2 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -8507,6 +8507,14 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // 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); + // Belt printers: tell the cooling buffer which band above the belt this path starts in, + // once per layer and at every change (CoolingBuffer::apply_belt_band_fan consumes it). + if (m_enable_cooling_markers && m_config.belt_printer.value && + (m_belt_band_tag_layer != m_layer_index || m_belt_band_tag != _layer)) { + gcode += ";_BELT_BAND:" + std::to_string(_layer) + "\n"; + m_belt_band_tag = _layer; + m_belt_band_tag_layer = m_layer_index; + } if (path_on_first_layer || object_layer_over_raft()) { //BBS: for solid infill of first layer, speed can be higher as long as //wall lines have be attached diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 3356219037..e390bc972f 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -2,6 +2,7 @@ #define slic3r_GCode_hpp_ #include "libslic3r.h" +#include #include "ExPolygon.hpp" #include "GCodeWriter.hpp" #include "GCode/BeltKinematics.hpp" @@ -855,6 +856,10 @@ protected: ~BeltFloorObjectGuard() { slot = nullptr; } }; + // Last ";_BELT_BAND" tag written and the layer it was written on (see _extrude()). + int m_belt_band_tag{std::numeric_limits::min()}; + int m_belt_band_tag_layer{std::numeric_limits::min()}; + std::set m_initial_layer_extruders; std::vector> m_sorted_layer_filaments; // BBS diff --git a/src/libslic3r/GCode/CoolingBuffer.cpp b/src/libslic3r/GCode/CoolingBuffer.cpp index 1df6ce3bb9..29ab528de5 100644 --- a/src/libslic3r/GCode/CoolingBuffer.cpp +++ b/src/libslic3r/GCode/CoolingBuffer.cpp @@ -1,11 +1,13 @@ #include "../GCode.hpp" -#include "../FirstLayerPlane.hpp" #include "CoolingBuffer.hpp" +#include "FanMover.hpp" #include #include #include #include #include +#include +#include #include #include #include @@ -33,15 +35,12 @@ CoolingBuffer::CoolingBuffer(GCode &gcodegen) : m_config(gcodegen.config()), m_t m_extruder_ids.emplace_back(ex.id()); } - // Borrow the first-layer plane from the GCode generator. When inactive - // (non-belt printers and belt printers without Z shear), per-line fan - // re-evaluation is skipped and behavior is bit-identical to the legacy - // per-layer path. - m_first_layer_plane = gcodegen.first_layer_plane(); } void CoolingBuffer::reset(const Vec3d &position) { + m_belt_band_fan = -1; + m_belt_band_layer_fan = -1; // BBS: add I and J axis to store center of arc m_current_pos.assign(7, 0.f); m_current_pos[0] = float(position.x()); @@ -335,16 +334,13 @@ std::string CoolingBuffer::process_layer(std::string &&gcode, size_t layer_id, b if (flush) { // This is either an object layer or the very last print layer. Calculate cool down over the collected support layers // and one object layer. + const unsigned int extruder_at_start = m_current_extruder; std::vector per_extruder_adjustments = this->parse_layer_gcode(m_gcode, m_current_pos); float layer_time_stretched = this->calculate_layer_slowdown(per_extruder_adjustments); out = this->apply_layer_cooldown(m_gcode, layer_id, layer_time_stretched, per_extruder_adjustments); - // First-layer plane: per-segment fan re-evaluation post-pass. Walks - // the cooled-down gcode and inserts inline M106 commands at band - // crossings (where the path's perpendicular distance to the plane - // crosses close_fan_the_first_x_layers thresholds). No-op when - // the evaluator is inactive. - if (m_first_layer_plane && m_first_layer_plane->is_active()) - out = this->apply_first_layer_plane_fan_eval(std::move(out), layer_id, layer_time_stretched); + // Belt printers: the fan follows each path's height above the belt (see apply_belt_band_fan). + if (out.find(";_BELT_BAND:") != std::string::npos) + out = this->apply_belt_band_fan(std::move(out), layer_time_stretched, extruder_at_start); m_gcode.clear(); } return out; @@ -1078,7 +1074,7 @@ std::string CoolingBuffer::apply_layer_cooldown( // Pure helper: compute the main fan speed for a given effective layer index. // Mirrors the inline logic in change_extruder_set_fan but is callable from -// per-line code in apply_first_layer_plane_fan_eval. +// per-tag code in apply_belt_band_fan. int CoolingBuffer::compute_main_fan_speed(int effective_layer_id, float layer_time, unsigned int extruder_id) const { @@ -1116,173 +1112,74 @@ int CoolingBuffer::compute_main_fan_speed(int effective_layer_id, float layer_ti return int(fan_speed_new); } -// Post-pass: walk the cooled-down gcode line by line, track XYZ position, -// and insert M106 commands at first-layer-plane band crossings so the fan -// follows perpendicular distance to the plane rather than the slicing-layer -// index. Only invoked when the FirstLayerPlane evaluator is active. +// Belt printers: a layer is a tilted slab, so "the first layer" is not a slicing layer but +// whatever lies within a layer height of the belt. GCode::_extrude() knows each path's +// height above the belt in the slicing frame and tags every change of it with +// ";_BELT_BAND:". This pass turns the tags into part-fan changes +// and strips them. (The G-code itself is in machine coordinates, which is why the band +// is not worked out from the moves here.) // -// This implementation is intentionally minimal: it overrides only the MAIN -// fan (the one set by GCodeWriter::set_fan); overhang/internal-bridge/etc -// special fans remain at their layer-level values from apply_layer_cooldown. -// That keeps the per-line logic small while still giving the user precise -// fan control near the belt surface, which is the main quality concern. -std::string CoolingBuffer::apply_first_layer_plane_fan_eval( - std::string &&gcode_in, size_t /*layer_id*/, float layer_time) +// Only the main part-cooling fan is touched; overhang and bridge fans keep the values +// apply_layer_cooldown() gave them. +std::string CoolingBuffer::apply_belt_band_fan(std::string &&gcode_in, float layer_time, unsigned int extruder_at_start) { - if (!m_first_layer_plane || !m_first_layer_plane->is_active()) - return std::move(gcode_in); - + static constexpr std::string_view band_tag = ";_BELT_BAND:"; const std::string &gcode = gcode_in; std::string out; - out.reserve(gcode.size() + 256); + out.reserve(gcode.size()); - // Match the PWM floor applied at every other set_fan call in this file so - // band-crossing M106 emissions start the fan reliably at low speeds. + // Match the PWM floor applied at every other set_fan call in this file. const unsigned int part_cooling_fan_min_pwm = static_cast(std::max(0, m_config.part_cooling_fan_min_pwm.value)); + unsigned int active_extruder = extruder_at_start; - // Track position in slicing-frame mm. Seed from m_current_pos which the - // CoolingBuffer keeps up-to-date across layers. - Vec3d cur_pos_mm(m_current_pos[0], m_current_pos[1], m_current_pos[2]); - - // Track current main fan speed by parsing M106 commands as we walk so - // we can restore it after a band exit. - int current_main_fan = m_fan_speed; - int pre_band_main_fan = current_main_fan; - // Implicit initial state: assume the layer started "out of the band" - // (i.e., the layer-level fan setting from apply_layer_cooldown is in - // effect). The first movement we encounter will reconcile this. - bool in_first_layer_band = false; - unsigned int active_extruder = m_current_extruder; - - auto parse_xyz_into = [](const std::string_view &line_sv, Vec3d &p) { - if (line_sv.size() < 3) return false; - if (line_sv[0] != 'G') return false; - if (line_sv[1] != '0' && line_sv[1] != '1') return false; - if (line_sv[2] != ' ' && line_sv[2] != '\t') return false; - const char *c = line_sv.data() + 3; - const char *end = line_sv.data() + line_sv.size(); - bool any = false; - while (c < end && *c != ';') { - while (c < end && (*c == ' ' || *c == '\t')) ++c; - if (c >= end || *c == ';' || *c == '\n' || *c == '\r') break; - char axis = *c; - ++c; - if (axis == 'X' || axis == 'Y' || axis == 'Z') { - char *next; - double v = std::strtod(c, &next); - if (next != c) { - if (axis == 'X') p.x() = v; - else if (axis == 'Y') p.y() = v; - else p.z() = v; - c = next; - any = true; - continue; - } - } - // Skip unrecognized word. - while (c < end && *c != ' ' && *c != '\t' && *c != ';' && *c != '\n') - ++c; - } - return any; - }; - - auto parse_m106 = [](const std::string_view &line_sv) -> int { - // Returns -1 if not an M106, otherwise the S value (0..255). - if (line_sv.size() < 4 || line_sv[0] != 'M') return -1; - if (!(line_sv[1] == '1' && line_sv[2] == '0' && line_sv[3] == '6')) - return -1; - // Find S - size_t s_pos = line_sv.find('S'); - if (s_pos == std::string_view::npos) return -1; - const char *c = line_sv.data() + s_pos + 1; - char *next; - long v = std::strtol(c, &next, 10); - if (next == c) return -1; - return int(std::clamp(v, 0, 255)); - }; - - auto parse_m107 = [](const std::string_view &line_sv) -> bool { - return line_sv.size() >= 4 && line_sv[0] == 'M' && - line_sv[1] == '1' && line_sv[2] == '0' && line_sv[3] == '7'; - }; - - auto parse_tool_change = [this](const std::string_view &line_sv) -> int { - // Returns the new extruder id, or -1 if not a toolchange. - if (line_sv.size() < m_toolchange_prefix.size() + 1) return -1; - if (line_sv.compare(0, m_toolchange_prefix.size(), m_toolchange_prefix) != 0) - return -1; - const char *c = line_sv.data() + m_toolchange_prefix.size(); - char *next; - long v = std::strtol(c, &next, 10); - if (next == c) return -1; - return int(v); - }; - - const char *p = gcode.c_str(); - const char *end = gcode.c_str() + gcode.size(); + const char *p = gcode.data(); + const char *end = p + gcode.size(); while (p < end) { - const char *line_end = p; - while (line_end < end && *line_end != '\n') ++line_end; - const char *next_line = line_end; - if (next_line < end) ++next_line; // include the '\n' + const char *line_end = static_cast(std::memchr(p, '\n', end - p)); + if (line_end == nullptr) + line_end = end; + const char *next_line = line_end < end ? line_end + 1 : end; + const std::string_view line(p, line_end - p); - std::string_view line_sv(p, line_end - p); - - // Track tool changes so the per-line fan eval uses the right extruder. - int new_tool = parse_tool_change(line_sv); - if (new_tool >= 0) - active_extruder = unsigned(new_tool); - - // Track existing fan commands so we can restore the right value when - // exiting a band. - int m106_speed = parse_m106(line_sv); - if (m106_speed >= 0) { - current_main_fan = m106_speed; - if (!in_first_layer_band) - pre_band_main_fan = m106_speed; - } else if (parse_m107(line_sv)) { - current_main_fan = 0; - if (!in_first_layer_band) - pre_band_main_fan = 0; + if (line.size() > band_tag.size() && line.compare(0, band_tag.size(), band_tag) == 0) { + const int eff_idx = std::atoi(std::string(line.substr(band_tag.size())).c_str()); + const bool in_band = eff_idx < std::max(m_config.close_fan_the_first_x_layers.get_at(active_extruder), 1); + int target = this->compute_main_fan_speed(eff_idx, layer_time, active_extruder); + // Clear of the belt and nothing to add: fall back to what the layer asked for. + if (! in_band && target == 0 && m_belt_band_layer_fan >= 0) + target = m_belt_band_layer_fan; + if (target != m_belt_band_fan) { + out += GCodeWriter::set_fan(m_config.gcode_flavor, target, part_cooling_fan_min_pwm); + m_belt_band_fan = target; + } + // The tag itself is dropped. + p = next_line; + continue; } - // Movement line: parse XYZ, evaluate plane, possibly emit a fan - // change BEFORE this line. - bool moved = parse_xyz_into(line_sv, cur_pos_mm); - if (moved) { - const int eff_idx = m_first_layer_plane->effective_layer_index(cur_pos_mm); - const int close_n = m_config.close_fan_the_first_x_layers.get_at(active_extruder); - const bool now_in_band = eff_idx < std::max(close_n, 1); - if (now_in_band != in_first_layer_band) { - // Band crossing: emit a M106 with the appropriate speed. - int target_fan; - if (now_in_band) { - // Entering the first-layer band: fan off. - pre_band_main_fan = current_main_fan; - target_fan = compute_main_fan_speed(eff_idx, layer_time, active_extruder); - } else { - // Exiting the band: restore the layer's normal fan speed. - // Use compute_main_fan_speed with the effective index so - // the linear ramp factor (close_fan→full_fan_speed_layer) - // also follows distance from the plane. - target_fan = compute_main_fan_speed(eff_idx, layer_time, active_extruder); - if (target_fan == 0) - target_fan = pre_band_main_fan; - } - if (target_fan != current_main_fan) { - out += GCodeWriter::set_fan(m_config.gcode_flavor, target_fan, part_cooling_fan_min_pwm); - current_main_fan = target_fan; - m_fan_speed = target_fan; - m_current_fan_speed = target_fan; - } - in_first_layer_band = now_in_band; + if (! line.empty() && line.front() == 'M') { + // Follow the fan commands of the layer-level cooling. FanMover's parser ignores + // auxiliary and chamber fans (M106 P2, P3...), which must not be taken for the part fan. + const int16_t raw = get_fan_speed(std::string(line), m_config.gcode_flavor); + if (raw >= 0) { + m_belt_band_fan = int(std::lround(double(std::min(raw, 255)) * 100. / 255.)); + m_belt_band_layer_fan = m_belt_band_fan; } + } else if (line.size() > m_toolchange_prefix.size() && line.compare(0, m_toolchange_prefix.size(), m_toolchange_prefix) == 0) { + char *num_end = nullptr; + const std::string num(line.substr(m_toolchange_prefix.size())); + const long tool = std::strtol(num.c_str(), &num_end, 10); + if (num_end != num.c_str() && tool >= 0) + active_extruder = unsigned(tool); } out.append(p, next_line - p); p = next_line; } + // The fan's real state, so the next layer's cooling re-issues its own speed if it differs. + if (m_belt_band_fan >= 0) + m_current_fan_speed = m_belt_band_fan; return out; } diff --git a/src/libslic3r/GCode/CoolingBuffer.hpp b/src/libslic3r/GCode/CoolingBuffer.hpp index 1adb6f5005..0d758b5a5b 100644 --- a/src/libslic3r/GCode/CoolingBuffer.hpp +++ b/src/libslic3r/GCode/CoolingBuffer.hpp @@ -10,7 +10,6 @@ namespace Slic3r { class GCode; class Layer; -class FirstLayerPlane; struct PerExtruderAdjustments; // A standalone G-code filter, to control cooling of the print. @@ -37,13 +36,10 @@ private: // Returns the adjusted G-code. std::string apply_layer_cooldown(const std::string &gcode, size_t layer_id, float layer_time, std::vector &per_extruder_adjustments); - // First-layer plane: per-line fan re-evaluation post-pass. Walks the - // post-cooldown gcode, tracks XYZ position, and inserts M106 commands at - // band-crossing transitions in slicing-frame coordinates. Only runs - // when m_first_layer_plane is active. - std::string apply_first_layer_plane_fan_eval(std::string &&gcode_in, - size_t layer_id, - float layer_time); + // Belt printers: turn the ";_BELT_BAND:" tags GCode::_extrude() leaves in the + // layer's G-code into part-fan changes, so the fan follows a path's height above the + // belt rather than the slicing layer index, and strip the tags. + std::string apply_belt_band_fan(std::string &&gcode_in, float layer_time, unsigned int extruder_at_start); // Pure helper: compute the main fan speed for a given effective layer // index (layer-id units, mapped through the plane evaluator) and the @@ -74,9 +70,10 @@ private: unsigned int m_current_nozzle; //BBS: current fan speed int m_current_fan_speed; - // First-layer plane evaluator, borrowed from GCode. Null = inactive - // (legacy per-layer fan control). - const FirstLayerPlane *m_first_layer_plane = nullptr; + // Belt band pass state, kept across layers. The part fan as this pass last saw or set + // it (percent, -1 unknown), and the last value the layer-level cooling asked for. + int m_belt_band_fan = -1; + int m_belt_band_layer_fan = -1; }; } diff --git a/src/libslic3r/GCode/FanMover.hpp b/src/libslic3r/GCode/FanMover.hpp index 17addd855c..9567e8e2d5 100644 --- a/src/libslic3r/GCode/FanMover.hpp +++ b/src/libslic3r/GCode/FanMover.hpp @@ -27,6 +27,11 @@ public: } }; +// The part-cooling fan value a G-code line sets: the raw S (0..255) of an M106 that +// addresses the part fan, 0 for a fan-off command, -1 for any other line (auxiliary +// and chamber fans included). +int16_t get_fan_speed(const std::string &line, GCodeFlavor flavor); + class FanMover { private: diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 6f0eb7b00e..6d8d3d2ec9 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -596,3 +596,58 @@ TEST_CASE("Belt printers refuse an object taller than the gantry clearance", "[P CHECK_FALSE(print.validate().string.empty()); } } + +// On a belt every tilted layer starts on the belt, so "first layer" cooling is a band along +// the belt, not the first slicing layers: the part fan goes off for the paths that start +// within a layer height of the belt and back on above it, on every layer. The G-code is in +// machine coordinates, so the generator tags the band changes and the cooling buffer +// applies them; before that the buffer compared machine-frame moves with a slicing-frame +// plane and never switched the fan at all. +TEST_CASE("Belt printers switch the part fan by height above the belt", "[Print][belt][Cooling]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "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" }, + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "skirt_loops", 0 }, + { "z_hop", 0 }, + { "close_fan_the_first_x_layers", 1 }, + { "full_fan_speed_layer", 0 }, + { "fan_min_speed", 100 }, + { "fan_max_speed", 100 }, + { "slow_down_layer_time", 1000 }, + { "fan_cooling_layer_time", 1001 }, + { "reduce_fan_stop_start_freq", 0 }, + { "machine_start_gcode", "T[initial_tool]\n" }, + { "layer_change_gcode", "G92 E0\n" }, + }); + const std::string gcode = slice({ cube(20) }, config); + REQUIRE(! gcode.empty()); + + // The tags are consumed by the cooling buffer and never reach the file. + CHECK(gcode.find(";_BELT_BAND") == std::string::npos); + + size_t fan_off = 0, fan_on = 0; + GCodeReader parser; + parser.parse_buffer(gcode, [&](GCodeReader &, const GCodeReader::GCodeLine &line) { + if (line.cmd_is("M107")) + ++ fan_off; + else if (line.cmd_is("M106")) { + float s = 0.f; + if (line.has_value('S', s) && s <= 0.f) + ++ fan_off; + else + ++ fan_on; + } + }); + // A flat-bed print turns the fan on once. Here it cycles with the layers. + CHECK(fan_off > 10); + CHECK(fan_on > 10); +} From 360a68e07876b5bff9cd27d6df6c3cdcd93ce930 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 01:11:59 -0500 Subject: [PATCH 159/224] Belt: warn when the purge tower is enabled but the project has no tower object The purge tower is a model object the GUI creates and sizes, and libslic3r only purges into one that exists. A multi-filament belt project sliced from the CLI without it changed filament with nowhere to purge, silently. Raised in Hanif Koh's review of #14394. --- src/libslic3r/Print.cpp | 15 +++++++++++++++ tests/fff_print/test_print.cpp | 30 ++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 5bfdfe8eac..444bf7cde6 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1553,6 +1553,21 @@ StringObjectException Print::validate(std::vector *warnin add_warning(warningtemp); } + // The purge tower is a model object the GUI creates and sizes; libslic3r only purges + // into one that exists. A project sliced without it (the CLI on a project saved before + // the tower was generated) changes filament with nowhere to purge. + if (m_config.belt_printer.value && m_config.enable_belt_purge_tower.value + && m_config.print_sequence != PrintSequence::ByObject + && ! m_config.spiral_mode.value && extruders.size() > 1 && ! this->has_belt_purge_tower()) { + StringObjectException warningtemp; + warningtemp.string = L("The belt purge tower is enabled but the project has no purge tower object; " + "filament changes will not be purged. Open the project in the application " + "to generate the tower."); + warningtemp.opt_key = "enable_belt_purge_tower"; + warningtemp.is_warning = true; + add_warning(warningtemp); + } + if (m_config.belt_printer.value && m_config.enable_belt_purge_tower.value) { const size_t prism_count = std::count_if(m_objects.begin(), m_objects.end(), [](const PrintObject *object) { return object->config().belt_purge_tower_object.value; diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 6d8d3d2ec9..f9c9528461 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -336,6 +336,36 @@ TEST_CASE("Belt purge planning requires its managed purge object", "[Print][Purg CHECK_FALSE(print.has_wipe_tower()); } +// The GUI creates the purge tower object; a project sliced without one (the CLI) must say +// that its filament changes go unpurged. +TEST_CASE("Belt purge tower enabled without a tower object warns", "[Print][PurgeTower][belt]") +{ + DynamicPrintConfig config = multifilament_config(2, { + { "belt_printer", 1 }, + { "enable_belt_purge_tower", 1 }, + { "layer_change_gcode", "G92 E0\n" } + }); + auto purge_warnings = [](Print &print) { + std::vector warnings; + print.validate(&warnings); + return std::count_if(warnings.begin(), warnings.end(), [](const StringObjectException &w) { + return w.opt_key == "enable_belt_purge_tower"; + }); + }; + + Model model; + Print print; + build_cubes(model, print, config, /*n=*/2, /*overlap=*/false); + model.objects[1]->config.set_key_value("extruder", new ConfigOptionInt(2)); + print.apply(model, config); + REQUIRE(print.extruders().size() > 1); + CHECK(purge_warnings(print) == 1); + + model.objects.front()->config.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true)); + print.apply(model, config); + CHECK(purge_warnings(print) == 0); +} + TEST_CASE("Belt purge rejects multiple managed purge objects", "[Print][PurgeTower][Regression]") { DynamicPrintConfig config = multifilament_config(2, { From 412564cae3e72caae3d626962e9065df99dd9621 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 01:11:59 -0500 Subject: [PATCH 160/224] Belt: document the frame of the exclude-object outlines EXCLUDE_OBJECT_DEFINE keeps plate coordinates on a belt printer: the frame after the slicing rotation is undone and before the G-code axis remap and machine-frame shear, which is where the object stands on the belt. Raised in Hanif Koh's review of #14394. --- src/libslic3r/GCode.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 151efbf6b2..360e35cae2 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -10289,6 +10289,10 @@ std::string GCode::set_object_info(Print *print) { for (PrintInstance& inst : object->instances()) { inst.unique_id = unique_id++; inst.id = inst_id++; + // Outlines are in plate coordinates. On a belt printer that is the frame after + // the slicing rotation has been undone and before the G-code axis remap and + // machine-frame shear: where the object stands on the belt, which is what an + // object picker shows. Klipper cancels by name, so nothing depends on more. auto bbox = inst.get_bounding_box(); auto center = print->translate_to_print_space(Vec2d(bbox.center().x(), bbox.center().y())); auto inst_name = get_instance_name(object, inst); From 3beb448ae6ffb082dfd685996784ac40d4672a19 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 01:13:29 -0500 Subject: [PATCH 161/224] Belt brim: lattice lines closer to the belt than the band fraction move uphill With a first layer of about 0.28 mm or more at 45 degrees (or a shallower belt) the brim band is wider than one bead and its lines go on the nominal lattice. A lattice line could land where the belt is almost at the band's print_z; its flow was clamped to half a layer while the nozzle sat nearly on the belt. Such a line now moves uphill to the 0.75 fraction the single-line case uses, and a line that lands on the previous one is skipped. Ported from the Unlayered fork (patch 0007 of its belt port series, found there by fuzzing first layer heights). The fork's companion fix, restricting the brim filament to those the writer was handed (0008), is not needed here: ToolOrdering registers the brim filament on every band's layer, so the writer always has it. A test pins that with every object a flush target. --- src/libslic3r/BeltBrim.cpp | 40 ++++++++++++++------ tests/fff_print/test_skirt_brim.cpp | 58 +++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 12 deletions(-) diff --git a/src/libslic3r/BeltBrim.cpp b/src/libslic3r/BeltBrim.cpp index fa119c6609..92877ef0aa 100644 --- a/src/libslic3r/BeltBrim.cpp +++ b/src/libslic3r/BeltBrim.cpp @@ -1,3 +1,4 @@ +#include #include "BeltBrim.hpp" #include "ClipperUtils.hpp" @@ -284,7 +285,33 @@ static void belt_brim_band_paths(const BeltBrimContext &bc, // must not be pooled before the flow is resolved. // Overshoot the region so the clip, not the line's ends, decides the extent. const coord_t margin = coord_t(SCALED_EPSILON) + 1; - for (const coord_t u : us) { + coord_t u_prev = std::numeric_limits::min(); + for (coord_t u : us) { + // Nozzle-to-belt clearance for this line. Constant along the line, because the + // belt height depends only on the shear-axis coordinate. Band-anchored lines + // share one clearance by construction; lattice lines (shallow belts, or a first + // layer thick enough that the band is wider than a bead) each get their own. + // + // A lattice line can fall where the belt is only a hair below the band's print_z. + // The bead there would be laid scraping the belt while its flow is sized for a + // taller cell, so it is moved uphill to the same fraction of the band the + // single-line case uses. (The clearance is along slice Z; the real gap under the + // nozzle is clearance x cos(tilt), 0.53 h at 45 degrees for the 0.75 fraction.) + double clearance = uniform_clearance; + if (clearance <= 0.) { + const Point probe = bc.frame.from_axis == 0 ? Point(u, 0) : Point(0, u); + clearance = print_z - bc.ctx.floor_print_z(probe); + if (clearance < BAND_CLEARANCE_FRACTION * height) { + clearance = BAND_CLEARANCE_FRACTION * height; + u = scale_(bc.ctx.cutoff_u(print_z - clearance)); + } + clearance = std::min(clearance, height); + } + // Two lattice lines moved to the same place are one line. + if (u == u_prev) + continue; + u_prev = u; + Polyline line; if (bc.frame.from_axis == 0) line.points = { Point(u, coord_t(bc.region_bbox.min.y() - margin)), @@ -299,17 +326,6 @@ static void belt_brim_band_paths(const BeltBrimContext &bc, if (pieces.empty()) continue; - // Nozzle-to-belt clearance for this line. Constant along the line, because the - // belt height depends only on the shear-axis coordinate. Band-anchored lines - // share one clearance by construction; lattice lines (shallow belts) each get - // their own, clamped so neither end of a band yields an unprintable bead. - double clearance = uniform_clearance; - if (clearance <= 0.) { - const Point probe = bc.frame.from_axis == 0 ? Point(u, 0) : Point(0, u); - clearance = print_z - bc.ctx.floor_print_z(probe); - clearance = std::min(std::max(clearance, 0.5 * height), height); - } - // with_cross_section, not with_height: it reaches the prescribed volume while // KEEPING the extrusion spacing, so the bead is sized to fill exactly one // pitch x clearance cell of the sheet. diff --git a/tests/fff_print/test_skirt_brim.cpp b/tests/fff_print/test_skirt_brim.cpp index 5fa810106f..edd2f0a56a 100644 --- a/tests/fff_print/test_skirt_brim.cpp +++ b/tests/fff_print/test_skirt_brim.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include "test_helpers.hpp" // get access to init_print, etc @@ -1195,3 +1196,60 @@ TEST_CASE("Belt brim coexists with support material", "[SkirtBrim][belt]") REQUIRE(! gc.empty()); CHECK(role_passes(gc, "brim") > 0); } + +// With a 0.3 mm first layer at 45 degrees the brim band on the belt is wider than one bead, +// so its lines go on the nominal lattice instead of at a fixed fraction of the band. A +// lattice line can then land where the belt is almost at the band's print_z; it must be +// moved uphill to the same 0.75 fraction the single-line case uses, not laid scraping the +// belt with its flow clamped to half a layer. +TEST_CASE("Belt brim lattice lines keep their clearance above the belt", "[SkirtBrim][belt]") +{ + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "layer_height", 0.3 }, + { "initial_layer_print_height", 0.3 }, + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "brim_object_gap", 0 }, + }); + const std::string gcode = slice({ cube(20) }, config); + + // Heights of the brim extrusions, from the ;HEIGHT: tags inside ;TYPE:Brim sections. + std::vector brim_heights; + bool in_brim = false; + std::istringstream lines(gcode); + for (std::string line; std::getline(lines, line); ) { + if (boost::starts_with(line, ";TYPE:")) + in_brim = boost::starts_with(line, ";TYPE:Brim"); + else if (in_brim && boost::starts_with(line, ";HEIGHT:")) + brim_heights.push_back(std::stod(line.substr(8))); + } + REQUIRE(! brim_heights.empty()); + for (const double h : brim_heights) { + CHECK(h >= 0.75 * 0.3 - 1e-3); + CHECK(h <= 0.3 + 1e-3); + } +} + +// The brim prints in the object's outer wall filament even when every extrusion of the object +// is offered to purging (flush_into_objects): the tool ordering registers the brim filament +// itself, so the writer always knows it. +TEST_CASE("Belt brim slices when every object is a flush target", "[SkirtBrim][belt]") +{ + DynamicPrintConfig config = belt_brim_multifilament_config(2, { + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "brim_object_gap", 0 }, + { "flush_into_objects", 1 }, + { "flush_into_infill", 1 }, + }); + const std::vector> overrides { + { { "extruder", 1 } }, { { "extruder", 2 } }, + }; + Print print; + Model model; + init_print({ cube(20), cube(20) }, print, model, config, &overrides); + REQUIRE(print.validate().string.empty()); + const std::string out = gcode(print); + CHECK(out.find(";TYPE:Brim") != std::string::npos); +} From 1b392955de708bef61851650a347aa465fcb9fb9 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 01:13:29 -0500 Subject: [PATCH 162/224] tests: organic tree supports reaching the belt slice without a negative flow Covers the case from Hanif Koh's review of #14394 (belt raft layers below the object with no lower bound), which the negative-Z bottom layer fix in layer_initialize() addresses. --- tests/fff_print/test_print.cpp | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index f9c9528461..5b3baf7dc5 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -681,3 +681,32 @@ TEST_CASE("Belt printers switch the part fan by height above the belt", "[Print] CHECK(fan_off > 10); CHECK(fan_on > 10); } + +// Organic supports under an overhang on a belt printer reach below the object's first layer, +// where the virtual belt raft layers sit at negative Z. The lowest of them used to get a +// negative height and abort slicing with a negative flow error. +TEST_CASE("Belt printers slice organic tree supports that reach the belt", "[Print][belt][Support]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "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" }, + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "skirt_loops", 0 }, + { "z_hop", 0 }, + { "enable_support", 1 }, + { "support_type", "tree(auto)" }, + { "support_style", "organic" }, + { "machine_start_gcode", "T[initial_tool]\n" }, + { "layer_change_gcode", "G92 E0\n" }, + }); + std::string gcode; + REQUIRE_NOTHROW(gcode = slice({ TestMesh::overhang }, config)); + CHECK(! gcode.empty()); +} From 461063856d5184fc0e927c405bb61aff55e7a519 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 02:33:14 -0500 Subject: [PATCH 163/224] tests: fix two belt test expectations The clearance test needs the relative-E reset in its layer change G-code to get past validate()'s other checks, and now asserts the height message. The fan band test counts cycles rather than commands: the band is decided per path start, so a cube cycles the fan far less often than a benchy. --- tests/fff_print/test_print.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 5b3baf7dc5..b07c8c986a 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -609,6 +609,7 @@ TEST_CASE("Belt printers refuse an object taller than the gantry clearance", "[P { "gcode_remap_z", "pos_y" }, { "printable_height", printable_height }, { "skirt_loops", 0 }, + { "layer_change_gcode", "G92 E0\n" }, }); return config; }; @@ -623,7 +624,7 @@ TEST_CASE("Belt printers refuse an object taller than the gantry clearance", "[P Print print; Model model; init_print({ cube(60) }, print, model, belt_config(50)); - CHECK_FALSE(print.validate().string.empty()); + CHECK(print.validate().string.find("height") != std::string::npos); } } @@ -664,22 +665,21 @@ TEST_CASE("Belt printers switch the part fan by height above the belt", "[Print] // The tags are consumed by the cooling buffer and never reach the file. CHECK(gcode.find(";_BELT_BAND") == std::string::npos); - size_t fan_off = 0, fan_on = 0; + // The fan commands in order: '0' off, '1' on. + std::string fan_sequence; GCodeReader parser; parser.parse_buffer(gcode, [&](GCodeReader &, const GCodeReader::GCodeLine &line) { if (line.cmd_is("M107")) - ++ fan_off; + fan_sequence += '0'; else if (line.cmd_is("M106")) { float s = 0.f; - if (line.has_value('S', s) && s <= 0.f) - ++ fan_off; - else - ++ fan_on; + fan_sequence += (line.has_value('S', s) && s <= 0.f) ? '0' : '1'; } }); - // A flat-bed print turns the fan on once. Here it cycles with the layers. - CHECK(fan_off > 10); - CHECK(fan_on > 10); + // A flat-bed print turns the fan on once and leaves it on. Here it goes off again for + // paths that start back down at the belt, and on again above it. + INFO("fan sequence: " << fan_sequence); + CHECK(fan_sequence.find("101") != std::string::npos); } // Organic supports under an overhang on a belt printer reach below the object's first layer, From 441ae8e877378105b907824abe321d1256ae6119 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 02:36:13 -0500 Subject: [PATCH 164/224] Belt: decide the fan band per extrusion segment A tilted layer runs from the belt to the top of the part, so a wall loop that starts above the belt still passes along it. Tagging only the path's first point left such loops out of the band entirely; the band is now evaluated at each segment, with the tag capped where the fan stops depending on it. --- src/libslic3r/GCode.cpp | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 360e35cae2..45910cae00 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -8507,14 +8507,25 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // 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); - // Belt printers: tell the cooling buffer which band above the belt this path starts in, - // once per layer and at every change (CoolingBuffer::apply_belt_band_fan consumes it). - if (m_enable_cooling_markers && m_config.belt_printer.value && - (m_belt_band_tag_layer != m_layer_index || m_belt_band_tag != _layer)) { - gcode += ";_BELT_BAND:" + std::to_string(_layer) + "\n"; - m_belt_band_tag = _layer; - m_belt_band_tag_layer = m_layer_index; - } + // Belt printers: tell the cooling buffer which band above the belt the next extrusion is + // in (CoolingBuffer::apply_belt_band_fan consumes the tag). A tilted layer runs from the + // belt to the top of the part, so this is decided per segment, not per path; a tag is + // written once per layer and at every change, capped where the fan stops depending on it. + const bool belt_band_tags = m_enable_cooling_markers && m_config.belt_printer.value; + const int belt_band_cap = belt_band_tags + ? std::max(m_config.close_fan_the_first_x_layers.get_at(m_writer->filament()->id()), + m_config.full_fan_speed_layer.get_at(m_writer->filament()->id())) + 1 : 0; + auto tag_belt_band = [this, &gcode, belt_band_tags, belt_band_cap, z = path_point_mm.z()](coord_t x, coord_t y) { + if (! belt_band_tags) + return; + const int band = std::min(this->effective_layer_index_for_point(Vec3d(unscale(x), unscale(y), z)), belt_band_cap); + if (m_belt_band_tag_layer != m_layer_index || m_belt_band_tag != band) { + gcode += ";_BELT_BAND:" + std::to_string(band) + "\n"; + m_belt_band_tag = band; + m_belt_band_tag_layer = m_layer_index; + } + }; + tag_belt_band(path.first_point().x(), path.first_point().y()); if (path_on_first_layer || object_layer_over_raft()) { //BBS: for solid infill of first layer, speed can be higher as long as //wall lines have be attached @@ -8999,6 +9010,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, tempDescription += Slic3r::format(" | Old Flow Value: %0.5f Length: %0.5f",oldE, line_length); } } + tag_belt_band((line.a.x() + line.b.x()) / 2, (line.a.y() + line.b.y()) / 2); if (path.z_contoured) { // ZAA: Z anti-aliased extrusion with variable Z per point Vec2d dest2d = this->point_to_gcode(line.b.to_point()); @@ -9129,6 +9141,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, const ProcessedPoint &processed_point = new_points[i]; const ProcessedPoint &pre_processed_point = new_points[i-1]; Vec3d p = this->point_to_gcode_quantized(processed_point.p); + tag_belt_band((pre_processed_point.p.x() + processed_point.p.x()) / 2, (pre_processed_point.p.y() + processed_point.p.y()) / 2); if (m_enable_cooling_markers) { if (enable_overhang_bridge_fan) { cur_fan_enabled = check_overhang_fan(processed_point.overlap, path.role()); From 31494a9836aa417892c1096ffa3a292325a1f6fc Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 02:42:14 -0500 Subject: [PATCH 165/224] tests: give the belt fan band test a band the walls reach --- tests/fff_print/test_print.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index b07c8c986a..8cdd9e3c33 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -649,7 +649,9 @@ TEST_CASE("Belt printers switch the part fan by height above the belt", "[Print] { "initial_layer_print_height", 0.2 }, { "skirt_loops", 0 }, { "z_hop", 0 }, - { "close_fan_the_first_x_layers", 1 }, + // Three layers: the lowest wall of each tilted layer is centred about 0.3 mm above + // the belt (half a line width in from the contact edge), outside a one-layer band. + { "close_fan_the_first_x_layers", 3 }, { "full_fan_speed_layer", 0 }, { "fan_min_speed", 100 }, { "fan_max_speed", 100 }, From bfbf5ad1c28b4f2a3cadfac802dd814f907263d3 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 12:09:59 -0500 Subject: [PATCH 166/224] Arrange: keep a pile aligned to an off-centre point on the bed With best_object_pos away from the bed centre the placer packs the pile inside the bin and then translates it so its centre lands on that point, without checking that it still fits there. A belt printer aims at the leading end of the belt (BabyBelt Pro: 0.5, 0.05), so any pile longer than the 50 mm around that point was pushed past the edge: four 90 mm parts on the 95 x 500 mm belt ended with one across the edge and one outside while 290 mm of belt stayed free. The final alignment now stops the pile at the edge of the bin; the items' inflated boxes leave the object spacing as the margin. A pile that does not fit along an axis is centred on it, as before. --- .../include/libnest2d/placers/nfpplacer.hpp | 22 +++++++++++++++- tests/libslic3r/test_arrange.cpp | 26 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/deps_src/libnest2d/include/libnest2d/placers/nfpplacer.hpp b/deps_src/libnest2d/include/libnest2d/placers/nfpplacer.hpp index dc7733ebd6..cf66c4c97e 100644 --- a/deps_src/libnest2d/include/libnest2d/placers/nfpplacer.hpp +++ b/deps_src/libnest2d/include/libnest2d/placers/nfpplacer.hpp @@ -1111,7 +1111,27 @@ private: default: ; // DONT_ALIGN } - auto d = cb - ci; + auto d = cb - ci; + + // Keep the pile on the bin. A target near an edge (a belt printer starts its parts + // at the leading end of the belt) would otherwise centre a pile that is larger than + // the room around that point on it and push part of the pile off the bed. The pile + // stops at the edge instead; the items' boxes carry their inflation, which is the + // margin left there. A pile that does not fit along an axis is centred on it. + { + auto on_bin = [](Coord lo, Coord hi, Coord bin_lo, Coord bin_hi, Coord shift) { + if (hi - lo >= bin_hi - bin_lo) + return (bin_lo + bin_hi) / 2 - (lo + hi) / 2; + if (lo + shift < bin_lo) + shift = bin_lo - lo; + if (hi + shift > bin_hi) + shift = bin_hi - hi; + return shift; + }; + setX(d, on_bin(getX(bb.minCorner()), getX(bb.maxCorner()), getX(bbin.minCorner()), getX(bbin.maxCorner()), getX(d))); + setY(d, on_bin(getY(bb.minCorner()), getY(bb.maxCorner()), getY(bbin.minCorner()), getY(bbin.maxCorner()), getY(d))); + cb = ci + d; + } // BBS make sure the item won't clash with excluded regions // do we have wipe tower after arranging? diff --git a/tests/libslic3r/test_arrange.cpp b/tests/libslic3r/test_arrange.cpp index 3906cba8ba..a8bc56d2d5 100644 --- a/tests/libslic3r/test_arrange.cpp +++ b/tests/libslic3r/test_arrange.cpp @@ -1,3 +1,4 @@ +#include #include #include "libslic3r/Arrange.hpp" @@ -259,6 +260,31 @@ TEST_CASE("Arrange aligns the pile to a custom center", "[Arrange]") require_no_overlap(items); } +// A belt printer starts its parts at the leading end of the belt (best_object_pos 0.5, 0.05). +// Centring a pile on a point that close to the edge pushed everything longer than the room +// around it off the bed: four 90 mm parts on a 95 x 500 mm belt ended with one across the +// edge and one outside, with 290 mm of belt free behind them. The pile stops at the edge. +TEST_CASE("Arrange keeps a pile aligned near an edge on the bed", "[Arrange]") +{ + const BoundingBox belt = bed(95, 500); + ArrangePolygons items = squares(4, 90.); + ArrangeParams params = quiet_params(scaled(2.)); + params.align_center = Vec2d(0.5, 0.05); + + arrange(items, belt, params); + + coord_t lowest = std::numeric_limits::max(); + for (const ArrangePolygon &ap : items) { + REQUIRE(ap.bed_idx == 0); + const BoundingBox bb = ap.transformed_poly().contour.bounding_box(); + CHECK(belt.contains(bb)); + lowest = std::min(lowest, bb.min.y()); + } + // Snapped to the edge it was aimed at, less the spacing margin, not re-centred. + CHECK(lowest < scaled(10.)); + require_no_overlap(items); +} + TEST_CASE("Sequential print floors the object distance by object height", "[Arrange]") { // The only place sequential-print clearance is enforced. The arrange menu offers From 7a23d9ea2caaef06afdee45514921cf214a8c058 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 12:46:43 -0500 Subject: [PATCH 167/224] Belt slicing: only model parts set the layer range again The filter was lost with the debug logging it shared an #if with, so modifiers and support blockers stretched the sliced range of a belt object. --- src/libslic3r/BeltSliceStrategy.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libslic3r/BeltSliceStrategy.cpp b/src/libslic3r/BeltSliceStrategy.cpp index d16819e7db..74d8566000 100644 --- a/src/libslic3r/BeltSliceStrategy.cpp +++ b/src/libslic3r/BeltSliceStrategy.cpp @@ -41,6 +41,7 @@ void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo, double min_z = std::numeric_limits::max(); for (const ModelVolume *mv : model_volumes) { + if (!mv->is_model_part()) continue; Transform3d vol_trafo = trafo * mv->get_matrix(); const auto &its = mv->mesh().its; for (const stl_vertex &v : its.vertices) { From 007071bd56988198dacc3e5cf4441b3cd32590a0 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 12:46:44 -0500 Subject: [PATCH 168/224] GCode: skip the second crossing-avoidance pass on a brim apron layer too travel_to runs avoid-crossing a second time after a wipe; that call needs the current Layer as much as the first one does. --- src/libslic3r/GCode.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 45910cae00..4227bf7817 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -9461,7 +9461,8 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string // When "Wipe while retracting" is enabled, then extruder moves to another position, and travel from this position can cross perimeters. // Because of it, it is necessary to call avoid crossing perimeters again with new starting point after calling retraction() // FIXME Lukas H.: Try to predict if this second calling of avoid crossing perimeters will be needed or not. It could save computations. - if (last_post_before_retract != this->last_pos() && m_config.reduce_crossing_wall) { + if (last_post_before_retract != this->last_pos() && m_config.reduce_crossing_wall + && m_layer != nullptr) { // A brim apron layer has no Layer to avoid crossing // If in the previous call of m_avoid_crossing_perimeters.travel_to was use_external_mp_once set to true restore this value for next call. if (used_external_mp_once) m_avoid_crossing_perimeters.use_external_mp_once(); From 997fac87526e0ae2e12e9cebed41fa1e164f26be Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 12:46:44 -0500 Subject: [PATCH 169/224] GCodeProcessor: key the belt height-check skip on belt_printer The check was skipped for any machine-frame transform; what makes the printable height meaningless is the belt axis, so ask for that directly. --- src/libslic3r/GCode/GCodeProcessor.cpp | 20 +++++++++++--------- src/libslic3r/GCode/GCodeProcessor.hpp | 1 + 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index c2aedd8262..bf5d5ea980 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -2777,9 +2777,9 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int // Belt-printer post-gcode shear/scale/post_remap is applied as the final // step of BeltKinematics::to_machine, so MoveVertex.position is - // in the printer's machine frame. Undo it here so XY area and Z height - // checks operate in the build-volume frame that printable_area / - // printable_height are defined in. For non-belt printers + // in the printer's machine frame. Undo it here so the XY area check + // operates in the build-volume frame that printable_area is defined in + // (the height checks below are skipped on belt printers). For non-belt printers // (is_active() == false) apply_inverse is identity and behaviour is // unchanged from before. const bool machine_frame_active = m_machine_frame_transform.is_active(); @@ -2860,11 +2860,12 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int valid = false; } } - // Belt printers: machine Z is belt travel, which grows without bound over a - // print, while printable_height is the clearance above the belt; the two are - // not comparable, so the over-height check is skipped, as the preview's - // ToolHeightOutside warning already is. - if ( !machine_frame_active && iter->second.max_print_z > plate_printable_height ) { //over height + // Belt printers: the Z recorded here grows with belt travel (machine Z with the + // frame transform, the slicing-frame Z without it), while printable_height is the + // clearance above the belt; the two are not comparable, so the over-height check + // is skipped, as the preview's ToolHeightOutside warning already is. + // Print::validate() checks the object's height against the clearance. + if ( !m_belt_printer && iter->second.max_print_z > plate_printable_height ) { //over height m_result.gcode_check_result.error_code |= (1 << 3); std::pair filament_to_object_id; filament_to_object_id.first = iter->first; @@ -2905,7 +2906,7 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int } // check printable height - if (!machine_frame_active && (extruder_id < printable_heights.size()) && (iter->second.max_print_z > printable_heights[extruder_id])) { + if (!m_belt_printer && (extruder_id < printable_heights.size()) && (iter->second.max_print_z > printable_heights[extruder_id])) { m_result.gcode_check_result.error_code |= (1 << 1); std::pair filament_to_object_id; filament_to_object_id.first = iter->first; @@ -3076,6 +3077,7 @@ void GCodeProcessor::apply_config(const PrintConfig& config) // bounds rather than machine-frame positions. m_machine_frame_transform.init_from_config(config); m_result.machine_frame_transform_active = m_machine_frame_transform.is_active(); + m_belt_printer = config.belt_printer.value; auto filament_maps = config.option("filament_map"); if (filament_maps != nullptr) { diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index 85cc5a7544..6090a55eb6 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -1164,6 +1164,7 @@ class Print; // transform on move positions so bounds checks operate in the // pre-machine-frame (build-volume) frame. MachineFrameTransform m_machine_frame_transform; + bool m_belt_printer{ false }; unsigned int m_line_id; unsigned int m_last_line_id; From 550229b0faa79b93934679b70dff2c36a93e1f10 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 12:46:44 -0500 Subject: [PATCH 170/224] Belt validation: refuse a brim next to a purge tower object The purge plan moves objects onto a common layer grid after the brim bands are built, so the two cannot share a print. The prime tower setting alone still does not block a brim. The missing-prism warning now counts the filaments the objects use, as the GUI does. --- src/libslic3r/GCode/ToolOrdering.cpp | 4 ++-- src/libslic3r/Print.cpp | 12 ++++++++---- tests/fff_print/test_skirt_brim.cpp | 22 ++++++++++++++++++++++ 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index a59e5aa46b..a9f56a805f 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -1066,8 +1066,8 @@ void ToolOrdering::fill_wipe_tower_partitions(const PrintConfig &config, coordf_ // The `print_z < object_bottom_z` clause reads "below the object" as "raft // gap". On a belt printer that is wrong: the brim apron legitimately prints // below the object's first layer, and treating those layers as raft would put a - // wipe tower at negative Z. Belt brim and the prime tower are mutually - // exclusive (rejected in Print::validate()), so simply drop the clause there. + // wipe tower at negative Z. A belt printer never prints the classic + // prime tower (Print::has_wipe_tower()), so simply drop the clause there. // // Gate on config.belt_printer, NOT on has_belt_brim: every layer below the // object bottom on a belt printer is legitimately a sub-object stream - brim diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 444bf7cde6..66de6d88ee 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1420,10 +1420,14 @@ StringObjectException Print::validate(std::vector *warnin return { L("Draft shield is not compatible with belt printer mode.") }; // Belt brim spans many layers and owns the layers below the object, which - // spiral vase cannot share. The prime tower setting is no obstacle: belt - // printers never print the classic tower, and the belt purge prism is an - // ordinary object that never takes a brim. + // neither the belt purge tower nor spiral vase can share (the purge plan moves + // objects onto a common layer grid after the brim bands are built). The prime + // tower setting alone is no obstacle: it stays on for any multi-filament + // project, and belt printers never print the classic tower. if (this->has_belt_brim()) { + if (this->has_belt_purge_tower()) + return { L("Brim is not compatible with the belt purge tower. " + "Disable one of them.") }; if (m_config.spiral_mode.value) return { L("Brim is not compatible with spiral vase mode on a belt printer. " "Disable one of them.") }; @@ -1558,7 +1562,7 @@ StringObjectException Print::validate(std::vector *warnin // the tower was generated) changes filament with nowhere to purge. if (m_config.belt_printer.value && m_config.enable_belt_purge_tower.value && m_config.print_sequence != PrintSequence::ByObject - && ! m_config.spiral_mode.value && extruders.size() > 1 && ! this->has_belt_purge_tower()) { + && ! m_config.spiral_mode.value && this->object_extruders().size() > 1 && ! this->has_belt_purge_tower()) { StringObjectException warningtemp; warningtemp.string = L("The belt purge tower is enabled but the project has no purge tower object; " "filament changes will not be purged. Open the project in the application " diff --git a/tests/fff_print/test_skirt_brim.cpp b/tests/fff_print/test_skirt_brim.cpp index edd2f0a56a..d762c49421 100644 --- a/tests/fff_print/test_skirt_brim.cpp +++ b/tests/fff_print/test_skirt_brim.cpp @@ -930,6 +930,28 @@ TEST_CASE("Belt inner-only leading brim does not reject the prime tower or spira CHECK(print.validate().string.empty()); CHECK_FALSE(gcode(print).empty()); } + // An actual purge tower object is different: the purge plan moves objects onto a common + // layer grid after the brim bands are built, so the two are still refused together. + SECTION("a brim is refused next to a belt purge tower object") { + DynamicPrintConfig config = belt_brim_multifilament_config(2, { + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "brim_object_gap", 0 }, + { "enable_belt_purge_tower", 1 }, + }); + const std::vector> overrides { + { { "extruder", 1 } }, { { "extruder", 2 } }, + }; + Print print; + Model model; + init_print({ cube(20), cube(20) }, print, model, config, &overrides); + CHECK(print.validate().string.empty()); + + model.objects.back()->config.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true)); + print.apply(model, config); + REQUIRE(print.has_belt_purge_tower()); + CHECK_FALSE(print.validate().string.empty()); + } } TEST_CASE("Belt brim spans many layers instead of one", "[SkirtBrim][belt]") From 5ac0dd19d04d90fc25e3d50d51f15febe04f0ca5 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 12:46:44 -0500 Subject: [PATCH 171/224] Belt brim: no near-duplicate lines in a narrow lattice band A band barely wider than one line got a second line almost on top of the first, and lattice rows could repeat within half a pitch. --- src/libslic3r/BeltBrim.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/libslic3r/BeltBrim.cpp b/src/libslic3r/BeltBrim.cpp index 92877ef0aa..45ef5d14c9 100644 --- a/src/libslic3r/BeltBrim.cpp +++ b/src/libslic3r/BeltBrim.cpp @@ -253,7 +253,10 @@ static void belt_brim_band_paths(const BeltBrimContext &bc, std::vector us; double uniform_clearance = 0.; // 0 => derive per line from its own position double line_pitch = bc.in_plane_pitch; - if (band_in_plane <= bc.in_plane_pitch + EPSILON) { + // One line also serves a band up to half a bead wider than the nominal pitch (a 0.3 mm + // first layer at 45 degrees): its flow is matched to the band, so the bead is that much + // wider. Two lattice lines in such a band would land almost on top of each other. + if (band_in_plane <= 1.5 * bc.in_plane_pitch + EPSILON) { // Steep belt, which is the normal case: the band is narrower than one bead, so // exactly one line fits. Place it at a FIXED fraction of the band rather than // on a nominal-spacing lattice. On a lattice each line lands at an arbitrary @@ -307,8 +310,9 @@ static void belt_brim_band_paths(const BeltBrimContext &bc, } clearance = std::min(clearance, height); } - // Two lattice lines moved to the same place are one line. - if (u == u_prev) + // A line moved uphill can land on, or almost on, its neighbour; two beads closer + // than half a pitch would be laid into the same cell. + if (u_prev != std::numeric_limits::min() && std::abs(u - u_prev) < bc.pitch_u / 2) continue; u_prev = u; From d35af1806a8d6d918c2b24059504f2580502ab3d Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 12:46:44 -0500 Subject: [PATCH 172/224] Belt cooling: keep the fan off in the band inside the layer cooling pass The band was a second pass over the finished layer that fought the fan commands the layer pass had already written (overhang, bridge and resume requests). The generator now marks where each segment enters and leaves the band and the layer pass treats the band as the strongest fan request, so there is one place that decides the fan. --- src/libslic3r/GCode.cpp | 25 ++--- src/libslic3r/GCode.hpp | 5 +- src/libslic3r/GCode/CoolingBuffer.cpp | 151 +++++--------------------- src/libslic3r/GCode/CoolingBuffer.hpp | 19 +--- src/libslic3r/GCode/FanMover.hpp | 5 - tests/fff_print/test_print.cpp | 75 +++++++++---- 6 files changed, 95 insertions(+), 185 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 4227bf7817..f98cf62fc4 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -8507,25 +8507,20 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // 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); - // Belt printers: tell the cooling buffer which band above the belt the next extrusion is - // in (CoolingBuffer::apply_belt_band_fan consumes the tag). A tilted layer runs from the - // belt to the top of the part, so this is decided per segment, not per path; a tag is - // written once per layer and at every change, capped where the fan stops depending on it. - const bool belt_band_tags = m_enable_cooling_markers && m_config.belt_printer.value; - const int belt_band_cap = belt_band_tags - ? std::max(m_config.close_fan_the_first_x_layers.get_at(m_writer->filament()->id()), - m_config.full_fan_speed_layer.get_at(m_writer->filament()->id())) + 1 : 0; - auto tag_belt_band = [this, &gcode, belt_band_tags, belt_band_cap, z = path_point_mm.z()](coord_t x, coord_t y) { + // Belt printers: a tilted layer runs from the belt to the top of the part, so the + // "first layers" the fan stays off for are a band along the belt. Mark where the + // extrusion enters and leaves it, per segment, for the cooling buffer. + const bool belt_band_tags = m_enable_cooling_markers && m_config.belt_printer.value; + const int belt_band_layers = belt_band_tags ? m_config.close_fan_the_first_x_layers.get_at(m_writer->filament()->id()) : 0; + auto tag_belt_band = [this, &gcode, belt_band_tags, belt_band_layers, z = path_point_mm.z()](coord_t x, coord_t y) { if (! belt_band_tags) return; - const int band = std::min(this->effective_layer_index_for_point(Vec3d(unscale(x), unscale(y), z)), belt_band_cap); - if (m_belt_band_tag_layer != m_layer_index || m_belt_band_tag != band) { - gcode += ";_BELT_BAND:" + std::to_string(band) + "\n"; - m_belt_band_tag = band; - m_belt_band_tag_layer = m_layer_index; + const bool in_band = this->effective_layer_index_for_point(Vec3d(unscale(x), unscale(y), z)) < belt_band_layers; + if (in_band != m_belt_in_band) { + gcode += in_band ? ";_BELT_BAND_START\n" : ";_BELT_BAND_END\n"; + m_belt_in_band = in_band; } }; - tag_belt_band(path.first_point().x(), path.first_point().y()); if (path_on_first_layer || object_layer_over_raft()) { //BBS: for solid infill of first layer, speed can be higher as long as //wall lines have be attached diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index e390bc972f..17221101fd 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -856,9 +856,8 @@ protected: ~BeltFloorObjectGuard() { slot = nullptr; } }; - // Last ";_BELT_BAND" tag written and the layer it was written on (see _extrude()). - int m_belt_band_tag{std::numeric_limits::min()}; - int m_belt_band_tag_layer{std::numeric_limits::min()}; + // The last extrusion segment was inside the belt's first-layer fan band (see _extrude()). + bool m_belt_in_band{false}; std::set m_initial_layer_extruders; std::vector> m_sorted_layer_filaments; diff --git a/src/libslic3r/GCode/CoolingBuffer.cpp b/src/libslic3r/GCode/CoolingBuffer.cpp index 29ab528de5..a47dd2c148 100644 --- a/src/libslic3r/GCode/CoolingBuffer.cpp +++ b/src/libslic3r/GCode/CoolingBuffer.cpp @@ -1,13 +1,10 @@ #include "../GCode.hpp" #include "CoolingBuffer.hpp" -#include "FanMover.hpp" #include #include #include #include #include -#include -#include #include #include #include @@ -39,8 +36,7 @@ CoolingBuffer::CoolingBuffer(GCode &gcodegen) : m_config(gcodegen.config()), m_t void CoolingBuffer::reset(const Vec3d &position) { - m_belt_band_fan = -1; - m_belt_band_layer_fan = -1; + m_belt_band_active = false; // BBS: add I and J axis to store center of arc m_current_pos.assign(7, 0.f); m_current_pos[0] = float(position.x()); @@ -80,6 +76,9 @@ struct CoolingLine // ORCA: Add support for ironing fan speed control TYPE_IRONING_FAN_START = 1 << 19, TYPE_IRONING_FAN_END = 1 << 20, + // Belt printers: extrusions within the first-layer band above the belt. + TYPE_BELT_BAND_START = 1 << 21, + TYPE_BELT_BAND_END = 1 << 22, }; CoolingLine(unsigned int type, size_t line_start, size_t line_end) : @@ -334,13 +333,9 @@ std::string CoolingBuffer::process_layer(std::string &&gcode, size_t layer_id, b if (flush) { // This is either an object layer or the very last print layer. Calculate cool down over the collected support layers // and one object layer. - const unsigned int extruder_at_start = m_current_extruder; std::vector per_extruder_adjustments = this->parse_layer_gcode(m_gcode, m_current_pos); float layer_time_stretched = this->calculate_layer_slowdown(per_extruder_adjustments); out = this->apply_layer_cooldown(m_gcode, layer_id, layer_time_stretched, per_extruder_adjustments); - // Belt printers: the fan follows each path's height above the belt (see apply_belt_band_fan). - if (out.find(";_BELT_BAND:") != std::string::npos) - out = this->apply_belt_band_fan(std::move(out), layer_time_stretched, extruder_at_start); m_gcode.clear(); } return out; @@ -544,6 +539,10 @@ std::vector CoolingBuffer::parse_layer_gcode(const std:: line.type = CoolingLine::TYPE_IRONING_FAN_START; } else if (boost::starts_with(sline, ";_IRONING_FAN_END")) { // ORCA: Add support for ironing fan speed control line.type = CoolingLine::TYPE_IRONING_FAN_END; + } else if (boost::starts_with(sline, ";_BELT_BAND_START")) { + line.type = CoolingLine::TYPE_BELT_BAND_START; + } else if (boost::starts_with(sline, ";_BELT_BAND_END")) { + line.type = CoolingLine::TYPE_BELT_BAND_END; } else if (boost::starts_with(sline, "G4 ")) { // Parse the wait time. line.type = CoolingLine::TYPE_G4; @@ -885,7 +884,9 @@ std::string CoolingBuffer::apply_layer_cooldown( {CoolingLine::TYPE_SUPPORT_INTERFACE_FAN_START, false}, {CoolingLine::TYPE_IRONING_FAN_START, false}, // ORCA: Add support for ironing fan speed control {CoolingLine::TYPE_FORCE_RESUME_FAN, false}}; - bool need_set_fan = false; + // Belt printers: a band still open from the previous layer has to take the fan back from + // the layer-level speed issued just above. + bool need_set_fan = m_belt_band_active; for (const CoolingLine *line : lines) { const char *line_start = gcode.c_str() + line->line_start; @@ -899,6 +900,8 @@ std::string CoolingBuffer::apply_layer_cooldown( if (new_extruder != m_current_extruder) { m_current_extruder = new_extruder; change_extruder_set_fan(true); + if (m_belt_band_active) + need_set_fan = true; } } new_gcode.append(line_start, line_end - line_start); @@ -951,6 +954,13 @@ std::string CoolingBuffer::apply_layer_cooldown( if (m_additional_fan_speed != -1 && m_config.auxiliary_fan.value) new_gcode += GCodeWriter::set_additional_fan(m_additional_fan_speed); } + else if (line->type & CoolingLine::TYPE_BELT_BAND_START) { + m_belt_band_active = true; + need_set_fan = true; + } else if (line->type & CoolingLine::TYPE_BELT_BAND_END) { + m_belt_band_active = false; + need_set_fan = true; + } else if (line->type & CoolingLine::TYPE_EXTRUDE_END) { // Just remove this comment. } else if (line->type & (CoolingLine::TYPE_ADJUSTABLE | CoolingLine::TYPE_EXTERNAL_PERIMETER | CoolingLine::TYPE_WIPE | CoolingLine::TYPE_HAS_F)) { @@ -1043,7 +1053,15 @@ std::string CoolingBuffer::apply_layer_cooldown( m_current_fan_speed = speed; } }; - if (fan_speed_change_requests[CoolingLine::TYPE_OVERHANG_FAN_START]){ + if (m_belt_band_active) { + // Belt printers: a tilted layer runs from the belt to the top of the part, so + // "the first layers" are a band along the belt rather than the first slicing + // layers. Extrusions GCode::_extrude() marks as inside that band print with the + // fan off, whatever overhang, bridge or resume request is pending, as the first + // layers of a flat bed do. Leaving the band falls through to the branches below. + set_fan(0); + fan_speed_change_requests[CoolingLine::TYPE_FORCE_RESUME_FAN] = false; + } else if (fan_speed_change_requests[CoolingLine::TYPE_OVERHANG_FAN_START]){ set_fan(overhang_fan_speed); } else if (fan_speed_change_requests[CoolingLine::TYPE_INTERNAL_BRIDGE_FAN_START]){ // ORCA: Add support for separate internal bridge fan speed control set_fan(internal_bridge_fan_speed); @@ -1072,115 +1090,4 @@ std::string CoolingBuffer::apply_layer_cooldown( return new_gcode; } -// Pure helper: compute the main fan speed for a given effective layer index. -// Mirrors the inline logic in change_extruder_set_fan but is callable from -// per-tag code in apply_belt_band_fan. -int CoolingBuffer::compute_main_fan_speed(int effective_layer_id, float layer_time, - unsigned int extruder_id) const -{ -#define EXTRUDER_CFG(opt) m_config.opt.get_at(extruder_id) - float fan_min_speed = EXTRUDER_CFG(fan_min_speed); - float fan_max_speed = EXTRUDER_CFG(fan_max_speed); - bool reduce_fan_stop_start_freq = EXTRUDER_CFG(reduce_fan_stop_start_freq); - int close_fan_the_first_x_layers = EXTRUDER_CFG(close_fan_the_first_x_layers); - int full_fan_speed_layer = EXTRUDER_CFG(full_fan_speed_layer); - float slow_down_layer_time = float(EXTRUDER_CFG(slow_down_layer_time)); - float fan_cooling_layer_time = float(EXTRUDER_CFG(fan_cooling_layer_time)); -#undef EXTRUDER_CFG - - if (close_fan_the_first_x_layers <= 0 && full_fan_speed_layer > 0) - close_fan_the_first_x_layers = 1; - - float fan_speed_new = reduce_fan_stop_start_freq ? fan_min_speed : 0.f; - if (effective_layer_id >= close_fan_the_first_x_layers) { - if (layer_time < slow_down_layer_time) { - fan_speed_new = fan_max_speed; - } else if (layer_time < fan_cooling_layer_time) { - double t = (layer_time - slow_down_layer_time) / - (fan_cooling_layer_time - slow_down_layer_time); - fan_speed_new = float(int(floor(t * fan_min_speed + - (1. - t) * fan_max_speed) + 0.5)); - } - if (effective_layer_id + 1 < full_fan_speed_layer) { - float factor = float(effective_layer_id + 1 - close_fan_the_first_x_layers) - / float(full_fan_speed_layer - close_fan_the_first_x_layers); - fan_speed_new = float(std::clamp(int(fan_speed_new * factor + 0.5f), 0, 255)); - } - } else { - fan_speed_new = 0.f; - } - return int(fan_speed_new); -} - -// Belt printers: a layer is a tilted slab, so "the first layer" is not a slicing layer but -// whatever lies within a layer height of the belt. GCode::_extrude() knows each path's -// height above the belt in the slicing frame and tags every change of it with -// ";_BELT_BAND:". This pass turns the tags into part-fan changes -// and strips them. (The G-code itself is in machine coordinates, which is why the band -// is not worked out from the moves here.) -// -// Only the main part-cooling fan is touched; overhang and bridge fans keep the values -// apply_layer_cooldown() gave them. -std::string CoolingBuffer::apply_belt_band_fan(std::string &&gcode_in, float layer_time, unsigned int extruder_at_start) -{ - static constexpr std::string_view band_tag = ";_BELT_BAND:"; - const std::string &gcode = gcode_in; - std::string out; - out.reserve(gcode.size()); - - // Match the PWM floor applied at every other set_fan call in this file. - const unsigned int part_cooling_fan_min_pwm = static_cast(std::max(0, m_config.part_cooling_fan_min_pwm.value)); - unsigned int active_extruder = extruder_at_start; - - const char *p = gcode.data(); - const char *end = p + gcode.size(); - while (p < end) { - const char *line_end = static_cast(std::memchr(p, '\n', end - p)); - if (line_end == nullptr) - line_end = end; - const char *next_line = line_end < end ? line_end + 1 : end; - const std::string_view line(p, line_end - p); - - if (line.size() > band_tag.size() && line.compare(0, band_tag.size(), band_tag) == 0) { - const int eff_idx = std::atoi(std::string(line.substr(band_tag.size())).c_str()); - const bool in_band = eff_idx < std::max(m_config.close_fan_the_first_x_layers.get_at(active_extruder), 1); - int target = this->compute_main_fan_speed(eff_idx, layer_time, active_extruder); - // Clear of the belt and nothing to add: fall back to what the layer asked for. - if (! in_band && target == 0 && m_belt_band_layer_fan >= 0) - target = m_belt_band_layer_fan; - if (target != m_belt_band_fan) { - out += GCodeWriter::set_fan(m_config.gcode_flavor, target, part_cooling_fan_min_pwm); - m_belt_band_fan = target; - } - // The tag itself is dropped. - p = next_line; - continue; - } - - if (! line.empty() && line.front() == 'M') { - // Follow the fan commands of the layer-level cooling. FanMover's parser ignores - // auxiliary and chamber fans (M106 P2, P3...), which must not be taken for the part fan. - const int16_t raw = get_fan_speed(std::string(line), m_config.gcode_flavor); - if (raw >= 0) { - m_belt_band_fan = int(std::lround(double(std::min(raw, 255)) * 100. / 255.)); - m_belt_band_layer_fan = m_belt_band_fan; - } - } else if (line.size() > m_toolchange_prefix.size() && line.compare(0, m_toolchange_prefix.size(), m_toolchange_prefix) == 0) { - char *num_end = nullptr; - const std::string num(line.substr(m_toolchange_prefix.size())); - const long tool = std::strtol(num.c_str(), &num_end, 10); - if (num_end != num.c_str() && tool >= 0) - active_extruder = unsigned(tool); - } - - out.append(p, next_line - p); - p = next_line; - } - - // The fan's real state, so the next layer's cooling re-issues its own speed if it differs. - if (m_belt_band_fan >= 0) - m_current_fan_speed = m_belt_band_fan; - return out; -} - } // namespace Slic3r diff --git a/src/libslic3r/GCode/CoolingBuffer.hpp b/src/libslic3r/GCode/CoolingBuffer.hpp index 0d758b5a5b..e0b5044921 100644 --- a/src/libslic3r/GCode/CoolingBuffer.hpp +++ b/src/libslic3r/GCode/CoolingBuffer.hpp @@ -36,18 +36,6 @@ private: // Returns the adjusted G-code. std::string apply_layer_cooldown(const std::string &gcode, size_t layer_id, float layer_time, std::vector &per_extruder_adjustments); - // Belt printers: turn the ";_BELT_BAND:" tags GCode::_extrude() leaves in the - // layer's G-code into part-fan changes, so the fan follows a path's height above the - // belt rather than the slicing layer index, and strip the tags. - std::string apply_belt_band_fan(std::string &&gcode_in, float layer_time, unsigned int extruder_at_start); - - // Pure helper: compute the main fan speed for a given effective layer - // index (layer-id units, mapped through the plane evaluator) and the - // current extruder. Mirrors the inline logic in the change_extruder_set_fan - // lambda but is callable from per-line code. - int compute_main_fan_speed(int effective_layer_id, float layer_time, - unsigned int extruder_id) const; - // G-code snippet cached for the support layers preceding an object layer. std::string m_gcode; // Internal data. @@ -70,10 +58,9 @@ private: unsigned int m_current_nozzle; //BBS: current fan speed int m_current_fan_speed; - // Belt band pass state, kept across layers. The part fan as this pass last saw or set - // it (percent, -1 unknown), and the last value the layer-level cooling asked for. - int m_belt_band_fan = -1; - int m_belt_band_layer_fan = -1; + // Belt printers: the extrusion being processed lies in the first-layer band above the + // belt (between a ";_BELT_BAND_START" and a ";_BELT_BAND_END"). Kept across layers. + bool m_belt_band_active = false; }; } diff --git a/src/libslic3r/GCode/FanMover.hpp b/src/libslic3r/GCode/FanMover.hpp index 9567e8e2d5..17addd855c 100644 --- a/src/libslic3r/GCode/FanMover.hpp +++ b/src/libslic3r/GCode/FanMover.hpp @@ -27,11 +27,6 @@ public: } }; -// The part-cooling fan value a G-code line sets: the raw S (0..255) of an M106 that -// addresses the part fan, 0 for a fan-off command, -1 for any other line (auxiliary -// and chamber fans included). -int16_t get_fan_speed(const std::string &line, GCodeFlavor flavor); - class FanMover { private: diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 8cdd9e3c33..e9a98324d9 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -20,6 +20,9 @@ #include "test_utils.hpp" #include +#include +#include +#include #include #include #include @@ -628,13 +631,11 @@ TEST_CASE("Belt printers refuse an object taller than the gantry clearance", "[P } } -// On a belt every tilted layer starts on the belt, so "first layer" cooling is a band along -// the belt, not the first slicing layers: the part fan goes off for the paths that start -// within a layer height of the belt and back on above it, on every layer. The G-code is in -// machine coordinates, so the generator tags the band changes and the cooling buffer -// applies them; before that the buffer compared machine-frame moves with a slicing-frame -// plane and never switched the fan at all. -TEST_CASE("Belt printers switch the part fan by height above the belt", "[Print][belt][Cooling]") +// On a belt every tilted layer starts on the belt, so "the first layers" the fan stays off +// for are a band along the belt, not the first slicing layers. The generator marks where +// each extrusion segment enters and leaves that band and the cooling buffer keeps the fan +// off inside it, on every layer. +TEST_CASE("Belt printers keep the part fan off within the band above the belt", "[Print][belt][Cooling]") { DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); config.set_deserialize_strict({ @@ -649,8 +650,8 @@ TEST_CASE("Belt printers switch the part fan by height above the belt", "[Print] { "initial_layer_print_height", 0.2 }, { "skirt_loops", 0 }, { "z_hop", 0 }, - // Three layers: the lowest wall of each tilted layer is centred about 0.3 mm above - // the belt (half a line width in from the contact edge), outside a one-layer band. + // Three layers, 0.6 mm: the lowest wall of each tilted layer is centred about 0.3 mm + // above the belt (half a line width in from the contact edge). { "close_fan_the_first_x_layers", 3 }, { "full_fan_speed_layer", 0 }, { "fan_min_speed", 100 }, @@ -664,24 +665,50 @@ TEST_CASE("Belt printers switch the part fan by height above the belt", "[Print] const std::string gcode = slice({ cube(20) }, config); REQUIRE(! gcode.empty()); - // The tags are consumed by the cooling buffer and never reach the file. + // The markers are consumed by the cooling buffer and never reach the file. CHECK(gcode.find(";_BELT_BAND") == std::string::npos); - // The fan commands in order: '0' off, '1' on. - std::string fan_sequence; - GCodeReader parser; - parser.parse_buffer(gcode, [&](GCodeReader &, const GCodeReader::GCodeLine &line) { - if (line.cmd_is("M107")) - fan_sequence += '0'; - else if (line.cmd_is("M106")) { - float s = 0.f; - fan_sequence += (line.has_value('S', s) && s <= 0.f) ? '0' : '1'; + // With this axis mapping machine Y is the height above the belt along the gantry. Walk + // the moves with the fan state: extrusions that stay within 0.45 mm of the belt are well + // inside the band and must print with the fan off; extrusions that stay 5 mm clear of it + // must print with it on. The first three slicing layers have the fan off altogether. + size_t in_band = 0, in_band_fan_on = 0, clear = 0, clear_fan_off = 0; + int layer = -1; + bool fan_on = false; + double y = 0.; + std::istringstream lines(gcode); + for (std::string line; std::getline(lines, line); ) { + if (boost::starts_with(line, ";LAYER_CHANGE")) { + ++ layer; + } else if (boost::starts_with(line, "M107")) { + fan_on = false; + } else if (boost::starts_with(line, "M106")) { + const size_t s = line.find('S'); + fan_on = s != std::string::npos && std::atof(line.c_str() + s + 1) > 0.; + } else if (boost::starts_with(line, "G1 ")) { + const size_t comment = line.find(';'); + const std::string cmd = line.substr(0, comment); + const size_t ypos = cmd.find(" Y"), epos = cmd.find(" E"); + if (ypos == std::string::npos) + continue; + const double y_new = std::atof(cmd.c_str() + ypos + 2); + const bool extruding = epos != std::string::npos && std::atof(cmd.c_str() + epos + 2) > 0.; + if (extruding && layer >= 3) { + if (std::max(y, y_new) < 0.45) { + ++ in_band; + in_band_fan_on += fan_on; + } else if (std::min(y, y_new) > 5.) { + ++ clear; + clear_fan_off += ! fan_on; + } + } + y = y_new; } - }); - // A flat-bed print turns the fan on once and leaves it on. Here it goes off again for - // paths that start back down at the belt, and on again above it. - INFO("fan sequence: " << fan_sequence); - CHECK(fan_sequence.find("101") != std::string::npos); + } + CHECK(in_band > 20); + CHECK(in_band_fan_on == 0); + CHECK(clear > 20); + CHECK(clear_fan_off == 0); } // Organic supports under an overhang on a belt printer reach below the object's first layer, From 488c6d8226a55699f488df4a4457aaf8c6a83d0d Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 3 Oct 2026 10:02:36 -0500 Subject: [PATCH 173/224] Belt profiles: bump the IdeaFormer and Printcepts bundle versions The IR3 V2 and BabyBelt Pro printer profiles changed since the last bump; the updater only installs a strictly newer bundle. --- resources/profiles/IdeaFormer.json | 2 +- resources/profiles/Printcepts.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/profiles/IdeaFormer.json b/resources/profiles/IdeaFormer.json index f9f32d2f3f..ee076dfffb 100644 --- a/resources/profiles/IdeaFormer.json +++ b/resources/profiles/IdeaFormer.json @@ -1,6 +1,6 @@ { "name": "IdeaFormer", - "version": "02.00.00.04", + "version": "02.00.00.05", "force_update": "0", "description": "IdeaFormer belt printer configurations", "machine_model_list": [ diff --git a/resources/profiles/Printcepts.json b/resources/profiles/Printcepts.json index 1abe84873b..cdeda8f04d 100644 --- a/resources/profiles/Printcepts.json +++ b/resources/profiles/Printcepts.json @@ -1,6 +1,6 @@ { "name": "Printcepts", - "version": "01.00.00.02", + "version": "01.00.00.03", "force_update": "0", "description": "Printcepts belt printer configurations", "machine_model_list": [ From 6b304884d24ded41e428517e995decf5915dca47 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 3 Oct 2026 12:03:18 -0500 Subject: [PATCH 174/224] Belt arrange: group colours along the belt, keep the purge tower's strip free, stop the tower at the plate end On a belt the parts print in belt order, so every colour change between parts is a filament change. Arrange packs items in extruder order already, but it grew the pile around its centre, so the colours ended up interleaved. A belt print now packs from the leading end of the bed, each row filling across the belt before the pile advances, and the objective charges an item for every packed part of another colour it does not fully follow along the belt, counting the tilted layers that reach cot(angle) * height past a part, so each colour prints as one run. The direction follows the slicing rotation: a rotation about X prints toward +Y, one about Y toward -X, and a negative angle flips it. Packing from the edge also means the brim has to be kept on the bed: a belt brim is printed brim_width wide for every brim type, so that much is reserved along every edge (between parts the brims may overlap, as on any printer). The purge prism is regenerated from the arranged parts, flush with the far edge of the bed, yet arrange moved it about like a part and packed parts into the strip it comes back to. Arrange now skips the prism and reserves its strip with a fixed virtual item, like a bed exclusion area, whenever the parts use more than one filament. The prism's length follows the parts plus a ramp per unit of height; with the height at its cap that ran 100 mm past the end of a 500 mm belt and the project could not print. The bar now stops at the plate end, and the purge planner's existing warning reports what the shortened bar cannot absorb. --- src/libslic3r/Arrange.cpp | 93 +++++++++++++++++++++++++--- src/libslic3r/Arrange.hpp | 7 +++ src/slic3r/GUI/BeltPurgeTower.cpp | 34 ++++++++--- src/slic3r/GUI/BeltPurgeTower.hpp | 4 +- src/slic3r/GUI/Jobs/ArrangeJob.cpp | 97 ++++++++++++++++++++++++++++-- src/slic3r/GUI/Jobs/ArrangeJob.hpp | 1 + tests/libslic3r/test_arrange.cpp | 44 ++++++++++++++ 7 files changed, 254 insertions(+), 26 deletions(-) diff --git a/src/libslic3r/Arrange.cpp b/src/libslic3r/Arrange.cpp index fd496537f4..d6e386b5d4 100644 --- a/src/libslic3r/Arrange.cpp +++ b/src/libslic3r/Arrange.cpp @@ -276,7 +276,13 @@ Points get_shrink_bedpts(const DynamicPrintConfig* print_cfg, const ArrangeParam template void fill_config(PConf& pcfg, const ArrangeParams ¶ms) { - if (params.is_seq_print) { + if (params.is_belt) { + // Pack from the end of the belt that prints first. + pcfg.starting_point = !params.belt_reversed ? PConf::Alignment::BOTTOM_LEFT : + params.belt_axis == 1 ? PConf::Alignment::TOP_LEFT : + PConf::Alignment::BOTTOM_RIGHT; + } + else if (params.is_seq_print) { // Start placing the items from the center of the print bed pcfg.starting_point = PConf::Alignment::BOTTOM_LEFT; } @@ -421,7 +427,51 @@ protected: return bindist; } - double dist_to_bin(const Box& ibb, const ClipperLib::IntPoint& origin_pack, typename Packer::PlacementConfig::Alignment starting_point_alignment) + // Belt printers pack from the end of the belt that prints first, and a corner + // packer's checks (pile inside the bin, pack origin) apply to them as well. + bool corner_packing() const { return params.is_belt || m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT; } + + static double at(const Box::PointType &pt, int i) { return double(i == 0 ? getX(pt) : getY(pt)); } + + // Position along the belt in print order: increasing from the end that prints first. + double belt_pos(const Box::PointType &pt) const { return params.belt_reversed ? -at(pt, params.belt_axis) : at(pt, params.belt_axis); } + double belt_start(const Box &bb) const { return belt_pos(params.belt_reversed ? bb.maxCorner() : bb.minCorner()); } + double belt_end(const Box &bb) const { return belt_pos(params.belt_reversed ? bb.minCorner() : bb.maxCorner()); } + + // The corner of the bin the belt pile grows from. + Box::PointType belt_origin() const + { + const Box bb = sl::boundingBox(m_bin); + auto o = bb.minCorner(); + if (params.belt_reversed) { + if (params.belt_axis == 0) setX(o, getX(bb.maxCorner())); + else setY(o, getY(bb.maxCorner())); + } + return o; + } + + // An item's far edge in print order is what it costs (so a row fills across the + // belt before the pile advances), with a slight pull toward the near lateral + // edge and the same penalty as the bottom-left heuristic for sitting outside + // the corner. + double dist_along_belt(const Box &ibb) + { + const Box bin = sl::boundingBox(m_bin); + const int l = 1 - params.belt_axis; + const double lat = at(ibb.minCorner(), l) - at(bin.minCorner(), l); + double d = belt_end(ibb) - belt_start(bin); + d += lat < 0 ? 10 * -lat : 0.1 * lat; + if (double behind = belt_start(ibb) - belt_start(bin); behind < 0) + d += 10 * -behind; + return norm(d); + } + + double corner_bindist(const Box &ibb, const ClipperLib::IntPoint &origin_pack) + { + return params.is_belt ? dist_along_belt(ibb) : dist_for_BOTTOM_LEFT(ibb, origin_pack); + } + + double dist_to_bin(const Box& ibb, const ClipperLib::IntPoint& origin_pack, typename Packer::PlacementConfig::Alignment starting_point_alignment) { double bindist = 0; if (starting_point_alignment == PConfig::Alignment::BOTTOM_LEFT) @@ -510,8 +560,8 @@ protected: // The smalles distance from the arranged pile center: double dist = norm(*(std::min_element(dists.begin(), dists.end()))); - if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT) { - double bindist = dist_for_BOTTOM_LEFT(ibb, origin_pack); + if (corner_packing()) { + double bindist = corner_bindist(ibb, origin_pack); score = 0.2 * dist + 0.8 * bindist; } else { @@ -568,8 +618,8 @@ protected: break; } case LAST_BIG_ITEM: { - if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT) { - score = dist_for_BOTTOM_LEFT(ibb, origin_pack); + if (corner_packing()) { + score = corner_bindist(ibb, origin_pack); } else { if (m_pilebb.defined) @@ -584,8 +634,8 @@ protected: // already processed bigger items. // No need to play around with the anchor points, the center will be // just fine for small items - if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT) - score = dist_for_BOTTOM_LEFT(ibb, origin_pack); + if (corner_packing()) + score = corner_bindist(ibb, origin_pack); else { // Align mainly around existing items score = 0.8 * norm(pl::distance(ibb.center(), bigbb.center()))+ 0.2*norm(pl::distance(ibb.center(), origin_pack)); @@ -686,6 +736,28 @@ protected: score += 1 * (new_extruder_cnt-last_extruder_cnt); } + // On a belt the parts print in belt order, so every colour change between + // parts is a filament change. Items arrive sorted by extruder and the pile + // grows from the leading end; keep each colour's run contiguous by charging + // an item for every packed item of another colour it does not fully follow, + // counting the tilted layers that reach belt_tilt_slope * height past that + // item's far edge. + if (params.is_belt && !params.is_seq_print) { + const std::set item_colours(item.extrude_ids.begin(), item.extrude_ids.end()); + const double item_start = belt_start(ibb); + for (Item &p : m_items) { + if (p.is_virt_object) + continue; + const std::set p_colours(p.extrude_ids.begin(), p.extrude_ids.end()); + const bool same_colour = std::includes(item_colours.begin(), item_colours.end(), p_colours.begin(), p_colours.end()) + || std::includes(p_colours.begin(), p_colours.end(), item_colours.begin(), item_colours.end()); + if (same_colour) + continue; + if (item_start < belt_end(p.boundingBox()) + scaled(p.height * params.belt_tilt_slope)) + score += 10.; + } + } + return std::make_tuple(score, fullbb); } @@ -762,7 +834,8 @@ public: auto binbb = sl::boundingBox(m_bin); - auto starting_point = cfg.starting_point == PConfig::Alignment::BOTTOM_LEFT ? binbb.minCorner() : binbb.center(); + auto starting_point = this->params.is_belt ? belt_origin() : + cfg.starting_point == PConfig::Alignment::BOTTOM_LEFT ? binbb.minCorner() : binbb.center(); // if we have wipe tower, items should be arranged around wipe tower for (Item itm : items) { if (itm.is_wipe_tower) { @@ -913,7 +986,7 @@ std::function AutoArranger::g auto mp = m_merged_pile; mp.emplace_back(itm.transformedShape()); auto chull = sl::convexHull(mp); - if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT) + if (corner_packing()) { if (!sl::isInside(chull, m_bin)) score += LARGE_COST_TO_REJECT; diff --git a/src/libslic3r/Arrange.hpp b/src/libslic3r/Arrange.hpp index ad125aee7d..b8fc0f7268 100644 --- a/src/libslic3r/Arrange.hpp +++ b/src/libslic3r/Arrange.hpp @@ -137,6 +137,13 @@ struct ArrangeParams { float nozzle_height = 0; float printable_height = 256.0; Vec2d align_center{ 0.5,0.5 }; + // Belt printer: items print in the order they lie along the belt axis, from + // its low end unless belt_reversed, and a part's top prints + // belt_tilt_slope * height further along it than its base. + bool is_belt = false; + int belt_axis = 1; // 0 = X, 1 = Y + bool belt_reversed = false; + float belt_tilt_slope = 1.f; // cot(belt tilt angle), 0 when the belt is not tilted ArrangePolygons excluded_regions; // regions cant't be used ArrangePolygons nonprefered_regions; // regions can be used but not prefered diff --git a/src/slic3r/GUI/BeltPurgeTower.cpp b/src/slic3r/GUI/BeltPurgeTower.cpp index a8b0b0c0b8..3dc50d4cb2 100644 --- a/src/slic3r/GUI/BeltPurgeTower.cpp +++ b/src/slic3r/GUI/BeltPurgeTower.cpp @@ -266,6 +266,11 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object const Vec3d plate_origin = plate->get_origin(); new_sig.key[10] = q(plate_origin.x()); new_sig.key[11] = q(plate_origin.y()); + if (const auto *bed_opt = printer_config.option("printable_area"); bed_opt != nullptr && !bed_opt->values.empty()) { + const BoundingBoxf bed = get_extents(bed_opt->values); + new_sig.key[12] = q(bed.max.x()); + new_sig.key[13] = q(bed.max.y()); + } if (prism_idxs.size() == 1 && model.objects[size_t(prism_idxs.front())]->instances.size() == 1 && new_sig == sig) return false; // already up to date — do not touch the model @@ -278,26 +283,35 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object // whole band needs belt_start <= y_min - H*cot (bar's own leading ramp) and // belt_end >= y_max + z_max*cot (parts' top features print further up the // belt). The trailing z_max*cot term dominates the bar's own ramp. + // The bed (printable_area) is plate-local but model instances live in the + // plate's world frame, so the plate origin is added to every bed coordinate. + const double inset = 1.; + BoundingBoxf bed_ext; + if (const auto *bed_opt = printer_config.option("printable_area"); + bed_opt != nullptr && !bed_opt->values.empty()) + bed_ext = get_extents(bed_opt->values); + + // Along the belt the bar stops at the end of the plate: a longer bar cannot be + // printed, and the cross-sections it loses there are reported by the purge + // planner when the parts' last layers then purge more than the bar holds. const double margin = 5.; const double ramp_compensation = height / sin_t; const double belt_origin = plate_origin[belt_is_y ? 1 : 0]; - const double belt_start = std::max(belt_origin, belt_min - ramp_compensation); // leading ramp, toward belt origin - const double belt_end = belt_max + margin + ramp_compensation + z_max * cot_t; // + parts' top-feature belt reach + double belt_end = belt_max + margin + ramp_compensation + z_max * cot_t; // + parts' top-feature belt reach + if (bed_ext.defined) + belt_end = std::min(belt_end, belt_origin + (belt_is_y ? bed_ext.max.y() : bed_ext.max.x()) - inset); + const double belt_start = std::max(belt_origin, std::min(belt_min - ramp_compensation, belt_end - 10.)); // leading ramp, toward belt origin const double length = std::max(belt_end - belt_start, 10.); + belt_end = belt_start + length; const double belt_center = 0.5 * (belt_start + belt_end); // Across-belt: flush against the bed's maximum edge, inset by half the bar // width so the bar's far edge sits on the boundary and the whole bar stays - // on the bed. The bed (printable_area) is plate-local but model instances - // live in the plate's world frame, so add the plate origin's lateral - // component. lat_min/lat_max come from instance_bounding_box (world frame). + // on the bed. lat_min/lat_max come from instance_bounding_box (world frame). const double lat_origin = plate_origin[belt_is_y ? 0 : 1]; - const double inset = 1.; double lat_center = lat_max + 5. + 0.5 * width; // fallback: just past the parts - if (const auto *bed_opt = printer_config.option("printable_area"); - bed_opt != nullptr && !bed_opt->values.empty()) { - const BoundingBoxf bed_ext = get_extents(bed_opt->values); - const double bed_lat_max = belt_is_y ? bed_ext.max.x() : bed_ext.max.y(); + if (bed_ext.defined) { + const double bed_lat_max = belt_is_y ? bed_ext.max.x() : bed_ext.max.y(); lat_center = lat_origin + bed_lat_max - inset - 0.5 * width; } diff --git a/src/slic3r/GUI/BeltPurgeTower.hpp b/src/slic3r/GUI/BeltPurgeTower.hpp index c5a6d301a0..27d5b47c94 100644 --- a/src/slic3r/GUI/BeltPurgeTower.hpp +++ b/src/slic3r/GUI/BeltPurgeTower.hpp @@ -22,12 +22,12 @@ struct BeltPurgeSignature { bool valid = false; int filament_count = 0; - long key[12] = {0}; // rounded geometry and plate inputs (0.1 mm units) + long key[14] = {0}; // rounded geometry, plate and bed inputs (0.1 mm units) bool operator==(const BeltPurgeSignature &o) const { if (valid != o.valid || filament_count != o.filament_count) return false; - for (int i = 0; i < 12; ++i) + for (int i = 0; i < 14; ++i) if (key[i] != o.key[i]) return false; return true; diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.cpp b/src/slic3r/GUI/Jobs/ArrangeJob.cpp index cd4cc83dab..a21bf52c19 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.cpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.cpp @@ -15,6 +15,9 @@ #include "slic3r/GUI/GUI_ObjectList.hpp" #include "libnest2d/common.hpp" +#include "libslic3r/Geometry.hpp" +#include +#include #define SAVE_ARRANGE_POLY 0 @@ -63,6 +66,14 @@ public: } }; +// The belt purge prism is generated from the arranged parts (ensure_belt_purge_tower), +// so arrange neither moves it nor packs around it; it reserves the prism's strip instead. +static bool is_belt_purge_prism(const ModelObject *mo) +{ + const ConfigOption *opt = mo->config.option("belt_purge_tower_object"); + return opt != nullptr && opt->getBool(); +} + // BBS: add partplate logic static WipeTower get_wipe_tower(const Plater &plater, int plate_idx) { @@ -77,6 +88,63 @@ arrangement::ArrangePolygon get_wipetower_arrange_poly(WipeTower* tower) return ap; } +// Belt printers pack their parts against the edges of the bed, so what has to stay +// free there is reserved with fixed virtual items on every plate, the way the bed's +// own exclusion areas are: +// - the strip the purge prism comes back to, flush with the far lateral edge (see +// ensure_belt_purge_tower), when the parts use more than one filament; +// - the brim along every edge: a belt brim is printed brim_width wide for every brim +// type but none. Between parts the brims may overlap, as on any printer. +void ArrangeJob::prepare_belt_regions(int num_plates) +{ + if (!params.is_belt || params.is_seq_print) + return; + const DynamicPrintConfig &config = wxGetApp().preset_bundle->full_config(); + const BoundingBoxf bed = get_extents(config.opt("printable_area")->values); + const bool belt_is_y = params.belt_axis == 1; + std::vector regions; + + std::set filaments; + for (const ArrangePolygons *items : { &m_selected, &m_unselected }) + for (const ArrangePolygon &ap : *items) + if (!ap.is_virt_object) + filaments.insert(ap.extrude_ids.begin(), ap.extrude_ids.end()); + if (config.opt_bool("enable_belt_purge_tower") && filaments.size() > 1) { + // The prism is at least one millimetre per island plus the gaps between them. + const int islands = int(filaments.size()) - 1; + const double width = std::max(config.opt_float("belt_purge_tower_width"), 2. * islands - 1.) + 1.; // + the prism's edge inset + BoundingBoxf strip = bed; + if (belt_is_y) + strip.min.x() = std::max(bed.min.x(), bed.max.x() - width); + else + strip.min.y() = std::max(bed.min.y(), bed.max.y() - width); + regions.push_back(strip); + } + + // Virtual items are inflated by the one millimetre exclusion gap already. + const double brim = config.opt_enum("brim_type") == btNoBrim ? 0. : + config.opt_float("brim_width") + config.opt_float("brim_object_gap") + config.opt_float("extra_brim_width") - 1.; + if (brim > 0.) { + regions.emplace_back(bed.min, Vec2d(bed.min.x() + brim, bed.max.y())); + regions.emplace_back(Vec2d(bed.max.x() - brim, bed.min.y()), bed.max); + regions.emplace_back(bed.min, Vec2d(bed.max.x(), bed.min.y() + brim)); + regions.emplace_back(Vec2d(bed.min.x(), bed.max.y() - brim), bed.max); + } + + for (int j = 0; j < num_plates; ++j) + for (size_t i = 0; i < regions.size(); ++i) { + ArrangePolygon ap; + ap.poly.contour = scaled(regions[i]).polygon(); + ap.translation = Vec2crd(0, 0); + ap.rotation = 0.f; + ap.is_virt_object = true; + ap.bed_idx = j; + ap.height = 1; + ap.name = "BeltRegion" + std::to_string(i); + m_unselected.emplace_back(std::move(ap)); + } +} + void ArrangeJob::clear_input() { const Model &model = m_plater->model(); @@ -128,6 +196,8 @@ void ArrangeJob::prepare_selected() { for (size_t oidx = 0; oidx < model.objects.size(); ++oidx) { const Selection::InstanceIdxsList* instlist = obj_sel[oidx]; ModelObject* mo = model.objects[oidx]; + if (is_belt_purge_prism(mo)) + continue; std::vector inst_sel(mo->instances.size(), false); @@ -176,6 +246,7 @@ void ArrangeJob::prepare_selected() { } prepare_wipe_tower(); + prepare_belt_regions(MAX_NUM_PLATES); // The strides have to be removed from the fixed items. For the @@ -206,6 +277,8 @@ void ArrangeJob::prepare_all() { // Go through the objects and check if inside the selection for (size_t oidx = 0; oidx < model.objects.size(); ++oidx) { ModelObject *mo = model.objects[oidx]; + if (is_belt_purge_prism(mo)) + continue; for (size_t i = 0; i < mo->instances.size(); ++i) { ModelInstance * mi = mo->instances[i]; @@ -252,6 +325,7 @@ void ArrangeJob::prepare_all() { // add the virtual object into unselect list if has plate_list.preprocess_exclude_areas(m_unselected, enable_wrapping, MAX_NUM_PLATES); + prepare_belt_regions(MAX_NUM_PLATES); } arrangement::ArrangePolygon estimate_wipe_tower_info(int plate_index, std::set& extruder_ids) @@ -290,10 +364,9 @@ void ArrangeJob::prepare_wipe_tower() bool enable_prime_tower = op && op->getBool(); if (!enable_prime_tower || params.is_seq_print) return; - // Belt printers have no classic wipe tower; purging goes into the belt - // purge prism, which is a real model object and arranges like any other. - if (const auto *belt_opt = wxGetApp().preset_bundle->printers.get_edited_preset().config.option("belt_printer"); - belt_opt && belt_opt->value) + // Belt printers have no classic wipe tower; purging goes into the belt purge + // prism, whose strip prepare_belt_regions() reserves. + if (params.is_belt) return; bool smooth_timelapse = false; @@ -399,6 +472,8 @@ void ArrangeJob::prepare_partplate() { for (size_t oidx = 0; oidx < model.objects.size(); ++oidx) { ModelObject* mo = model.objects[oidx]; + if (is_belt_purge_prism(mo)) + continue; for (size_t inst_idx = 0; inst_idx < mo->instances.size(); ++inst_idx) { bool in_plate = plate->contain_instance(oidx, inst_idx) || plate->intersect_instance(oidx, inst_idx); @@ -434,6 +509,7 @@ void ArrangeJob::prepare_partplate() { // add the virtual object into unselect list if has plate_list.preprocess_exclude_areas(m_unselected, enable_wrapping, current_plate_index + 1); + prepare_belt_regions(current_plate_index + 1); } //BBS: add partplate logic @@ -785,6 +861,19 @@ arrangement::ArrangeParams init_arrange_params(Plater *p) params.is_seq_print = settings.is_seq_print; params.min_obj_distance = scaled(settings.distance); params.align_to_y_axis = settings.align_to_y_axis; + if (print_config.belt_printer.value) { + // Parts print in belt order: the belt runs across the gantry's tilt axis, a + // rotation about X prints toward +Y and one about Y toward -X (see + // BeltTransform), a negative angle flips that, and a tilted layer reaches + // cot(angle) * height past a part's far edge. + const BeltRotationAxis axis = print_config.belt_slice_rotation.value; + const double angle = print_config.belt_slice_rotation_angle.value; + const bool tilted = (axis == BeltRotationAxis::X || axis == BeltRotationAxis::Y) && std::abs(angle) > EPSILON; + params.is_belt = true; + params.belt_axis = axis == BeltRotationAxis::Y ? 0 : 1; + params.belt_reversed = tilted && ((axis == BeltRotationAxis::Y) != (angle < 0.)); + params.belt_tilt_slope = tilted ? float(1. / std::tan(Geometry::deg2rad(std::clamp(std::abs(angle), 5., 90.)))) : 0.f; + } int state = p->get_prepare_state(); if (state == Job::JobPrepareState::PREPARE_STATE_MENU) { diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.hpp b/src/slic3r/GUI/Jobs/ArrangeJob.hpp index 0c9f03de01..19143c3254 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.hpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.hpp @@ -46,6 +46,7 @@ class ArrangeJob : public Job //BBS:prepare the items from current selected partplate void prepare_partplate(); void prepare_wipe_tower(); + void prepare_belt_regions(int num_plates); ArrangePolygon prepare_arrange_polygon(void* instance); diff --git a/tests/libslic3r/test_arrange.cpp b/tests/libslic3r/test_arrange.cpp index a8bc56d2d5..1c13180f01 100644 --- a/tests/libslic3r/test_arrange.cpp +++ b/tests/libslic3r/test_arrange.cpp @@ -285,6 +285,50 @@ TEST_CASE("Arrange keeps a pile aligned near an edge on the bed", "[Arrange]") require_no_overlap(items); } +// On a belt the parts print in belt order, so two colours that alternate along the +// belt, or sit side by side, cost a filament change on every shared layer. Arrange +// keeps each colour together: no part shares belt length with a part of another +// colour, counting the tilted layers that run cot(angle) * height past its far edge, +// whichever end of the belt prints first. +TEST_CASE("Arrange groups the colours of a belt print along the belt", "[Arrange][belt]") +{ + const bool reversed = GENERATE(false, true); + CAPTURE(reversed); + const BoundingBox belt = bed(95, 500); + ArrangePolygons items = squares(6, 30., 20.); + for (size_t i = 0; i < items.size(); ++i) + items[i].extrude_ids = { int(i % 3) + 1 }; // three colours, two parts each + ArrangeParams params = quiet_params(scaled(2.)); + params.align_center = Vec2d(0.5, 0.05); + params.is_belt = true; + params.belt_axis = 1; + params.belt_reversed = reversed; + params.belt_tilt_slope = 1.f; // 45 degrees + + arrange(items, belt, params); + require_no_overlap(items); + + // Belt position in print order, so the same check serves both directions. + const coord_t dir = reversed ? -1 : 1; + auto start = [&](const ArrangePolygon &ap) { const BoundingBox bb = ap.transformed_poly().contour.bounding_box(); return dir * (reversed ? bb.max.y() : bb.min.y()); }; + auto end = [&](const ArrangePolygon &ap) { const BoundingBox bb = ap.transformed_poly().contour.bounding_box(); return dir * (reversed ? bb.min.y() : bb.max.y()) + scaled(ap.height * params.belt_tilt_slope); }; + + for (const ArrangePolygon &ap : items) { + REQUIRE(ap.bed_idx == 0); + CHECK(belt.contains(ap.transformed_poly().contour.bounding_box())); + } + for (const ArrangePolygon &a : items) + for (const ArrangePolygon &b : items) { + if (a.extrude_ids == b.extrude_ids) + continue; + // The part printed later starts after the earlier one has finished. + const coord_t earlier_end = start(a) <= start(b) ? end(a) : end(b); + const coord_t later_start = std::max(start(a), start(b)); + INFO("colour " << a.extrude_ids.front() << " vs " << b.extrude_ids.front()); + CHECK(earlier_end <= later_start); + } +} + TEST_CASE("Sequential print floors the object distance by object height", "[Arrange]") { // The only place sequential-print clearance is enforced. The arrange menu offers From 0c3b7bc72e50541ef99794aff654edb1e61d0703 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 3 Oct 2026 12:03:18 -0500 Subject: [PATCH 175/224] Belt brim: move the apron bands with the purge plan's layer grid The purge plan snaps every object onto one layer grid after the brim is built; the per-layer brim bands follow their layers but the apron bands below the first layer carry their own print_z and were left behind, which is why a brim was refused next to a purge tower object. The shift now moves them too and the combination is accepted again. --- src/libslic3r/BeltPurge.cpp | 3 +++ src/libslic3r/Print.cpp | 10 +++------- tests/fff_print/test_skirt_brim.cpp | 11 +++++------ 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/libslic3r/BeltPurge.cpp b/src/libslic3r/BeltPurge.cpp index cff3de8846..c4cbc83425 100644 --- a/src/libslic3r/BeltPurge.cpp +++ b/src/libslic3r/BeltPurge.cpp @@ -322,6 +322,9 @@ void PrintObject::belt_shift_layer_grid(double delta) layer->print_z += delta; for (SupportLayer *layer : m_support_layers) layer->print_z += delta; + // The brim's apron bands below the first layer carry their own print_z. + for (BeltBrimBand &band : m_belt_brim_prologue) + band.print_z += delta; m_slicing_params.belt_floor_z_shift += delta; } diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 66de6d88ee..550cfbbd64 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1420,14 +1420,10 @@ StringObjectException Print::validate(std::vector *warnin return { L("Draft shield is not compatible with belt printer mode.") }; // Belt brim spans many layers and owns the layers below the object, which - // neither the belt purge tower nor spiral vase can share (the purge plan moves - // objects onto a common layer grid after the brim bands are built). The prime - // tower setting alone is no obstacle: it stays on for any multi-filament - // project, and belt printers never print the classic tower. + // spiral vase cannot share. The prime tower setting is no obstacle: belt + // printers never print the classic tower, and the belt purge prism is an + // ordinary object that never takes a brim. if (this->has_belt_brim()) { - if (this->has_belt_purge_tower()) - return { L("Brim is not compatible with the belt purge tower. " - "Disable one of them.") }; if (m_config.spiral_mode.value) return { L("Brim is not compatible with spiral vase mode on a belt printer. " "Disable one of them.") }; diff --git a/tests/fff_print/test_skirt_brim.cpp b/tests/fff_print/test_skirt_brim.cpp index d762c49421..ede355c151 100644 --- a/tests/fff_print/test_skirt_brim.cpp +++ b/tests/fff_print/test_skirt_brim.cpp @@ -930,9 +930,9 @@ TEST_CASE("Belt inner-only leading brim does not reject the prime tower or spira CHECK(print.validate().string.empty()); CHECK_FALSE(gcode(print).empty()); } - // An actual purge tower object is different: the purge plan moves objects onto a common - // layer grid after the brim bands are built, so the two are still refused together. - SECTION("a brim is refused next to a belt purge tower object") { + // A purge tower object is accepted too: the purge plan moves every object, apron + // bands included, onto one layer grid. + SECTION("a brim is accepted next to a belt purge tower object") { DynamicPrintConfig config = belt_brim_multifilament_config(2, { { "brim_type", "outer_only" }, { "brim_width", 4 }, @@ -945,12 +945,11 @@ TEST_CASE("Belt inner-only leading brim does not reject the prime tower or spira Print print; Model model; init_print({ cube(20), cube(20) }, print, model, config, &overrides); - CHECK(print.validate().string.empty()); - model.objects.back()->config.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true)); print.apply(model, config); REQUIRE(print.has_belt_purge_tower()); - CHECK_FALSE(print.validate().string.empty()); + CHECK(print.validate().string.empty()); + CHECK_FALSE(gcode(print).empty()); } } From 2782374d8d287eeea24a6badb59ab863fb69bf15 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 3 Oct 2026 13:12:41 -0500 Subject: [PATCH 176/224] Belt: write the belt header outside the optional file header block The G-code viewer takes the belt tilt only from the belt header comments, but they were written inside the header block that is left out when a BTT TFT thumbnail is configured, so such a printer never got belt view. The comments are not part of the header block; they go after it, and after the thumbnails that firmware needs first. --- src/libslic3r/GCode.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index f98cf62fc4..d2fbea5742 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -3093,8 +3093,6 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.write_format("; HEADER_BLOCK_START\n"); // Write information on the generator. file.write_format("; generated by %s on %s\n", Slic3r::header_slic3r_generated().c_str(), Slic3r::Utils::local_timestamp().c_str()); - // Belt printer: embed angle and transform configs in header via virtual hook. - this->write_belt_header(file, print); if (is_bbl_printers) file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str()); //BBS: total layer number @@ -3193,6 +3191,10 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } } + // Belt printer: the tilt and transform settings the G-code viewer reads back. They + // are comments outside the config block, so they go after the thumbnails that a + // BTT TFT firmware needs first, and are written whether or not that header block is. + this->write_belt_header(file, print); // Write some terse information on the slicing parameters. const PrintObject *first_object = print.objects().front(); From 82c462ab2aa4cd5a61979f248d94b966ccb91ecd Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 3 Oct 2026 13:12:41 -0500 Subject: [PATCH 177/224] Prepare view: drop the dormant tilted-bed rendering Plater::set_bed_shape read the belt keys from the plater's own config, which never carries them, so the branch that tilted the bed model, drew the slicing arrow and plane and switched the build volume to belt mode never ran. The Prepare view shows the bed as the slicing pipeline treats it, flat; the gravity arrow from build_plate_tilt stays, as does the preview's belt view, which takes its angle from the G-code header. --- src/libslic3r/BuildVolume.cpp | 30 -------- src/libslic3r/BuildVolume.hpp | 8 -- src/slic3r/GUI/3DBed.cpp | 129 +-------------------------------- src/slic3r/GUI/3DBed.hpp | 18 ----- src/slic3r/GUI/GCodeViewer.cpp | 6 +- src/slic3r/GUI/Plater.cpp | 31 -------- 6 files changed, 3 insertions(+), 219 deletions(-) diff --git a/src/libslic3r/BuildVolume.cpp b/src/libslic3r/BuildVolume.cpp index 4efcb89a91..15e6668bae 100644 --- a/src/libslic3r/BuildVolume.cpp +++ b/src/libslic3r/BuildVolume.cpp @@ -180,31 +180,6 @@ BuildVolume::BuildVolume(const std::vector &printable_area, const double BOOST_LOG_TRIVIAL(debug) << "BuildVolume printable_area clasified as: " << this->type_name(); } -void BuildVolume::set_belt_printer(bool enabled, double angle_deg, bool infinite_y) -{ - m_is_belt_printer = enabled; - m_belt_angle = angle_deg; - m_belt_infinite_y = infinite_y; - - // Restart from the unmodified bbox each call. Without this, toggling - // belt mode off (or switching infinite_y true→false) would leave the - // extents inflated and break collision / object_state checks. - BoundingBoxf bboxf = get_extents(m_bed_shape); - m_bboxf = BoundingBoxf3{ to_3d(bboxf.min, 0.), to_3d(bboxf.max, m_max_print_height) }; - - if (enabled) { - if (infinite_y) { - // Extend the Y bound to a very large value for infinite belt. - m_bboxf.max.y() = 100000.; - } - // Belt printer: the Z extent already equals printable_height (set above), which - // is the usable vertical clearance above the belt. The gantry's axis range is - // sized to reach height/cos(tilt), so no diagonal scaling is applied here — this - // keeps the live "outside build volume" highlight in agreement with Print::validate(). - (void) angle_deg; - } -} - #if 0 // Tests intersections of projected triangles, not just their vertices against a bounding box. // This test also correctly evaluates collision of a non-convex object with the bounding box. @@ -413,11 +388,6 @@ BuildVolume::ObjectState BuildVolume::object_state(const indexed_triangle_set& i build_volume.max.z() = std::numeric_limits::max(); if (ignore_bottom) build_volume.min.z() = -std::numeric_limits::max(); - // Belt printer: extend Y bounds for infinite Y. - if (m_is_belt_printer && m_belt_infinite_y) { - build_volume.min.y() = -std::numeric_limits::max(); - build_volume.max.y() = std::numeric_limits::max(); - } BoundingBox3Base build_volumef(build_volume.min.cast(), build_volume.max.cast()); // The following test correctly interprets intersection of a non-convex object with a rectangular build volume. //return rectangle_test(its, trafo, to_2d(build_volume.min), to_2d(build_volume.max), build_volume.max.z()); diff --git a/src/libslic3r/BuildVolume.hpp b/src/libslic3r/BuildVolume.hpp index 2b0d8c2746..f311d026b0 100644 --- a/src/libslic3r/BuildVolume.hpp +++ b/src/libslic3r/BuildVolume.hpp @@ -57,10 +57,6 @@ public: // Initialize from PrintConfig::printable_area and PrintConfig::printable_height BuildVolume(const std::vector &printable_area, const double printable_height, const std::vector> &extruder_areas, const std::vector& extruder_printable_heights); - // Belt printer configuration. - void set_belt_printer(bool enabled, double angle_deg, bool infinite_y); - bool is_belt_printer() const { return m_is_belt_printer; } - // Source data, unscaled coordinates. const std::vector& printable_area() const { return m_bed_shape; } double printable_height() const { return m_max_print_height; } @@ -143,10 +139,6 @@ private: // Source definition of the print volume height (PrintConfig::printable_height) double m_max_print_height { 0.f }; std::vector m_extruder_printable_height; - // Belt printer state. - bool m_is_belt_printer { false }; - double m_belt_angle { 0. }; - bool m_belt_infinite_y { false }; // Derived values. BuildVolume_Type m_type { BuildVolume_Type::Invalid }; diff --git a/src/slic3r/GUI/3DBed.cpp b/src/slic3r/GUI/3DBed.cpp index 3032059678..69e5a1d340 100644 --- a/src/slic3r/GUI/3DBed.cpp +++ b/src/slic3r/GUI/3DBed.cpp @@ -387,9 +387,6 @@ void Bed3D::render_internal(GLCanvas3D& canvas, const Transform3d& view_matrix, m_model.set_color(m_is_dark ? DEFAULT_MODEL_COLOR_DARK : DEFAULT_MODEL_COLOR); - // Belt printer: bed rotation is applied inside render_model() and render_default() - // using m_is_belt_printer and m_belt_angle members. - switch (m_type) { case Type::System: { render_system(canvas, view_matrix, projection_matrix, bottom); break; } @@ -398,8 +395,6 @@ void Bed3D::render_internal(GLCanvas3D& canvas, const Transform3d& view_matrix, } render_gravity_arrow(view_matrix, projection_matrix); - render_slicing_arrow(view_matrix, projection_matrix); - render_slicing_plane(view_matrix, projection_matrix); glsafe(::glDisable(GL_DEPTH_TEST)); } @@ -700,12 +695,6 @@ void Bed3D::render_model(const Transform3d& view_matrix, const Transform3d& proj shader->start_using(); shader->set_uniform("emission_factor", 0.0f); Transform3d model_matrix = Geometry::assemble_transform(m_model_offset); - // Belt printer: rotate the bed model about the tilt axis so the belt tilt - // is visible. Negative angle: belt surface tilts downward away from the nozzle. - if (m_is_belt_printer && m_belt_angle > 0.f) { - double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle)); - model_matrix = Eigen::AngleAxisd(-angle_rad, belt_tilt_unit_axis()) * model_matrix; - } shader->set_uniform("volume_world_matrix", model_matrix); shader->set_uniform("view_model_matrix", view_matrix * model_matrix); shader->set_uniform("projection_matrix", projection_matrix); @@ -797,114 +786,6 @@ void Bed3D::render_gravity_arrow(const Transform3d& view_matrix, const Transform shader->stop_using(); } -void Bed3D::render_slicing_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix) -{ - if (!m_is_belt_printer || m_belt_angle <= 0.f) - return; - - // Build the arrow model: shorter and wider than the gravity arrow. - if (!m_slicing_arrow.is_initialized()) { - const float stem_length = 15.0f; // shorter than gravity arrow (25) - const float stem_radius = 1.0f; // wider than gravity arrow (~0.33) - const float tip_radius = 3.0f; // wider tip - const float tip_length = 5.0f; - m_slicing_arrow.init_from(stilized_arrow(16, tip_radius, tip_length, stem_radius, stem_length)); - } - - // The slicing direction: layers stack along the gantry normal, i.e. the image of - // +Z under the mesh rotation about the tilt axis. Use the same AngleAxis as the - // slicing pipeline so the arrow matches whichever tilt axis is configured. - double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle)); - Vec3d slice_dir = (Eigen::AngleAxisd(angle_rad, belt_tilt_unit_axis()).toRotationMatrix() - * Vec3d::UnitZ()).normalized(); - - // Compute rotation to align +Z (arrow default) with slice_dir. - Vec3d from = Vec3d::UnitZ(); - double dot = from.dot(slice_dir); - Transform3d rot = Transform3d::Identity(); - if (dot < -0.9999) { - rot = Eigen::AngleAxisd(M_PI, Vec3d::UnitX()) * rot; - } else if (dot < 0.9999) { - Vec3d axis = from.cross(slice_dir).normalized(); - double angle = std::acos(std::clamp(dot, -1.0, 1.0)); - rot = Eigen::AngleAxisd(angle, axis) * rot; - } - - GLShaderProgram* shader = wxGetApp().get_shader("flat"); - if (shader == nullptr) - return; - - // Disable depth test so the arrow is always visible (not occluded by the tilted bed). - glsafe(::glDisable(GL_DEPTH_TEST)); - shader->start_using(); - - const Camera& camera = wxGetApp().plater()->get_camera(); - Transform3d model_matrix = rot; - shader->set_uniform("view_model_matrix", camera.get_view_matrix() * model_matrix); - shader->set_uniform("projection_matrix", camera.get_projection_matrix()); - - m_slicing_arrow.set_color({ 1.0f, 0.2f, 0.6f, 1.0f }); // pink - m_slicing_arrow.render(); - - shader->stop_using(); - glsafe(::glEnable(GL_DEPTH_TEST)); -} - -void Bed3D::render_slicing_plane(const Transform3d& view_matrix, const Transform3d& projection_matrix) -{ - if (!m_is_belt_printer || m_belt_angle <= 0.f) - return; - - // Build a quad in the XZ plane (world frame) representing the belt slicing plane. - // The plane is tilted at belt_angle from horizontal, with normal (0, -sin(a), cos(a)). - // We render it as a semi-transparent quad centered on the build plate. - if (!m_slicing_plane.is_initialized()) { - const float half_size = 120.f; // mm, large enough to be visible - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 }; - init_data.reserve_vertices(4); - init_data.reserve_indices(2); // 2 triangles - - // Quad corners in local frame (XY plane, will be rotated to match slicing plane) - Vec3f n = Vec3f::UnitZ(); - init_data.add_vertex(Vec3f(-half_size, -half_size, 0.f), n); - init_data.add_vertex(Vec3f( half_size, -half_size, 0.f), n); - init_data.add_vertex(Vec3f( half_size, half_size, 0.f), n); - init_data.add_vertex(Vec3f(-half_size, half_size, 0.f), n); - init_data.add_triangle(0, 1, 2); - init_data.add_triangle(0, 2, 3); - - m_slicing_plane.init_from(std::move(init_data)); - } - - GLShaderProgram* shader = wxGetApp().get_shader("flat"); - if (shader == nullptr) - return; - - glsafe(::glEnable(GL_DEPTH_TEST)); - glsafe(::glEnable(GL_BLEND)); - glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); - - shader->start_using(); - - // Show a tilted plane representing the slicing direction. - // The slicing plane is rotated by belt_angle about the tilt axis from horizontal. - // Raise it slightly so it's visible above the bed surface. - double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle)); - Transform3d model_matrix = Transform3d::Identity(); - model_matrix.translate(Vec3d(0., 0., 30.)); - model_matrix.rotate(Eigen::AngleAxisd(angle_rad, belt_tilt_unit_axis())); - - shader->set_uniform("view_model_matrix", view_matrix * model_matrix); - shader->set_uniform("projection_matrix", projection_matrix); - - m_slicing_plane.set_color({ 0.2f, 0.6f, 1.0f, 0.3f }); // semi-transparent blue - m_slicing_plane.render(); - - glsafe(::glDisable(GL_BLEND)); - shader->stop_using(); -} - void Bed3D::render_default(bool bottom, const Transform3d& view_matrix, const Transform3d& projection_matrix) { // m_texture.reset(); @@ -915,15 +796,7 @@ void Bed3D::render_default(bool bottom, const Transform3d& view_matrix, const Tr if (shader != nullptr) { shader->start_using(); - // Belt printer: rotate the default bed about X so the belt tilt is visible. - Transform3d view_model_matrix = view_matrix; - if (m_is_belt_printer && m_belt_angle > 0.f) { - double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle)); - Transform3d belt_rotation = Transform3d::Identity(); - belt_rotation.rotate(Eigen::AngleAxisd(-angle_rad, Vec3d::UnitX())); - view_model_matrix = view_matrix * belt_rotation; - } - shader->set_uniform("view_model_matrix", view_model_matrix); + shader->set_uniform("view_model_matrix", view_matrix); shader->set_uniform("projection_matrix", projection_matrix); glsafe(::glEnable(GL_DEPTH_TEST)); diff --git a/src/slic3r/GUI/3DBed.hpp b/src/slic3r/GUI/3DBed.hpp index 788d8f52a3..4298ff8b1d 100644 --- a/src/slic3r/GUI/3DBed.hpp +++ b/src/slic3r/GUI/3DBed.hpp @@ -111,8 +111,6 @@ private: GLModel m_model; Vec3d m_model_offset{ Vec3d::Zero() }; GLModel m_gravity_arrow; - GLModel m_slicing_arrow; // Pink arrow showing the effective slicing direction - GLModel m_slicing_plane; // Debug: shows the intended slicing plane direction Axes m_axes; float m_scale_factor{ 1.0f }; @@ -122,11 +120,6 @@ private: std::vector> m_extruder_shapes; std::vector m_extruder_heights; bool m_is_dark = false; - // Belt printer state for rendering. - bool m_is_belt_printer = false; - float m_belt_angle = 0.f; - // Tilt axis: 0 = X (belt travels along Y, the common case), 1 = Y. - int m_belt_tilt_axis = 0; public: Bed3D() = default; @@ -148,15 +141,6 @@ public: const BuildVolume& build_volume() const { return m_build_volume; } BuildVolume& build_volume() { return m_build_volume; } - // Belt printer bed settings. tilt_axis: 0 = X (belt along Y), 1 = Y. - void set_belt_printer(bool enabled, float angle_deg, int tilt_axis = 0) { - m_is_belt_printer = enabled; m_belt_angle = angle_deg; m_belt_tilt_axis = tilt_axis; - } - bool is_belt_printer() const { return m_is_belt_printer; } - float belt_angle() const { return m_belt_angle; } - // Unit vector of the tilt axis in bed space. - Vec3d belt_tilt_unit_axis() const { return m_belt_tilt_axis == 1 ? Vec3d::UnitY() : Vec3d::UnitX(); } - // Was the model provided, or was it generated procedurally? Type get_type() const { return m_type; } // Was the model generated procedurally? @@ -196,8 +180,6 @@ private: void render_custom(GLCanvas3D& canvas, const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom); void render_default(bool bottom, const Transform3d& view_matrix, const Transform3d& projection_matrix); void render_gravity_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix); - void render_slicing_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix); - void render_slicing_plane(const Transform3d& view_matrix, const Transform3d& projection_matrix); // BBS: remove the bed picking logic // void register_raycasters_for_picking(const GLModel::Geometry& geometry, const Transform3d& trafo); diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 9193cc5ef8..d34b8c2c63 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1223,10 +1223,8 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const m_loaded_as_preview = false; // Belt printers: drive the designed/raw view UI (legend checkbox, hotkey B, canvas-toolbar - // menu item) from the loaded print here. Plater::set_bed_shape also calls set_belt_printer(), - // but only on bed-shape changes — not reliably on every slice/preview load — so the UI was - // staying hidden even though the (config-driven) designed view rendered. The tilt magnitude - // comes from the G-code header (gcode_result.belt_tilt_angle, abs of the slicing rotation). + // menu item) from the loaded print. The tilt magnitude comes from the G-code header + // (gcode_result.belt_tilt_angle, abs of the slicing rotation). m_belt_view_enabled = print.config().belt_printer.value; m_belt_angle_deg = gcode_result.belt_tilt_angle; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 5b6e2c6300..cb5d2f7ace 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -14145,37 +14145,6 @@ void Plater::priv::set_bed_shape(const Pointfs &shape, Vec2d shape_position = partplate_list.get_current_shape_position(); bool new_shape = bed.set_shape(shape, printable_height, extruder_areas, extruder_heights, custom_model, force_as_custom, shape_position); - // Belt printer: configure build volume and bed rendering for belt mode. - { - const auto *belt_opt = config->option("belt_printer"); - bool is_belt = belt_opt && belt_opt->value; - if (is_belt) { - // The slicing rotation is the single source of truth for the belt tilt: - // its magnitude is the physical tilt angle and its axis is the tilt axis. - auto rot_axis = config->option>("belt_slice_rotation")->value; - double rot_angle = config->opt_float("belt_slice_rotation_angle"); - double belt_angle = std::abs(rot_angle); // physical tilt magnitude - int tilt_axis = (rot_axis == BeltRotationAxis::Y) ? 1 : 0; - bool infinite_y = config->opt_bool("belt_printer_infinite_y"); - bed.build_volume().set_belt_printer(true, belt_angle, infinite_y); - bed.set_belt_printer(true, static_cast(belt_angle), tilt_axis); - if (preview) - preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(true, static_cast(belt_angle)); - // The belt "designed view" back-transform is rebuilt from the print config at - // G-code load time (GCodeViewer::compute_belt_back_transform), so no mesh-side - // inverse needs to be pushed to the viewer here. - } else { - // Reset the BuildVolume belt state too: Bed3D::set_shape early-returns when - // the bed params are unchanged, so a belt->normal switch (or toggling belt off - // on the same printer) would otherwise leave the BuildVolume with - // m_is_belt_printer=true and an inflated Y bbox, wrongly treating out-of-bounds - // objects as printable. Idempotent for a printer that was never belt. - bed.build_volume().set_belt_printer(false, 0., false); - bed.set_belt_printer(false, 0.f); - if (preview) - preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(false, 0.f); - } - } float prev_height_lid, prev_height_rod; partplate_list.get_height_limits(prev_height_lid, prev_height_rod); From ca934716f5db9a08e467a97cb2900f4ad1acbf7f Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 3 Oct 2026 15:39:49 -0500 Subject: [PATCH 178/224] Plate icons: keep them inside the gap to the next plate The plate's icons, number and name scale with the plate's depth, but they sit in the gap to the next plate, which scales with its width. On a long, narrow bed (a 95 x 500 mm belt) they came out 40 mm wide and ran across the neighbouring plate. The scale is now also bounded by the gap, which leaves ordinary beds unchanged. --- src/slic3r/GUI/PartPlate.cpp | 19 +++++++++++++++---- src/slic3r/GUI/PartPlate.hpp | 1 + 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 12f4e423e5..2b9bbbdf02 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -612,6 +612,17 @@ void PartPlate::calc_height_limit() { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "Unable to create height limit top lines\n"; } +// The plate's icons and labels grow with its depth, but they sit in the gap to the +// next plate, which grows with its width: on a long, narrow bed (a belt) they would +// otherwise run across the neighbouring plate. +float PartPlate::icon_scale_factor() const +{ + const BoundingBoxf bed_ext = get_extents(m_shape); + const double by_depth = bed_ext.size().y() / 200.; + const double by_gap = bed_ext.size().x() * LOGICAL_PART_PLATE_GAP / (PARTPLATE_ICON_SIZE + 2 * PARTPLATE_ICON_GAP_LEFT); + return float(std::min(by_depth, by_gap)); +} + void PartPlate::calc_vertex_for_number(int index, bool one_number, GLModel &buffer) { buffer.reset(); @@ -628,7 +639,7 @@ void PartPlate::calc_vertex_for_number(int index, bool one_number, GLModel &buff #else //in the bottom auto bed_ext = get_extents(m_shape); Vec2d p = bed_ext[1]; - float factor = bed_ext.size()(1) / 200.0; + float factor = icon_scale_factor(); float size = PARTPLATE_ICON_SIZE * factor; float offset_y = PARTPLATE_TEXT_OFFSET_Y * factor; float offset_x = (one_number?PARTPLATE_TEXT_OFFSET_X1: PARTPLATE_TEXT_OFFSET_X2) * factor; @@ -650,7 +661,7 @@ void PartPlate::calc_vertex_for_plate_name_edit_icon(GLTexture *texture, int ind ExPolygon poly; auto bed_ext = get_extents(m_shape); Vec2d p = bed_ext[3]; - float factor = bed_ext.size()(1) / 200.0; + float factor = icon_scale_factor(); float icon_sz = factor * PARTPLATE_EDIT_PLATE_NAME_ICON_SIZE; float width = icon_sz; float height = icon_sz; @@ -683,7 +694,7 @@ void PartPlate::calc_vertex_for_icons(int index, PickingModel &model) ExPolygon poly; auto bed_ext = get_extents(m_shape); Vec2d p = bed_ext[2]; - auto factor = bed_ext.size()(1) / 200.0; + float factor = icon_scale_factor(); float size = PARTPLATE_ICON_SIZE * factor; float gap_left = PARTPLATE_ICON_GAP_LEFT * factor; float gap_y = PARTPLATE_ICON_GAP_Y * factor; @@ -2589,7 +2600,7 @@ void PartPlate::generate_plate_name_texture() ExPolygon poly; auto bed_ext = get_extents(m_shape); Vec2d p = bed_ext[3]; - float factor = bed_ext.size()(1) / 200.0; + float factor = icon_scale_factor(); float icon_sz = factor * PARTPLATE_EDIT_PLATE_NAME_ICON_SIZE; float width = icon_sz * m_name_texture.get_width() / m_name_texture.get_height(); // icon size * text_bb_ratio float height = icon_sz; // scale with icon size to preserve ratio while system scaling diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index babc88f364..67e8e300f5 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -178,6 +178,7 @@ private: void calc_triangles_from_polygon(const ExPolygon &poly, GLModel& render_model); void calc_gridlines(const ExPolygon& poly, const BoundingBox& pp_bbox); void calc_height_limit(); + float icon_scale_factor() const; void calc_vertex_for_number(int index, bool one_number, GLModel &buffer); void calc_vertex_for_plate_name_edit_icon(GLTexture *texture, int index, PickingModel &model); void calc_vertex_for_icons(int index, PickingModel &model); From 6a0d07664f60cb09a5ee6418584368b672a180d2 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 3 Oct 2026 18:55:50 -0500 Subject: [PATCH 179/224] Belt purge tower: one prism per plate ensure_belt_purge_tower only ever looked at the current plate and kept a single prism, so the other plates had no tower and switching plates moved the one prism around. Every plate is now planned on its own: a prism that lies on no plate is stale, a plate whose prism matches its recorded inputs is left alone, the rest are deleted and recreated, highest index first. --- src/slic3r/GUI/BeltPurgeTower.cpp | 558 ++++++++++++++++-------------- src/slic3r/GUI/BeltPurgeTower.hpp | 9 +- src/slic3r/GUI/Plater.cpp | 4 +- 3 files changed, 313 insertions(+), 258 deletions(-) diff --git a/src/slic3r/GUI/BeltPurgeTower.cpp b/src/slic3r/GUI/BeltPurgeTower.cpp index 3dc50d4cb2..e90491a972 100644 --- a/src/slic3r/GUI/BeltPurgeTower.cpp +++ b/src/slic3r/GUI/BeltPurgeTower.cpp @@ -33,28 +33,45 @@ namespace GUI { // is sized so each tilted slicing plane's cross-section through the prism can // absorb the worst-case purge volume of one layer; length follows the printed // objects along the belt (plus ramp/height compensation at both tilted ends). -bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, ObjectList *obj_list, BeltPurgeSignature &sig) +bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, ObjectList *obj_list, std::vector &sigs) { auto is_prism = [](const ModelObject *mo) { const ConfigOption *opt = mo->config.option("belt_purge_tower_object"); return opt != nullptr && opt->getBool(); }; - std::vector prism_idxs; + // Every plate gets its own prism. A prism belongs to the plate it lies on; one + // that lies on no plate is stale. + const int plate_count = partplate_list.get_plate_count(); + sigs.resize(size_t(plate_count)); + std::vector> prisms_by_plate(static_cast(plate_count)); + std::vector stale_prisms; for (int i = 0; i < (int) model.objects.size(); ++i) - if (is_prism(model.objects[i])) - prism_idxs.push_back(i); + if (is_prism(model.objects[i])) { + const int plate_idx = model.objects[i]->instances.empty() ? -1 : partplate_list.find_instance(i, 0); + if (plate_idx >= 0 && plate_idx < plate_count) + prisms_by_plate[size_t(plate_idx)].push_back(i); + else + stale_prisms.push_back(i); + } // Deletes prism objects, keeping the sidebar and part plates in sync // (same primitives as Plater::priv::remove(), minus scene update — the - // caller refreshes the scene). - auto remove_prisms = [&](const std::vector &idxs) { + // caller refreshes the scene). Highest index first, so the others stay valid. + auto remove_prisms = [&](std::vector idxs) { + std::sort(idxs.begin(), idxs.end()); for (auto it = idxs.rbegin(); it != idxs.rend(); ++it) { model.delete_object(size_t(*it)); partplate_list.notify_instance_removed(*it, -1); obj_list->delete_object_from_list(size_t(*it)); } }; + auto all_prisms = [&]() { + std::vector all = stale_prisms; + for (const auto &v : prisms_by_plate) + all.insert(all.end(), v.begin(), v.end()); + return all; + }; // Cheap early-out for non-belt printers: only belt printers ever get a belt // purge tower, and this runs on every background-process tick — so avoid the @@ -63,10 +80,11 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object { const auto *belt_pre = wxGetApp().preset_bundle->printers.get_edited_preset().config.option("belt_printer"); if (belt_pre == nullptr || !belt_pre->value) { - sig = BeltPurgeSignature{}; - if (prism_idxs.empty()) + std::fill(sigs.begin(), sigs.end(), BeltPurgeSignature{}); + const std::vector all = all_prisms(); + if (all.empty()) return false; - remove_prisms(prism_idxs); + remove_prisms(all); return true; } } @@ -93,97 +111,15 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object const auto *seq_opt = print_config.option>("print_sequence"); const bool by_object = seq_opt != nullptr && seq_opt->value == PrintSequence::ByObject; - // Filaments used and bounding extent of the non-prism objects on the - // current plate (1-based filament ids; volume extruder 0 = object default). - PartPlate *plate = partplate_list.get_curr_plate(); - std::set filaments; - double x_min = std::numeric_limits::max(); - double x_max = -std::numeric_limits::max(); - double y_min = std::numeric_limits::max(); - double y_max = -std::numeric_limits::max(); - double z_max = 0.; - bool have_objects = false; - if (belt && plate != nullptr) { - for (int obj_idx = 0; obj_idx < (int) model.objects.size(); ++obj_idx) { - const ModelObject *mo = model.objects[obj_idx]; - if (is_prism(mo)) - continue; - int obj_extruder = 1; - if (const ConfigOption *opt = mo->config.option("extruder"); opt != nullptr && opt->getInt() > 0) - obj_extruder = opt->getInt(); - bool any_instance_on_plate = false; - for (int inst_idx = 0; inst_idx < (int) mo->instances.size(); ++inst_idx) { - if (!plate->contain_instance_totally(obj_idx, inst_idx)) - continue; - any_instance_on_plate = true; - const BoundingBoxf3 bb = mo->instance_bounding_box(inst_idx); - x_min = std::min(x_min, bb.min.x()); - x_max = std::max(x_max, bb.max.x()); - y_min = std::min(y_min, bb.min.y()); - y_max = std::max(y_max, bb.max.y()); - z_max = std::max(z_max, bb.max.z()); - } - if (!any_instance_on_plate) - continue; - have_objects = true; - for (const ModelVolume *mv : mo->volumes) - for (int e : mv->get_extruders()) - filaments.insert(e > 0 ? e : obj_extruder); - } - } + // Mixed filament slots are expanded to their physical components below. + std::vector is_mixed; + std::vector comp_strs; + if (const auto *o = full_cfg.option("filament_is_mixed")) + is_mixed = o->values; + if (const auto *o = full_cfg.option("filament_mixed_components")) + comp_strs = o->values; - // A mixed filament slot is VIRTUAL: it never reaches a nozzle. At slice time - // ToolOrdering::resolve_mixed_filaments() replaces it with its physical - // components, so the toolchanges the prism has to absorb are between those - // components, not to the mixed slot itself. Counting the slot as a filament - // of its own therefore over-provisions the prism by one island per mixed slot - // -- the "extra purge tower" -- and, when every component is already used by - // another object, by an island that can never be reached at all. - // - // Expand here with the same helper the backend uses (Print.cpp's sequential - // path), so the GUI sizes the prism against the same filament set the slicer - // will actually produce. No-op when no filament is mixed. - { - // Copies, not references: a ternary with an empty-vector fallback would bind - // a reference to a temporary. - std::vector is_mixed; - std::vector comp_strs; - if (const auto *o = full_cfg.option("filament_is_mixed")) - is_mixed = o->values; - if (const auto *o = full_cfg.option("filament_mixed_components")) - comp_strs = o->values; - if (has_any_mixed_filament(is_mixed)) { - std::vector zero_based; - zero_based.reserve(filaments.size()); - for (int f : filaments) - if (f > 0) - zero_based.push_back((unsigned int) (f - 1)); - zero_based = expand_mixed_filaments(zero_based, is_mixed, comp_strs); - filaments.clear(); - for (unsigned int f : zero_based) - filaments.insert((int) f + 1); - } - } - - const bool wanted = belt && prime_tower_enabled && !by_object && have_objects && filaments.size() > 1; - if (!wanted) { - sig = BeltPurgeSignature{}; - if (prism_idxs.empty()) - return false; - remove_prisms(prism_idxs); - return true; - } - - // --- Sizing ----------------------------------------------------------- - const int n_islands = std::max(1, (int) filaments.size() - 1); - const double gap = 1.0; - // Every disconnected island needs at least 1 mm of printable width. Honor - // the configured total width whenever possible, but never let the island - // layout silently grow past the footprint used for placement. - const double min_width = n_islands + (n_islands - 1) * gap; - const double width = std::max(min_width, - print_config.has("belt_purge_tower_width") ? print_config.opt_float("belt_purge_tower_width") : 35.); - const double printable_width = width - (n_islands - 1) * gap; + const double gap = 1.0; const double layer_h = print_config.has("layer_height") ? print_config.opt_float("layer_height") : 0.2; // Belt geometry. The rotation axis is the gantry tilt axis; the belt @@ -201,15 +137,6 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object const double sin_t = std::sin(theta); const double cot_t = std::cos(theta) / sin_t; - // Parts' extent along the belt-travel axis and the lateral (across-belt) axis. - const double belt_min = belt_is_y ? y_min : x_min; - const double belt_max = belt_is_y ? y_max : x_max; - const double lat_min = belt_is_y ? x_min : y_min; - const double lat_max = belt_is_y ? x_max : y_max; - - // Worst-case purge volume of one layer: up to (filament count - 1) - // toolchanges, each needing the worst flush matrix entry (mirrors the - // volume selection in Print::_plan_belt_purge()). // NOTE: both purge_in_prime_tower and single_extruder_multi_material are // PRINTER options (Preset.cpp s_Preset_printer_options) — read them from the // printer preset. Reading purge_in_prime_tower from the print preset returns @@ -218,71 +145,9 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object // the backend Print::_plan_belt_purge() which reads both from the merged config. const bool use_matrix = (printer_config.has("purge_in_prime_tower") && printer_config.opt_bool("purge_in_prime_tower")) && (printer_config.has("single_extruder_multi_material") && printer_config.opt_bool("single_extruder_multi_material")); - double max_flush = print_config.has("prime_volume") ? print_config.opt_float("prime_volume") : 45.; - if (use_matrix) { - const size_t extruder_nums = wxGetApp().preset_bundle->get_printer_extruder_count(); - const std::vector matrix = get_flush_volumes_matrix( - project_config.option("flush_volumes_matrix")->values, 0, extruder_nums); - const auto * multi_opt = project_config.option("flush_multiplier"); - const double multiplier = multi_opt != nullptr && !multi_opt->values.empty() ? multi_opt->get_at(0) : 1.; - const int n_total = (int) (std::sqrt(double(matrix.size())) + 0.5); - double m = 0.; - for (int i : filaments) - for (int j : filaments) - if (i != j && i <= n_total && j <= n_total) - m = std::max(m, matrix[size_t(i - 1) * n_total + size_t(j - 1)]); - if (m > 0.) - max_flush = m * multiplier; - } - const double v_layer = double(filaments.size() - 1) * max_flush; - - // Height from the per-layer purge demand. A tilted slicing plane cuts a - // printable_width x (height/sin) rectangle out of the bars, so one layer - // slab absorbs printable_width * (height/sin) * layer_height of purge. Solve for the height that - // holds the worst-case per-layer purge, with eta (infill/perimeter packing) - // and a safety margin for the tilt ramps / grid-alignment slop, plus a - // minimum so the tower is a real printable body rather than a sliver. - const double eta = 0.85; - const double safety = 1.6; + const double prime_volume = print_config.has("prime_volume") ? print_config.opt_float("prime_volume") : 45.; const double printable_height = printer_config.has("printable_height") ? printer_config.opt_float("printable_height") : 250.; - double height = safety * v_layer * sin_t / (printable_width * layer_h * eta); - height = std::clamp(height, 8.0, std::max(8.0, printable_height)); - // --- Idempotence (input-keyed) ---------------------------------------- - auto q = [](double v) { return std::lround(v * 10.0); }; // 0.1 mm quantization - BeltPurgeSignature new_sig; - new_sig.valid = true; - new_sig.filament_count = (int) filaments.size(); - new_sig.key[0] = q(width); - new_sig.key[1] = q(layer_h); - new_sig.key[2] = q(height); - new_sig.key[3] = q(belt_min); - new_sig.key[4] = q(belt_max); - new_sig.key[5] = q(lat_min); - new_sig.key[6] = q(z_max); - new_sig.key[7] = static_cast(rot); - new_sig.key[8] = std::lround(theta * 10000.0); - new_sig.key[9] = q(lat_max); - const Vec3d plate_origin = plate->get_origin(); - new_sig.key[10] = q(plate_origin.x()); - new_sig.key[11] = q(plate_origin.y()); - if (const auto *bed_opt = printer_config.option("printable_area"); bed_opt != nullptr && !bed_opt->values.empty()) { - const BoundingBoxf bed = get_extents(bed_opt->values); - new_sig.key[12] = q(bed.max.x()); - new_sig.key[13] = q(bed.max.y()); - } - if (prism_idxs.size() == 1 && model.objects[size_t(prism_idxs.front())]->instances.size() == 1 && new_sig == sig) - return false; // already up to date — do not touch the model - - // --- Position ---------------------------------------------------------- - // Belt-travel axis. With the mesh rotated by theta before slicing, a machine - // point (y,z) maps to slicing-Z = y*sin(theta) + z*cos(theta). The parts - // occupy slicing-Z in [y_min*sin, y_max*sin + z_max*cos], and the bar's - // FULL-cross-section region (the part not in a triangular end ramp) spans - // slicing-Z [belt_start*sin + H*cos, belt_end*sin]. Covering the parts' - // whole band needs belt_start <= y_min - H*cot (bar's own leading ramp) and - // belt_end >= y_max + z_max*cot (parts' top features print further up the - // belt). The trailing z_max*cot term dominates the bar's own ramp. // The bed (printable_area) is plate-local but model instances live in the // plate's world frame, so the plate origin is added to every bed coordinate. const double inset = 1.; @@ -291,34 +156,225 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object bed_opt != nullptr && !bed_opt->values.empty()) bed_ext = get_extents(bed_opt->values); - // Along the belt the bar stops at the end of the plate: a longer bar cannot be - // printed, and the cross-sections it loses there are reported by the purge - // planner when the parts' last layers then purge more than the bar holds. - const double margin = 5.; - const double ramp_compensation = height / sin_t; - const double belt_origin = plate_origin[belt_is_y ? 1 : 0]; - double belt_end = belt_max + margin + ramp_compensation + z_max * cot_t; // + parts' top-feature belt reach - if (bed_ext.defined) - belt_end = std::min(belt_end, belt_origin + (belt_is_y ? bed_ext.max.y() : bed_ext.max.x()) - inset); - const double belt_start = std::max(belt_origin, std::min(belt_min - ramp_compensation, belt_end - 10.)); // leading ramp, toward belt origin - const double length = std::max(belt_end - belt_start, 10.); - belt_end = belt_start + length; - const double belt_center = 0.5 * (belt_start + belt_end); + // What each plate needs, decided before anything is deleted or created. + struct Plan + { + bool wanted = false; + BeltPurgeSignature sig; + int n_islands = 1; + double w_sub = 0., length = 0., height = 0.; + Vec3d center = Vec3d::Zero(); + }; + std::vector plans(static_cast(plate_count)); - // Across-belt: flush against the bed's maximum edge, inset by half the bar - // width so the bar's far edge sits on the boundary and the whole bar stays - // on the bed. lat_min/lat_max come from instance_bounding_box (world frame). - const double lat_origin = plate_origin[belt_is_y ? 0 : 1]; - double lat_center = lat_max + 5. + 0.5 * width; // fallback: just past the parts - if (bed_ext.defined) { - const double bed_lat_max = belt_is_y ? bed_ext.max.x() : bed_ext.max.y(); - lat_center = lat_origin + bed_lat_max - inset - 0.5 * width; + for (int plate_idx = 0; plate_idx < plate_count; ++plate_idx) { + Plan &plan = plans[size_t(plate_idx)]; + PartPlate *plate = partplate_list.get_plate(plate_idx); + + // Filaments used and bounding extent of the non-prism objects on this + // plate (1-based filament ids; volume extruder 0 = object default). + std::set filaments; + double x_min = std::numeric_limits::max(); + double x_max = -std::numeric_limits::max(); + double y_min = std::numeric_limits::max(); + double y_max = -std::numeric_limits::max(); + double z_max = 0.; + bool have_objects = false; + if (belt && plate != nullptr) { + for (int obj_idx = 0; obj_idx < (int) model.objects.size(); ++obj_idx) { + const ModelObject *mo = model.objects[obj_idx]; + if (is_prism(mo)) + continue; + int obj_extruder = 1; + if (const ConfigOption *opt = mo->config.option("extruder"); opt != nullptr && opt->getInt() > 0) + obj_extruder = opt->getInt(); + bool any_instance_on_plate = false; + for (int inst_idx = 0; inst_idx < (int) mo->instances.size(); ++inst_idx) { + if (!plate->contain_instance_totally(obj_idx, inst_idx)) + continue; + any_instance_on_plate = true; + const BoundingBoxf3 bb = mo->instance_bounding_box(inst_idx); + x_min = std::min(x_min, bb.min.x()); + x_max = std::max(x_max, bb.max.x()); + y_min = std::min(y_min, bb.min.y()); + y_max = std::max(y_max, bb.max.y()); + z_max = std::max(z_max, bb.max.z()); + } + if (!any_instance_on_plate) + continue; + have_objects = true; + for (const ModelVolume *mv : mo->volumes) + for (int e : mv->get_extruders()) + filaments.insert(e > 0 ? e : obj_extruder); + } + } + + // A mixed filament slot is VIRTUAL: it never reaches a nozzle. At slice time + // ToolOrdering::resolve_mixed_filaments() replaces it with its physical + // components, so the toolchanges the prism has to absorb are between those + // components, not to the mixed slot itself. Counting the slot as a filament + // of its own therefore over-provisions the prism by one island per mixed slot + // -- the "extra purge tower" -- and, when every component is already used by + // another object, by an island that can never be reached at all. + // + // Expand here with the same helper the backend uses (Print.cpp's sequential + // path), so the GUI sizes the prism against the same filament set the slicer + // will actually produce. No-op when no filament is mixed. + if (has_any_mixed_filament(is_mixed)) { + std::vector zero_based; + zero_based.reserve(filaments.size()); + for (int f : filaments) + if (f > 0) + zero_based.push_back((unsigned int) (f - 1)); + zero_based = expand_mixed_filaments(zero_based, is_mixed, comp_strs); + filaments.clear(); + for (unsigned int f : zero_based) + filaments.insert((int) f + 1); + } + + plan.wanted = belt && prime_tower_enabled && !by_object && have_objects && filaments.size() > 1; + if (!plan.wanted) + continue; + + // --- Sizing ----------------------------------------------------------- + const int n_islands = std::max(1, (int) filaments.size() - 1); + // Every disconnected island needs at least 1 mm of printable width. Honor + // the configured total width whenever possible, but never let the island + // layout silently grow past the footprint used for placement. + const double min_width = n_islands + (n_islands - 1) * gap; + const double width = std::max(min_width, + print_config.has("belt_purge_tower_width") ? print_config.opt_float("belt_purge_tower_width") : 35.); + const double printable_width = width - (n_islands - 1) * gap; + + // Parts' extent along the belt-travel axis and the lateral (across-belt) axis. + const double belt_min = belt_is_y ? y_min : x_min; + const double belt_max = belt_is_y ? y_max : x_max; + const double lat_min = belt_is_y ? x_min : y_min; + const double lat_max = belt_is_y ? x_max : y_max; + + // Worst-case purge volume of one layer: up to (filament count - 1) + // toolchanges, each needing the worst flush matrix entry (mirrors the + // volume selection in Print::_plan_belt_purge()). + double max_flush = prime_volume; + if (use_matrix) { + const size_t extruder_nums = wxGetApp().preset_bundle->get_printer_extruder_count(); + const std::vector matrix = get_flush_volumes_matrix( + project_config.option("flush_volumes_matrix")->values, 0, extruder_nums); + const auto * multi_opt = project_config.option("flush_multiplier"); + const double multiplier = multi_opt != nullptr && !multi_opt->values.empty() ? multi_opt->get_at(0) : 1.; + const int n_total = (int) (std::sqrt(double(matrix.size())) + 0.5); + double m = 0.; + for (int i : filaments) + for (int j : filaments) + if (i != j && i <= n_total && j <= n_total) + m = std::max(m, matrix[size_t(i - 1) * n_total + size_t(j - 1)]); + if (m > 0.) + max_flush = m * multiplier; + } + const double v_layer = double(filaments.size() - 1) * max_flush; + + // Height from the per-layer purge demand. A tilted slicing plane cuts a + // printable_width x (height/sin) rectangle out of the bars, so one layer + // slab absorbs printable_width * (height/sin) * layer_height of purge. Solve for the height that + // holds the worst-case per-layer purge, with eta (infill/perimeter packing) + // and a safety margin for the tilt ramps / grid-alignment slop, plus a + // minimum so the tower is a real printable body rather than a sliver. + const double eta = 0.85; + const double safety = 1.6; + double height = safety * v_layer * sin_t / (printable_width * layer_h * eta); + height = std::clamp(height, 8.0, std::max(8.0, printable_height)); + + // --- Idempotence (input-keyed) ---------------------------------------- + auto q = [](double v) { return std::lround(v * 10.0); }; // 0.1 mm quantization + BeltPurgeSignature &new_sig = plan.sig; + new_sig.valid = true; + new_sig.filament_count = (int) filaments.size(); + new_sig.key[0] = q(width); + new_sig.key[1] = q(layer_h); + new_sig.key[2] = q(height); + new_sig.key[3] = q(belt_min); + new_sig.key[4] = q(belt_max); + new_sig.key[5] = q(lat_min); + new_sig.key[6] = q(z_max); + new_sig.key[7] = static_cast(rot); + new_sig.key[8] = std::lround(theta * 10000.0); + new_sig.key[9] = q(lat_max); + const Vec3d plate_origin = plate->get_origin(); + new_sig.key[10] = q(plate_origin.x()); + new_sig.key[11] = q(plate_origin.y()); + if (bed_ext.defined) { + new_sig.key[12] = q(bed_ext.max.x()); + new_sig.key[13] = q(bed_ext.max.y()); + } + + // --- Position ---------------------------------------------------------- + // Belt-travel axis. With the mesh rotated by theta before slicing, a machine + // point (y,z) maps to slicing-Z = y*sin(theta) + z*cos(theta). The parts + // occupy slicing-Z in [y_min*sin, y_max*sin + z_max*cos], and the bar's + // FULL-cross-section region (the part not in a triangular end ramp) spans + // slicing-Z [belt_start*sin + H*cos, belt_end*sin]. Covering the parts' + // whole band needs belt_start <= y_min - H*cot (bar's own leading ramp) and + // belt_end >= y_max + z_max*cot (parts' top features print further up the + // belt). The trailing z_max*cot term dominates the bar's own ramp. + // + // Along the belt the bar stops at the end of the plate: a longer bar cannot be + // printed, and the cross-sections it loses there are reported by the purge + // planner when the parts' last layers then purge more than the bar holds. + const double margin = 5.; + const double ramp_compensation = height / sin_t; + const double belt_origin = plate_origin[belt_is_y ? 1 : 0]; + double belt_end = belt_max + margin + ramp_compensation + z_max * cot_t; // + parts' top-feature belt reach + if (bed_ext.defined) + belt_end = std::min(belt_end, belt_origin + (belt_is_y ? bed_ext.max.y() : bed_ext.max.x()) - inset); + const double belt_start = std::max(belt_origin, std::min(belt_min - ramp_compensation, belt_end - 10.)); // leading ramp, toward belt origin + const double length = std::max(belt_end - belt_start, 10.); + belt_end = belt_start + length; + const double belt_center = 0.5 * (belt_start + belt_end); + + // Across-belt: flush against the bed's maximum edge, inset by half the bar + // width so the bar's far edge sits on the boundary and the whole bar stays + // on the bed. lat_min/lat_max come from instance_bounding_box (world frame). + const double lat_origin = plate_origin[belt_is_y ? 0 : 1]; + double lat_center = lat_max + 5. + 0.5 * width; // fallback: just past the parts + if (bed_ext.defined) { + const double bed_lat_max = belt_is_y ? bed_ext.max.x() : bed_ext.max.y(); + lat_center = lat_origin + bed_lat_max - inset - 0.5 * width; + } + + plan.n_islands = n_islands; + plan.w_sub = (width - (n_islands - 1) * gap) / n_islands; + plan.length = length; + plan.height = height; + plan.center = Vec3d(belt_is_y ? lat_center : belt_center, + belt_is_y ? belt_center : lat_center, + 0.5 * height); } - const Vec3d desired_center(belt_is_y ? lat_center : belt_center, - belt_is_y ? belt_center : lat_center, - 0.5 * height); + // --- Decide --------------------------------------------------------------- + // A plate whose prism exists and matches its recorded inputs is left alone, so + // subsequent ticks are no-ops until the parts/config actually change. + std::vector to_delete = stale_prisms; + std::vector to_create; + for (int plate_idx = 0; plate_idx < plate_count; ++plate_idx) { + const Plan &plan = plans[size_t(plate_idx)]; + const std::vector &existing = prisms_by_plate[size_t(plate_idx)]; + BeltPurgeSignature &sig = sigs[size_t(plate_idx)]; + if (!plan.wanted) { + sig = BeltPurgeSignature{}; + to_delete.insert(to_delete.end(), existing.begin(), existing.end()); + } else if (existing.size() == 1 && model.objects[size_t(existing.front())]->instances.size() == 1 && plan.sig == sig) { + continue; + } else { + to_delete.insert(to_delete.end(), existing.begin(), existing.end()); + to_create.push_back(plate_idx); + } + } + if (to_delete.empty() && to_create.empty()) + return false; + remove_prisms(to_delete); + + // --- (Re)create ----------------------------------------------------------- // Build the prism as N DISCONNECTED sub-bars side by side across the belt, // N = (filaments - 1) = the worst-case number of toolchanges on one layer. // Why: mark_wiping_extrusions overrides whole extrusion-entity COLLECTIONS, @@ -337,68 +393,64 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object // reintroduce the multi-swap-per-layer absorption bug; a hair over ~2 line // widths also keeps gap-fill from bridging them. 1 mm is about as close as // they can butt up while staying individually purgeable. - const double w_sub = (width - (n_islands - 1) * gap) / n_islands; + for (int plate_idx : to_create) { + const Plan &plan = plans[size_t(plate_idx)]; - // --- (Re)create --------------------------------------------------------- - if (!prism_idxs.empty()) - remove_prisms(prism_idxs); + TriangleMesh prism_mesh; + for (int i = 0; i < plan.n_islands; ++i) { + const double lat_off = i * (plan.w_sub + gap); + // Box dims: lateral = w_sub, along-belt = length, vertical = height. + TriangleMesh box = belt_is_y ? make_cube(plan.w_sub, plan.length, plan.height) // X = lateral, Y = belt + : make_cube(plan.length, plan.w_sub, plan.height); // X = belt, Y = lateral + box.translate(belt_is_y ? Vec3f((float) lat_off, 0.f, 0.f) : Vec3f(0.f, (float) lat_off, 0.f)); + prism_mesh.merge(box); + } - TriangleMesh prism_mesh; - for (int i = 0; i < n_islands; ++i) { - const double lat_off = i * (w_sub + gap); - // Box dims: lateral = w_sub, along-belt = length, vertical = height. - TriangleMesh box = belt_is_y ? make_cube(w_sub, length, height) // X = lateral, Y = belt - : make_cube(length, w_sub, height); // X = belt, Y = lateral - box.translate(belt_is_y ? Vec3f((float) lat_off, 0.f, 0.f) : Vec3f(0.f, (float) lat_off, 0.f)); - prism_mesh.merge(box); + ModelObject *new_object = model.add_object(); + new_object->name = _u8L("Belt Purge Tower"); + new_object->add_instance(); + ModelVolume *new_volume = new_object->add_volume(std::move(prism_mesh)); + new_volume->name = new_object->name; + + auto &cfg = new_object->config; + cfg.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true)); + cfg.set_key_value("flush_into_objects", new ConfigOptionBool(true)); + cfg.set_key_value("extruder", new ConfigOptionInt(1)); + // Sacrificial solid prism: one wall, no shells, dense rectilinear infill — + // every extrusion is overriddable, so the absorbed volume matches the + // cross-section x layer-height estimate used for the height above. + cfg.set_key_value("wall_loops", new ConfigOptionInt(1)); + cfg.set_key_value("top_shell_layers", new ConfigOptionInt(0)); + cfg.set_key_value("bottom_shell_layers", new ConfigOptionInt(0)); + cfg.set_key_value("sparse_infill_density", new ConfigOptionPercent(100)); + cfg.set_key_value("sparse_infill_pattern", new ConfigOptionEnum(ipRectilinear)); + cfg.set_key_value("enable_support", new ConfigOptionBool(false)); + cfg.set_key_value("brim_type", new ConfigOptionEnum(btNoBrim)); + cfg.set_key_value("seam_slope_type", new ConfigOptionEnum(SeamScarfType::None)); + cfg.set_key_value("precise_z_height", new ConfigOptionBool(false)); + + // Position by the belt-calibration pattern: drop to the bed, then translate + // the instance by the delta between the object's ACTUAL bbox center and the + // target. Setting the instance offset directly is unreliable here — the + // freshly added cube's local frame is not centered, so set_offset() lands + // the min corner (not the center) on the target, leaving the bar centered + // on the bed edge with half of it hanging off. + new_object->invalidate_bounding_box(); + new_object->ensure_on_bed(); + const BoundingBoxf3 cur = new_object->bounding_box_exact(); + new_object->translate_instances(Vec3d(plan.center.x() - cur.center().x(), + plan.center.y() - cur.center().y(), + 0.0)); + new_object->instances.front()->set_assemble_transformation(new_object->instances.front()->get_transformation()); + + const size_t obj_idx = model.objects.size() - 1; + // Registers the object in the sidebar and notifies the part plates; + // selection is left untouched (auto-managed object). + obj_list->add_object_to_list(obj_idx, /*call_selection_changed=*/false); + + sigs[size_t(plate_idx)] = plan.sig; } - ModelObject *new_object = model.add_object(); - new_object->name = _u8L("Belt Purge Tower"); - new_object->add_instance(); - ModelVolume *new_volume = new_object->add_volume(std::move(prism_mesh)); - new_volume->name = new_object->name; - - auto &cfg = new_object->config; - cfg.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true)); - cfg.set_key_value("flush_into_objects", new ConfigOptionBool(true)); - cfg.set_key_value("extruder", new ConfigOptionInt(1)); - // Sacrificial solid prism: one wall, no shells, dense rectilinear infill — - // every extrusion is overriddable, so the absorbed volume matches the - // cross-section x layer-height estimate used for the height above. - cfg.set_key_value("wall_loops", new ConfigOptionInt(1)); - cfg.set_key_value("top_shell_layers", new ConfigOptionInt(0)); - cfg.set_key_value("bottom_shell_layers", new ConfigOptionInt(0)); - cfg.set_key_value("sparse_infill_density", new ConfigOptionPercent(100)); - cfg.set_key_value("sparse_infill_pattern", new ConfigOptionEnum(ipRectilinear)); - cfg.set_key_value("enable_support", new ConfigOptionBool(false)); - cfg.set_key_value("brim_type", new ConfigOptionEnum(btNoBrim)); - cfg.set_key_value("seam_slope_type", new ConfigOptionEnum(SeamScarfType::None)); - cfg.set_key_value("precise_z_height", new ConfigOptionBool(false)); - - // Position by the belt-calibration pattern: drop to the bed, then translate - // the instance by the delta between the object's ACTUAL bbox center and the - // target. Setting the instance offset directly is unreliable here — the - // freshly added cube's local frame is not centered, so set_offset() lands - // the min corner (not the center) on the target, leaving the bar centered - // on the bed edge with half of it hanging off. - new_object->invalidate_bounding_box(); - new_object->ensure_on_bed(); - const BoundingBoxf3 cur = new_object->bounding_box_exact(); - new_object->translate_instances(Vec3d(desired_center.x() - cur.center().x(), - desired_center.y() - cur.center().y(), - 0.0)); - new_object->instances.front()->set_assemble_transformation(new_object->instances.front()->get_transformation()); - - const size_t obj_idx = model.objects.size() - 1; - // Registers the object in the sidebar and notifies the part plates; - // selection is left untouched (auto-managed object). - obj_list->add_object_to_list(obj_idx, /*call_selection_changed=*/false); - - // Record the inputs that produced this prism so subsequent ticks are no-ops - // until the parts/config actually change. - sig = new_sig; - return true; } diff --git a/src/slic3r/GUI/BeltPurgeTower.hpp b/src/slic3r/GUI/BeltPurgeTower.hpp index 27d5b47c94..a009eb4d4b 100644 --- a/src/slic3r/GUI/BeltPurgeTower.hpp +++ b/src/slic3r/GUI/BeltPurgeTower.hpp @@ -1,5 +1,7 @@ #pragma once +#include + // ORCA-Belt: auto-managed purge prism for belt printers. // // Kept in its own translation unit (not buried in Plater.cpp) so it stays out @@ -34,12 +36,13 @@ struct BeltPurgeSignature } }; -// Keep the auto-generated belt purge prism in sync with the current config and -// plate contents. Creates / updates / removes the marked prism ModelObject. +// Keep the auto-generated belt purge prisms, one per plate, in sync with the +// current config and plate contents. Creates / updates / removes the marked prism +// ModelObjects; `sigs` holds the last inputs per plate index. // Returns true when the model was mutated (caller should refresh the scene). // Runs on every background-process update, so it is idempotent: it only mutates // the model when the desired prism differs from the cached signature in `sig`. -bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, ObjectList *obj_list, BeltPurgeSignature &sig); +bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, ObjectList *obj_list, std::vector &sigs); } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index cb5d2f7ace..b011d55865 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -6993,7 +6993,7 @@ struct Plater::priv // config and plate contents (thin wrapper over GUI::ensure_belt_purge_tower // in BeltPurgeTower.cpp). Returns true when the model was mutated. bool ensure_belt_purge_tower(); - BeltPurgeSignature m_belt_purge_sig; + std::vector m_belt_purge_sigs; void delete_all_objects_from_model(); void reset(bool apply_presets_change = false); void center_selection(); @@ -10633,7 +10633,7 @@ void Plater::priv::process_validation_warnings(const std::vectorobj_list(), m_belt_purge_sig); + return GUI::ensure_belt_purge_tower(model, partplate_list, sidebar->obj_list(), m_belt_purge_sigs); } From 69e7cc1544c2ae7be32fdf39e2aee6667ee9fc7b Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 3 Oct 2026 19:19:02 -0500 Subject: [PATCH 180/224] Preview: judge a belt print's toolpaths by their back-transformed box all_paths_inside() accepts the path bounding box only within 3*EPSILON of the bed and otherwise tests every move; a belt print's moves are machine-frame coordinates whose Z is belt travel, so that test can never pass, and the designed view's min-corner anchor leaves the box a fraction of a millimetre below zero. Every multi-object belt plate therefore reported a path beyond the plate. The belt preview now judges the back-transformed box with a millimetre of room. --- src/slic3r/GUI/GCodeViewer.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index d34b8c2c63..89bc7e3fab 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1518,8 +1518,18 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const }); m_paths_bounding_box = BoundingBoxf3(libvgcode::convert(bbox[0]).cast(), libvgcode::convert(bbox[1]).cast()); - if (wxGetApp().is_editor()) - m_contained_in_bed = wxGetApp().plater()->build_volume().all_paths_inside(gcode_result, m_paths_bounding_box); + if (wxGetApp().is_editor()) { + if (is_belt) { + // The moves are machine-frame coordinates (Z is belt travel), so the per-move + // test inside all_paths_inside() can never pass on a belt. Judge the + // back-transformed box instead, with room for the designed view's min-corner + // anchor, which is only accurate to a fraction of a millimetre. + BoundingBoxf3 bed = wxGetApp().plater()->build_volume().bounding_volume(); + bed.offset(1.); + m_contained_in_bed = !m_paths_bounding_box.defined || (bed.contains(m_paths_bounding_box.min) && bed.contains(m_paths_bounding_box.max)); + } else + m_contained_in_bed = wxGetApp().plater()->build_volume().all_paths_inside(gcode_result, m_paths_bounding_box); + } m_extruders_count = gcode_result.filaments_count; From ee88b3f0b09926122a5e984c0a6e2e4e2294696e Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sun, 4 Oct 2026 19:16:59 -0500 Subject: [PATCH 181/224] GCode: hold the writer by value again Belt printing turned GCode::m_writer into a unique_ptr so BeltGCode could swap in a freshly built writer carrying the belt kinematics. Nothing subclasses GCodeWriter: the machine mapping lives in its MachineKinematics, which set_kinematics() installs on an existing writer. A GCode is built for every export and the only state on the writer when init_belt_writer() runs is the plate offset, which the swap had to copy across by hand. Install the belt kinematics on the writer in place, drop the copied offset, and drop the virtual markers on GCodeWriter that the old subclass needed. Every m_writer-> in GCode.cpp goes back to m_writer., which is most of the belt diff in that file and most of its conflicts with main. The pressure-advance pattern keeps its shared_ptr writer: the unique_ptr kinematics make GCodeWriter move-only and that class must stay copyable. --- src/libslic3r/BeltGCode.cpp | 8 +- src/libslic3r/GCode.cpp | 470 +++++++++++++++++----------------- src/libslic3r/GCode.hpp | 16 +- src/libslic3r/GCodeWriter.hpp | 15 +- 4 files changed, 251 insertions(+), 258 deletions(-) diff --git a/src/libslic3r/BeltGCode.cpp b/src/libslic3r/BeltGCode.cpp index 4082db66e9..20b19e41b0 100644 --- a/src/libslic3r/BeltGCode.cpp +++ b/src/libslic3r/BeltGCode.cpp @@ -8,14 +8,10 @@ namespace Slic3r { void BeltGCode::init_belt_writer(Print &print) { - auto belt_writer = std::make_unique(); // Axis remap and build volume max are set by base GCode after init_belt_writer // returns; set_kinematics() replays them, so install order does not matter. - install_belt_kinematics(*belt_writer, print.config()); - belt_writer->set_force_normal_lift(true); - // The plate origin was stored on the writer this one replaces. - belt_writer->set_xy_offset(m_gcode_offset.x(), m_gcode_offset.y()); - m_writer = std::move(belt_writer); + install_belt_kinematics(m_writer, print.config()); + m_writer.set_force_normal_lift(true); } void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index d2fbea5742..677b115530 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -1069,7 +1069,7 @@ static std::vector get_path_of_change_filament(const Print& print) //BBS: if needed, write the gcode_label_objects_end then priming tower, if the retract, didn't did it. std::string object_end_label_temp; - gcodegen.m_writer->add_object_end_labels(object_end_label_temp); + gcodegen.m_writer.add_object_end_labels(object_end_label_temp); // Process the custom change_filament_gcode. If it is empty, provide a simple Tn command to change the filament. // Otherwise, leave control to the user completely. @@ -1138,7 +1138,7 @@ static std::vector get_path_of_change_filament(const Print& print) // config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); // BBS { - GCodeWriter& gcode_writer = *gcodegen.m_writer; + GCodeWriter& gcode_writer = gcodegen.m_writer; FullPrintConfig& full_config = gcodegen.m_config; // Per-variant filament arrays can hold one column per variant a filament uses @@ -2002,9 +2002,9 @@ static std::vector get_path_of_change_filament(const Print& print) const std::vector ColorPrintColors::Colors = { "#C0392B", "#E67E22", "#F1C40F", "#27AE60", "#1ABC9C", "#2980B9", "#9B59B6" }; -#define EXTRUDER_CONFIG(OPT) m_config.OPT.get_at(m_writer->filament()->extruder_id()) -#define FILAMENT_CONFIG(OPT) m_config.OPT.get_at(get_filament_config_index(m_writer->filament()->id())) -#define NOZZLE_CONFIG(OPT) m_config.OPT.get_at(get_nozzle_config_index(m_writer->filament()->id())) +#define EXTRUDER_CONFIG(OPT) m_config.OPT.get_at(m_writer.filament()->extruder_id()) +#define FILAMENT_CONFIG(OPT) m_config.OPT.get_at(get_filament_config_index(m_writer.filament()->id())) +#define NOZZLE_CONFIG(OPT) m_config.OPT.get_at(get_nozzle_config_index(m_writer.filament()->id())) void GCode::PlaceholderParserIntegration::reset() { @@ -2492,7 +2492,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu BOOST_LOG_TRIVIAL(info) << boost::format("Will export G-code to %1% soon")%path; GCodeProcessor::s_IsBBLPrinter = print->is_BBL_printer(); - m_writer->set_is_bbl_machine(print->is_BBL_printer()); + m_writer.set_is_bbl_machine(print->is_BBL_printer()); print->set_started(psGCodeExport); // check if any custom gcode contains keywords used by the gcode processor to @@ -2589,7 +2589,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu m_processor.result().support_traditional_timelapse = m_support_traditional_timelapse; bool activate_long_retraction_when_cut = false; - for (const auto& filament : m_writer->extruders()) { + for (const auto& filament : m_writer.extruders()) { size_t fi = get_filament_config_index((int)filament.id()); activate_long_retraction_when_cut |= ( m_config.long_retractions_when_cut.get_at(fi) @@ -2629,7 +2629,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu m_processor.finalize(true); // DoExport::update_print_estimated_times_stats(m_processor, print->m_print_statistics); - DoExport::update_print_estimated_stats(m_processor, m_writer->extruders(), print->m_print_statistics, print->config()); + DoExport::update_print_estimated_stats(m_processor, m_writer.extruders(), print->m_print_statistics, print->config()); // Printed-mass safety check. Flushed filament leaves the bed, so subtract it // from the total to get the mass actually resting on the plate. Gated on machine_max_printed_mass // (>0 only for A2L machines), so no existing printer's gcode_check_result changes. @@ -2637,8 +2637,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu double mass_on_bed_total = print->m_print_statistics.total_weight; for (auto volume : m_processor.get_result().print_statistics.flush_per_filament) { size_t extruder_id = volume.first; - auto extruder = std::find_if(m_writer->extruders().begin(), m_writer->extruders().end(), [extruder_id](const Extruder &extr) { return extr.id() == extruder_id; }); - if (extruder == m_writer->extruders().end()) continue; + auto extruder = std::find_if(m_writer.extruders().begin(), m_writer.extruders().end(), [extruder_id](const Extruder &extr) { return extr.id() == extruder_id; }); + if (extruder == m_writer.extruders().end()) continue; mass_on_bed_total -= (volume.second * extruder->filament_density() * 0.001); } // flushed weight will not be keeped on the hot bed, exclude it if (mass_on_bed_total > m_print->config().machine_max_printed_mass.value) { @@ -2939,7 +2939,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato PROFILE_FUNC(); m_print = &print; - m_timelapse_pos_picker.init(&print,m_writer->get_xy_offset().cast()); + m_timelapse_pos_picker.init(&print,m_writer.get_xy_offset().cast()); // init as filament map update_layer_related_config(0); @@ -2964,7 +2964,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Belt printer: initialize belt-specific writer via virtual hook. this->init_belt_writer(print); - m_writer->set_is_bbl_machine(is_bbl_printers); + m_writer.set_is_bbl_machine(is_bbl_printers); // Standalone axis remap (works with or without belt mode). // Sync the writer's remap state to the current export UNCONDITIONALLY — even at @@ -2975,9 +2975,9 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato int rx = int(print.config().gcode_remap_x.value); int ry = int(print.config().gcode_remap_y.value); int rz = int(print.config().gcode_remap_z.value); - m_writer->set_axis_remap(rx, ry, rz); + m_writer.set_axis_remap(rx, ry, rz); BoundingBoxf bbox_bed(print.config().printable_area.values); - m_writer->set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(), + m_writer.set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(), print.config().printable_height.value)); } @@ -2990,7 +2990,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // per-point (see GCodeWriter::uses_pointwise_travel_speed()), which must not // change for non-belt printers. if (print.config().belt_printer.value) { - m_writer->set_first_layer_plane( + m_writer.set_first_layer_plane( m_first_layer_plane.get(), print.config().initial_layer_print_height.value); } @@ -3362,13 +3362,13 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Disable fan. if (m_config.auxiliary_fan.value && print.config().close_fan_the_first_x_layers.get_at(initial_extruder_id)) { - file.write(m_writer->set_fan(0)); + file.write(m_writer.set_fan(0)); //BBS: disable additional fan - file.write(m_writer->set_additional_fan(0)); + file.write(m_writer.set_additional_fan(0)); } // Update output variables after the extruders were initialized. - m_placeholder_parser_integration.init(*m_writer); + m_placeholder_parser_integration.init(m_writer); // Let the start-up script prime the 1st printing tool. @@ -3482,7 +3482,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato { BoundingBoxf bbox_bed(print.config().printable_area.values); - Vec2f plate_offset = m_writer->get_xy_offset(); + Vec2f plate_offset = m_writer.get_xy_offset(); this->placeholder_parser().set("print_bed_min", new ConfigOptionFloats({ bbox_bed.min.x(), bbox_bed.min.y()})); this->placeholder_parser().set("print_bed_max", new ConfigOptionFloats({ bbox_bed.max.x(), bbox_bed.max.y()})); this->placeholder_parser().set("print_bed_size", new ConfigOptionFloats({ bbox_bed.size().x(), bbox_bed.size().y() })); @@ -3601,7 +3601,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } bool activate_chamber_temp_control = false; auto max_chamber_temp = 0; - for (const auto &extruder : m_writer->extruders()) { + for (const auto &extruder : m_writer.extruders()) { activate_chamber_temp_control |= m_config.activate_chamber_temp_control.get_at(extruder.id()); max_chamber_temp = std::max(max_chamber_temp, m_config.chamber_temperature.get_at(extruder.id())); } @@ -3609,7 +3609,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato BedType curr_bed_type = m_config.curr_bed_type; int min_temperature_vitrification = std::numeric_limits::max(); - for (const auto& extruder : m_writer->extruders()) + for (const auto& extruder : m_writer.extruders()) min_temperature_vitrification = std::min(min_temperature_vitrification, m_config.temperature_vitrification.get_at(extruder.id())); @@ -3729,7 +3729,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato if (activate_chamber_temp_control && max_chamber_temp > 0){ int temp_out =0; if(!custom_gcode_sets_temperature(machine_start_gcode,141,191,false,temp_out)) - file.write(m_writer->set_chamber_temperature(max_chamber_temp, true)); // set chamber_temperature + file.write(m_writer.set_chamber_temperature(max_chamber_temp, true)); // set chamber_temperature } // Write the custom start G-code @@ -3743,11 +3743,11 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.write_format(";%s\n", GCodeProcessor::Machine_Start_GCode_End_Tag.c_str()); //BBS: gcode writer doesn't know where the real position of extruder is after inserting custom gcode - m_writer->set_current_position_clear(false); + m_writer.set_current_position_clear(false); m_start_gcode_filament = GCodeProcessor::get_gcode_last_filament(machine_start_gcode); if (is_bbl_printers) { - m_writer->init_extruder(initial_non_support_extruder_id); + m_writer.init_extruder(initial_non_support_extruder_id); // add the missing filament start gcode in machine start gcode { DynamicConfig config; @@ -3776,7 +3776,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } // Orca: add missing PA settings for initial filament if (m_config.enable_pressure_advance.get_at(initial_non_support_extruder_id)) { - file.write(m_writer->set_pressure_advance(m_config.pressure_advance.get_at(initial_non_support_extruder_id))); + file.write(m_writer.set_pressure_advance(m_config.pressure_advance.get_at(initial_non_support_extruder_id))); // Orca: Adaptive PA // Reset Adaptive PA processor last PA value m_pa_processor->resetPreviousPA(m_config.pressure_advance.get_at(initial_non_support_extruder_id)); @@ -3806,7 +3806,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato int during_print_exhaust_fan_speed = 0; // Orca: when activate_air_filtration is set on any extruder, find and set the highest during_print_exhaust_fan_speed - for (const auto &extruder : m_writer->extruders()) { + for (const auto &extruder : m_writer.extruders()) { size_t fi = get_filament_config_index((int)extruder.id()); if (m_config.activate_air_filtration.get_at(fi) && m_config.activate_air_filtration_during_print.get_at(fi)) { activate_air_filtration_during_print = true; @@ -3816,7 +3816,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } if (activate_air_filtration_during_print) - file.write(m_writer->set_exhaust_fan(during_print_exhaust_fan_speed)); + file.write(m_writer.set_exhaust_fan(during_print_exhaust_fan_speed)); } print.throw_if_canceled(); @@ -3833,7 +3833,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_seam_placer.init(print, throw_if_canceled_func); // BBS: get path for change filament - if (m_writer->multiple_extruders) { + if (m_writer.multiple_extruders) { std::vector points = get_path_of_change_filament(print); if (points.size() == 3) { travel_point_1 = points[0]; @@ -3870,12 +3870,12 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato std::string gcode; gcode += ";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Layer_Change) + "\n"; if ((NOZZLE_CONFIG(outer_wall_acceleration) > 0 && NOZZLE_CONFIG(outer_wall_acceleration) > 0)) { - gcode += m_writer->set_print_acceleration((unsigned int)floor(NOZZLE_CONFIG(outer_wall_acceleration) + 0.5)); + gcode += m_writer.set_print_acceleration((unsigned int)floor(NOZZLE_CONFIG(outer_wall_acceleration) + 0.5)); } if (NOZZLE_CONFIG(outer_wall_jerk) > 0) { double jerk = NOZZLE_CONFIG(outer_wall_jerk); - gcode += m_writer->set_jerk_xy(jerk); + gcode += m_writer.set_jerk_xy(jerk); } auto params = print.calib_params(); @@ -3895,10 +3895,10 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // belt plane, not the slicing plane. const bool belt_world_coords = print.config().belt_printer.value; if (belt_world_coords) - install_belt_kinematics(*m_writer, print.config(), /*world_coordinates=*/true); + install_belt_kinematics(m_writer, print.config(), /*world_coordinates=*/true); gcode += pa_test.generate_test(params.start, params.step, std::llround(std::ceil((params.end - params.start) / params.step)) + 1); if (belt_world_coords) - install_belt_kinematics(*m_writer, print.config(), /*world_coordinates=*/false); + install_belt_kinematics(m_writer, print.config(), /*world_coordinates=*/false); file.write(gcode); } else { @@ -3946,7 +3946,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_enable_cooling_markers = false; // we're not filtering these moves through CoolingBuffer m_avoid_crossing_perimeters.use_external_mp_once(); // BBS. change tool before moving to origin point. - if (m_writer->need_toolchange(initial_extruder_id)) { + if (m_writer.need_toolchange(initial_extruder_id)) { const PrintObjectConfig& object_config = object.config(); coordf_t initial_layer_print_height = print.config().initial_layer_print_height.value; @@ -3959,7 +3959,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } else { file.write(this->retract()); } - file.write(m_writer->travel_to_z(m_max_layer_z + m_writer->config.z_hop.get_at(get_filament_config_index((int)initial_extruder_id)))); + file.write(m_writer.travel_to_z(m_max_layer_z + m_writer.config.z_hop.get_at(get_filament_config_index((int)initial_extruder_id)))); file.write(this->travel_to(Point(0, 0), erNone, "move to origin position for next object")); m_enable_cooling_markers = true; // Disable motion planner when traveling to first object point. @@ -3999,7 +3999,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato { const auto plr_mode = print.config().enable_power_loss_recovery.value; if (m_second_layer_things_done && plr_mode == PowerLossRecoveryMode::Enable) { - file.write(m_writer->enable_power_loss_recovery(PowerLossRecoveryMode::Disable)); + file.write(m_writer.enable_power_loss_recovery(PowerLossRecoveryMode::Disable)); } } ++ finished_objects; @@ -4020,7 +4020,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_wipe_tower->set_wipe_tower_bbx(print.get_wipe_tower_bbx()); m_wipe_tower->set_rib_offset(print.get_rib_offset()); //BBS - file.write(m_writer->travel_to_z(initial_layer_print_height + m_config.z_offset.value, "Move to the first layer height")); + file.write(m_writer.travel_to_z(initial_layer_print_height + m_config.z_offset.value, "Move to the first layer height")); if (wipe_tower_type == WipeTowerType::Type2 && print.config().single_extruder_multi_material_priming) { file.write(m_wipe_tower->prime(*this)); @@ -4079,7 +4079,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Orca: disable power loss recovery if (m_second_layer_things_done && print.config().enable_power_loss_recovery.value == PowerLossRecoveryMode::Enable) { - file.write(m_writer->enable_power_loss_recovery(PowerLossRecoveryMode::Disable)); + file.write(m_writer.enable_power_loss_recovery(PowerLossRecoveryMode::Disable)); } if (m_wipe_tower) // Purge the extruder, pull out the active filament. @@ -4093,14 +4093,14 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // if needed, write the gcode_label_objects_end { std::string gcode; - m_writer->add_object_change_labels(gcode); + m_writer.add_object_change_labels(gcode); file.write(gcode); } - file.write(m_writer->set_fan(0)); + file.write(m_writer.set_fan(0)); //BBS: make sure the additional fan is closed when end if(m_config.auxiliary_fan.value) - file.write(m_writer->set_additional_fan(0)); + file.write(m_writer.set_additional_fan(0)); if (is_bbl_printers) { //BBS: close spaghetti detector //Note: M981 is also used to tell xcam the last layer is finished, so we need always send it even if spaghetti option is disabled. @@ -4122,14 +4122,14 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato DynamicConfig config; config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index)); //BBS - config.set_key_value("layer_z", new ConfigOptionFloat(m_writer->get_position()(2) - m_config.z_offset.value)); + config.set_key_value("layer_z", new ConfigOptionFloat(m_writer.get_position()(2) - m_config.z_offset.value)); config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); config.set_key_value("nozzle_diameter_at_nozzle_id", new ConfigOptionFloats(get_nozzle_diameters_by_nozzle_id(group_result.get()))); config.set_key_value("nozzle_volume_types", new ConfigOptionStrings(get_nozzle_volume_types_by_nozzle_id(group_result.get()))); if (print.config().single_extruder_multi_material) { // Process the filament_end_gcode for the active filament only. - int extruder_id = m_writer->filament()->id(); + int extruder_id = m_writer.filament()->id(); config.set_key_value("filament_extruder_id", new ConfigOptionInt(extruder_id)); config.set_key_value("current_filament_id", new ConfigOptionInt(extruder_id)); config.set_key_value("current_extruder_id", new ConfigOptionInt((int) get_extruder_id(extruder_id))); @@ -4145,13 +4145,13 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.writeln(this->placeholder_parser_process("filament_end_gcode", end_gcode, extruder_id, &config)); } } - file.writeln(this->placeholder_parser_process("machine_end_gcode", print.config().machine_end_gcode, m_writer->filament()->id(), &config)); + file.writeln(this->placeholder_parser_process("machine_end_gcode", print.config().machine_end_gcode, m_writer.filament()->id(), &config)); } - file.write(m_writer->update_progress(m_layer_count, m_layer_count, true)); // 100% - file.write(m_writer->postamble()); + file.write(m_writer.update_progress(m_layer_count, m_layer_count, true)); // 100% + file.write(m_writer.postamble()); if (activate_chamber_temp_control && max_chamber_temp > 0) - file.write(m_writer->set_chamber_temperature(0, false)); //close chamber_temperature + file.write(m_writer.set_chamber_temperature(0, false)); //close chamber_temperature // Orca: when air filtration is supported, check if it needs to be activated after print completion and set the exhaust fan speed accordingly if (m_config.support_air_filtration.value) { @@ -4159,7 +4159,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato int complete_print_exhaust_fan_speed = 0; // Orca: when activate_air_filtration is set on any extruder, find and set the highest complete_print_exhaust_fan_speed - for (const auto& extruder : m_writer->extruders()) { + for (const auto& extruder : m_writer.extruders()) { size_t fi = get_filament_config_index((int)extruder.id()); if (m_config.activate_air_filtration.get_at(fi) && m_config.activate_air_filtration_on_completion.get_at(fi)) { activate_air_filtration_on_completion = true; @@ -4168,7 +4168,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } if (activate_air_filtration_on_completion) - file.write(m_writer->set_exhaust_fan(complete_print_exhaust_fan_speed)); + file.write(m_writer.set_exhaust_fan(complete_print_exhaust_fan_speed)); } // adds tags for time estimators @@ -4181,7 +4181,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.write(DoExport::update_print_stats_and_format_filament_stats( // Const inputs has_wipe_tower, print.wipe_tower_data(), - m_writer->extruders(), + m_writer.extruders(), // Modifies print.m_print_statistics, // Const input (tool-change fallback for non-wipe-tower prints) @@ -4320,8 +4320,8 @@ void GCode::check_placeholder_parser_failed() size_t GCode::cur_extruder_index() const { //TODO: check if the function is duplicated - //just return m_writer->filament()->extruder_id() - return get_extruder_id(m_writer->filament()->id()); + //just return m_writer.filament()->extruder_id() + return get_extruder_id(m_writer.filament()->id()); } size_t GCode::get_extruder_id(unsigned int filament_id) const @@ -4422,7 +4422,7 @@ void GCode::process_layers( ); const auto fan_mover = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, - [&fan_mover = this->m_fan_mover, &config = this->config(), &writer = *this->m_writer](std::string in)->std::string { + [&fan_mover = this->m_fan_mover, &config = this->config(), &writer = this->m_writer](std::string in)->std::string { CNumericLocalesSetter locales_setter; @@ -4522,7 +4522,7 @@ void GCode::process_layers( ); const auto fan_mover = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, - [&fan_mover = this->m_fan_mover, &config = this->config(), &writer = *this->m_writer](std::string in)->std::string { + [&fan_mover = this->m_fan_mover, &config = this->config(), &writer = this->m_writer](std::string in)->std::string { if (config.fan_speedup_time.value != 0 || config.fan_kickstart.value > 0) { if (fan_mover.get() == nullptr) @@ -4590,26 +4590,26 @@ std::string GCode::placeholder_parser_process(const std::string &name, const std PlaceholderParserIntegration &ppi = m_placeholder_parser_integration; try { - ppi.update_from_gcodewriter(*m_writer); + ppi.update_from_gcodewriter(m_writer); std::string output = ppi.parser.process(templ, current_filament_id, config_override, &ppi.output_config, &ppi.context); ppi.validate_output_vector_variables(); const CustomGCodeMotionStateChanges motion_state_changes = custom_gcode_motion_state_changes(output); if (motion_state_changes.acceleration) - m_writer->invalidate_acceleration(); + m_writer.invalidate_acceleration(); if (motion_state_changes.jerk) - m_writer->invalidate_jerk(); + m_writer.invalidate_jerk(); if (const std::vector &pos = ppi.opt_position->values; ppi.position != pos) { // Update G-code writer. - m_writer->set_position({ pos[0], pos[1], pos[2] }); + m_writer.set_position({ pos[0], pos[1], pos[2] }); this->set_last_pos(this->gcode_to_point({ pos[0], pos[1] })); } - for (const Extruder &e : m_writer->extruders()) { + for (const Extruder &e : m_writer.extruders()) { unsigned int eid = e.id(); assert(eid < ppi.num_extruders); if ( eid < ppi.num_extruders) { - if (! m_writer->config.use_relative_e_distances && ! is_approx(ppi.e_position[eid], ppi.opt_e_position->values[eid])) + if (! m_writer.config.use_relative_e_distances && ! is_approx(ppi.e_position[eid], ppi.opt_e_position->values[eid])) const_cast(e).set_position(ppi.opt_e_position->values[eid]); if (! is_approx(ppi.e_retracted[eid], ppi.opt_e_retracted->values[eid]) || ! is_approx(ppi.e_restart_extra[eid], ppi.opt_e_restart_extra->values[eid])) @@ -4646,7 +4646,7 @@ void GCode::print_machine_envelope(GCodeOutputStream &file, Print &print) // Get all physical tool ids current print will use std::unordered_set used_extruders; - for (const auto& extruder : m_writer->extruders()) { + for (const auto& extruder : m_writer.extruders()) { used_extruders.insert(extruder.extruder_id()); } @@ -4770,9 +4770,9 @@ void GCode::_print_first_layer_bed_temperature(GCodeOutputStream &file, Print &p temp = temp_by_gcode; #endif - // Always call m_writer->set_bed_temperature() so it will set the internal "current" state of the bed temp as if + // Always call m_writer.set_bed_temperature() so it will set the internal "current" state of the bed temp as if // the custom start G-code emited these. - std::string set_temp_gcode = m_writer->set_bed_temperature(bed_temp, wait); + std::string set_temp_gcode = m_writer.set_bed_temperature(bed_temp, wait); if (! temp_set_by_gcode) file.write(set_temp_gcode); } @@ -4792,14 +4792,14 @@ void GCode::_print_first_layer_extruder_temperatures(GCodeOutputStream &file, Pr int temp = print.config().nozzle_temperature_initial_layer.get_at(get_filament_config_index((int)first_printing_extruder_id)); if (temp_by_gcode >= 0 && temp_by_gcode < 1000) temp = temp_by_gcode; - m_writer->set_temperature(temp, wait, first_printing_extruder_id); + m_writer.set_temperature(temp, wait, first_printing_extruder_id); } else { // Custom G-code does not set the extruder temperature. Do it now. if (print.config().single_extruder_multi_material.value) { // Set temperature of the first printing extruder only. int temp = print.config().nozzle_temperature_initial_layer.get_at(get_filament_config_index((int)first_printing_extruder_id)); if (temp > 0) - file.write(m_writer->set_temperature(temp, wait, first_printing_extruder_id)); + file.write(m_writer.set_temperature(temp, wait, first_printing_extruder_id)); } else { // Set temperatures of all the printing extruders. for (unsigned int tool_id : print.extruders()) { @@ -4811,7 +4811,7 @@ void GCode::_print_first_layer_extruder_temperatures(GCodeOutputStream &file, Pr temp = print.config().idle_temperature.get_at(tool_id); } if (temp > 0) - file.write(m_writer->set_temperature(temp, wait, tool_id)); + file.write(m_writer.set_temperature(temp, wait, tool_id)); } } } @@ -5343,7 +5343,7 @@ LayerResult GCode::process_belt_brim_layer( std::string gcode; const unsigned int extruder_id = layer_tools.extruders.front(); - if (m_writer->filament() == nullptr || m_writer->filament()->id() != extruder_id) + if (m_writer.filament() == nullptr || m_writer.filament()->id() != extruder_id) gcode += this->set_extruder(extruder_id, print_z); // An apron band is a real printed layer: it is counted in m_layer_count, it advances @@ -5370,7 +5370,7 @@ LayerResult GCode::process_belt_brim_layer( config.set_key_value("layer_z", new ConfigOptionFloat(print_z)); config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); gcode += this->placeholder_parser_process("before_layer_change_gcode", - print.config().before_layer_change_gcode.value, m_writer->filament()->id(), &config) + "\n"; + print.config().before_layer_change_gcode.value, m_writer.filament()->id(), &config) + "\n"; } gcode += this->change_layer(print_z); @@ -5381,12 +5381,12 @@ LayerResult GCode::process_belt_brim_layer( config.set_key_value("layer_z", new ConfigOptionFloat(print_z)); config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); gcode += this->placeholder_parser_process("layer_change_gcode", - print.config().layer_change_gcode.value, m_writer->filament()->id(), &config) + "\n"; + print.config().layer_change_gcode.value, m_writer.filament()->id(), &config) + "\n"; } // Objects sharing this apron Z may use different brim filaments; print each in its own tool. for (const unsigned int brim_extruder : layer_tools.extruders) { - if (m_writer->filament() == nullptr || m_writer->filament()->id() != brim_extruder) + if (m_writer.filament() == nullptr || m_writer.filament()->id() != brim_extruder) gcode += this->set_extruder(brim_extruder, print_z); gcode += this->emit_belt_brim_bands(print, layers, single_object_instance_idx, brim_extruder); } @@ -5647,13 +5647,13 @@ std::string GCode::generate_timelapse_gcode(const Print &print, coordf_t print_z const std::vector *printed_objects, bool skip_pos_pick) { - if (!m_writer->filament()) + if (!m_writer.filament()) return {}; PosPickCtx ctx; - ctx.curr_pos = { (coord_t)(scale_(m_writer->get_position().x())),(coord_t)(scale_(m_writer->get_position().y())) }; + ctx.curr_pos = { (coord_t)(scale_(m_writer.get_position().x())),(coord_t)(scale_(m_writer.get_position().y())) }; ctx.curr_layer = this->layer(); - ctx.curr_extruder_id = m_writer->filament()->extruder_id(); + ctx.curr_extruder_id = m_writer.filament()->extruder_id(); ctx.picture_extruder_id = most_used_extruder; if (m_farthest_point_timelapse.enabled) { // farthest_point is stored in the global print frame (includes plate origin); the picker works in @@ -5684,17 +5684,17 @@ std::string GCode::generate_timelapse_gcode(const Print &print, coordf_t print_z // that leaves the toggle off, so their template evaluates to the pre-existing +0.4 lift byte-for-byte. config.set_key_value("farthest_point_timelapse_enabled", new ConfigOptionBool(m_farthest_point_timelapse.enabled)); config.set_key_value("clear_to_x0", new ConfigOptionBool(m_timelapse_pos_picker.get_is_clear_to_x0(ctx))); - timelapse_gcode = this->placeholder_parser_process("timelapse_gcode", print.config().time_lapse_gcode.value, m_writer->filament()->id(), &config) + "\n"; + timelapse_gcode = this->placeholder_parser_process("timelapse_gcode", print.config().time_lapse_gcode.value, m_writer.filament()->id(), &config) + "\n"; } if (!timelapse_gcode.empty()) { - m_writer->set_current_position_clear(false); + m_writer.set_current_position_clear(false); double temp_z_after_tool_change; if (GCodeProcessor::get_last_z_from_gcode(timelapse_gcode, temp_z_after_tool_change)) { - Vec3d pos = m_writer->get_position(); + Vec3d pos = m_writer.get_position(); pos(2) = temp_z_after_tool_change; - m_writer->set_position(pos); + m_writer.set_position(pos); } } @@ -5778,7 +5778,7 @@ LayerResult GCode::process_layer( m_cur_layer_idx = layer.id(); // A per-layer nozzle grouping can move the active filament to another variant column on a // layer boundary without a toolchange, so re-resolve the writer's config column here. - if (Extruder *cur_filament = m_writer->filament()) + if (Extruder *cur_filament = m_writer.filament()) cur_filament->set_config_index((int)get_filament_config_index((int)cur_filament->id())); LayerResult result { {}, layer.id(), false, last_layer }; if (layer_tools.extruders.empty()) @@ -5791,7 +5791,7 @@ LayerResult GCode::process_layer( //support is attached above the object, and support layers has independent layer height, then the lowest support //interface layer id is 0. bool first_layer = (layer.id() == 0 && abs(layer.bottom_z()) < EPSILON); - m_writer->set_is_first_layer(first_layer); + m_writer.set_is_first_layer(first_layer); unsigned int first_extruder_id = layer_tools.extruders.front(); // Initialize config with the 1st object to be printed at this layer. @@ -5841,7 +5841,7 @@ LayerResult GCode::process_layer( config.set_key_value("layer_z", new ConfigOptionFloat(print_z)); config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); gcode += this->placeholder_parser_process("before_layer_change_gcode", - print.config().before_layer_change_gcode.value, m_writer->filament()->id(), &config) + print.config().before_layer_change_gcode.value, m_writer.filament()->id(), &config) + "\n"; } @@ -5886,7 +5886,7 @@ LayerResult GCode::process_layer( config.set_key_value("layer_z", new ConfigOptionFloat(print_z)); config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); gcode += this->placeholder_parser_process("timelapse_gcode", - print.config().time_lapse_gcode.value, m_writer->filament()->id(), &config) + print.config().time_lapse_gcode.value, m_writer.filament()->id(), &config) + "\n"; } @@ -5896,11 +5896,11 @@ LayerResult GCode::process_layer( config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index)); config.set_key_value("layer_z", new ConfigOptionFloat(print_z)); { - const int cur_filament_id = (int) m_writer->filament()->id(); + const int cur_filament_id = (int) m_writer.filament()->id(); const auto group_result = m_print->get_layered_nozzle_group_result(); config.set_key_value("current_filament_id", new ConfigOptionInt(cur_filament_id)); config.set_key_value("current_nozzle_id", new ConfigOptionInt( - nozzle_id_for_gcode_placeholder(group_result, cur_filament_id, (int) m_writer->filament()->extruder_id(), m_layer_index))); + nozzle_id_for_gcode_placeholder(group_result, cur_filament_id, (int) m_writer.filament()->extruder_id(), m_layer_index))); } // Bedslinger mass model. Compute the running printed mass at this layer (same weight calc as @@ -5909,7 +5909,7 @@ LayerResult GCode::process_layer( // returns the plain min Y accel unless the A2L force/bed-mass keys are set, so this is inert for every // existing printer. PrintStatistics curr_print_statistics; - for (const Extruder &extruder : m_writer->extruders()) { + for (const Extruder &extruder : m_writer.extruders()) { double extruded_volume = extruder.extruded_volume() + (has_wipe_tower ? print.wipe_tower_data().used_filament[extruder.id()] * 2.4052f : 0.f); // assumes 1.75mm filament diameter double filament_weight = extruded_volume * extruder.filament_density() * 0.001; if (filament_weight > 0.) @@ -5925,7 +5925,7 @@ LayerResult GCode::process_layer( config.set_key_value("curr_layer_mass", new ConfigOptionFloat(curr_layer_mass)); gcode += this->placeholder_parser_process("layer_change_gcode", - print.config().layer_change_gcode.value, m_writer->filament()->id(), &config) + print.config().layer_change_gcode.value, m_writer.filament()->id(), &config) + "\n"; config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); } @@ -6004,7 +6004,7 @@ LayerResult GCode::process_layer( case CalibMode::Calib_Input_shaping_freq: { if (m_layer_index == 1){ gcode += writer().set_input_shaping('A', print.calib_params().start, 0.f, print.calib_params().shaper_type); - if (m_writer->get_gcode_flavor() == gcfKlipper) { + if (m_writer.get_gcode_flavor() == gcfKlipper) { // Disable minimum cruise ratio to ensure consistent motion for calibration gcode += "SET_VELOCITY_LIMIT MINIMUM_CRUISE_RATIO=0\n"; } @@ -6020,7 +6020,7 @@ LayerResult GCode::process_layer( } case CalibMode::Calib_Input_shaping_damp: { if (m_layer_index == 1){ - if (m_writer->get_gcode_flavor() == gcfKlipper) { + if (m_writer.get_gcode_flavor() == gcfKlipper) { // Disable minimum cruise ratio to ensure consistent motion for calibration gcode += "SET_VELOCITY_LIMIT MINIMUM_CRUISE_RATIO=0\n"; } @@ -6032,7 +6032,7 @@ LayerResult GCode::process_layer( break; } case CalibMode::Calib_Cornering: { - if (m_writer->get_gcode_flavor() == gcfMarlinFirmware && + if (m_writer.get_gcode_flavor() == gcfMarlinFirmware && !m_config.machine_max_junction_deviation.values.empty() && m_config.machine_max_junction_deviation.values.front() > 0) { gcode += writer().set_junction_deviation(this->interpolate_value_across_layers(print.calib_params().start, print.calib_params().end)); @@ -6047,15 +6047,15 @@ LayerResult GCode::process_layer( if (first_layer) { // Orca: we don't need to optimize the Klipper as only set once if (NOZZLE_CONFIG(default_acceleration) > 0 && NOZZLE_CONFIG(initial_layer_acceleration) > 0) { - gcode += m_writer->set_print_acceleration((unsigned int)floor(NOZZLE_CONFIG(initial_layer_acceleration) + 0.5)); + gcode += m_writer.set_print_acceleration((unsigned int)floor(NOZZLE_CONFIG(initial_layer_acceleration) + 0.5)); } if (NOZZLE_CONFIG(default_jerk) > 0 && NOZZLE_CONFIG(initial_layer_jerk) > 0) { - gcode += m_writer->set_jerk_xy(NOZZLE_CONFIG(initial_layer_jerk)); + gcode += m_writer.set_jerk_xy(NOZZLE_CONFIG(initial_layer_jerk)); } - if (m_writer->get_gcode_flavor() == gcfMarlinFirmware && NOZZLE_CONFIG(default_junction_deviation) > 0) { - gcode += m_writer->set_junction_deviation(NOZZLE_CONFIG(default_junction_deviation)); + if (m_writer.get_gcode_flavor() == gcfMarlinFirmware && NOZZLE_CONFIG(default_junction_deviation) > 0) { + gcode += m_writer.set_junction_deviation(NOZZLE_CONFIG(default_junction_deviation)); } } @@ -6093,7 +6093,7 @@ LayerResult GCode::process_layer( if (past_first_layer_band && !m_second_layer_things_done) { // Orca: set power loss recovery const auto plr_mode = print.config().enable_power_loss_recovery.value; - gcode += m_writer->enable_power_loss_recovery(plr_mode); + gcode += m_writer.enable_power_loss_recovery(plr_mode); if (print.is_BBL_printer()) { // BBS: open first layer inspection at second layer @@ -6108,24 +6108,24 @@ LayerResult GCode::process_layer( // Reset acceleration at sencond layer // Orca: only set once, don't need to call set_accel_and_jerk if (NOZZLE_CONFIG(default_acceleration) > 0 && NOZZLE_CONFIG(initial_layer_acceleration) > 0) { - gcode += m_writer->set_print_acceleration((unsigned int) floor(NOZZLE_CONFIG(default_acceleration) + 0.5)); + gcode += m_writer.set_print_acceleration((unsigned int) floor(NOZZLE_CONFIG(default_acceleration) + 0.5)); } if (NOZZLE_CONFIG(default_jerk) > 0 && NOZZLE_CONFIG(initial_layer_jerk) > 0) { - gcode += m_writer->set_jerk_xy(NOZZLE_CONFIG(default_jerk)); + gcode += m_writer.set_jerk_xy(NOZZLE_CONFIG(default_jerk)); } // Transition from 1st to 2nd layer. Adjust nozzle temperatures as prescribed by the nozzle dependent // nozzle_temperature_initial_layer vs. temperature settings. - for (const Extruder& extruder : m_writer->extruders()) { + for (const Extruder& extruder : m_writer.extruders()) { if ((print.config().single_extruder_multi_material.value || m_ooze_prevention.enable) && - extruder.id() != m_writer->filament()->id()) + extruder.id() != m_writer.filament()->id()) // In single extruder multi material mode, set the temperature for the current extruder only. continue; size_t fi = get_filament_config_index((int)extruder.id()); int temperature = print.config().nozzle_temperature.get_at(fi); if (temperature > 0 && temperature != print.config().nozzle_temperature_initial_layer.get_at(fi)) - gcode += m_writer->set_temperature(temperature, false, extruder.id()); + gcode += m_writer.set_temperature(temperature, false, extruder.id()); } // BBS @@ -6134,14 +6134,14 @@ LayerResult GCode::process_layer( bed_temp = get_highest_bed_temperature(false,print); else bed_temp = get_bed_temperature(first_extruder_id, false, m_config.curr_bed_type); - gcode += m_writer->set_bed_temperature(bed_temp); + gcode += m_writer.set_bed_temperature(bed_temp); // Mark the temperature transition from 1st to 2nd layer to be finished. m_second_layer_things_done = true; } if (single_object_instance_idx == size_t(-1)) { // Normal (non-sequential) print. - gcode += ProcessLayer::emit_custom_gcode_per_print_z(*this, layer_tools.custom_gcode, m_writer->filament()->id(), first_extruder_id, print.config()); + gcode += ProcessLayer::emit_custom_gcode_per_print_z(*this, layer_tools.custom_gcode, m_writer.filament()->id(), first_extruder_id, print.config()); } // BBS: get next extruder according to flush and soluble @@ -6172,7 +6172,7 @@ LayerResult GCode::process_layer( return r->has_extrusions(); }); const bool enable_overhang_speed = std::any_of(regions.begin(), regions.end(), [this](const LayerRegion* r) { - return r->has_extrusions() && r->region().config().enable_overhang_speed.get_at(get_nozzle_config_index(m_writer->filament()->id())); + return r->has_extrusions() && r->region().config().enable_overhang_speed.get_at(get_nozzle_config_index(m_writer.filament()->id())); }); const bool enable_overhang_fan = m_enable_cooling_markers && has_extrusions && std::any_of(m_config.enable_overhang_bridge_fan.values.begin(), @@ -6622,16 +6622,16 @@ LayerResult GCode::process_layer( config.set_key_value("layer_z", new ConfigOptionFloat(print_z)); config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); config.set_key_value("most_used_physical_extruder_id", new ConfigOptionInt(m_config.physical_extruder_map.get_at(most_used_extruder))); - config.set_key_value("curr_physical_extruder_id", new ConfigOptionInt(m_config.physical_extruder_map.get_at(m_writer->filament()->extruder_id()))); - wrapping_gcode = this->placeholder_parser_process("wrapping_detection_gcode", print.config().wrapping_detection_gcode.value, m_writer->filament()->id(), &config) +"\n"; + config.set_key_value("curr_physical_extruder_id", new ConfigOptionInt(m_config.physical_extruder_map.get_at(m_writer.filament()->extruder_id()))); + wrapping_gcode = this->placeholder_parser_process("wrapping_detection_gcode", print.config().wrapping_detection_gcode.value, m_writer.filament()->id(), &config) +"\n"; } - m_writer->set_current_position_clear(false); + m_writer.set_current_position_clear(false); double temp_z_after_tool_change; if (GCodeProcessor::get_last_z_from_gcode(wrapping_gcode, temp_z_after_tool_change)) { - Vec3d pos = m_writer->get_position(); + Vec3d pos = m_writer.get_position(); pos(2) = temp_z_after_tool_change; - m_writer->set_position(pos); + m_writer.set_position(pos); } return wrapping_gcode; }; @@ -6716,7 +6716,7 @@ LayerResult GCode::process_layer( if (should_insert) { gcode += this->retract(false, false, auto_lift_type, true); - m_writer->add_object_change_labels(gcode); + m_writer.add_object_change_labels(gcode); gcode += insert_timelapse_gcode(); has_insert_timelapse_gcode = true; @@ -6736,7 +6736,7 @@ LayerResult GCode::process_layer( if (need_insert_timelapse_gcode_for_traditional && !has_insert_timelapse_gcode && !(m_farthest_point_timelapse.enabled && m_farthest_point_timelapse.farthest_is_photo_head) && - m_writer->need_toolchange(extruder_id) && + m_writer.need_toolchange(extruder_id) && m_config.nozzle_diameter.values.size() == 2 && writer().filament()) { bool curr_is_photo_head = get_extruder_id(writer().filament()->id()) == most_used_extruder; @@ -6744,7 +6744,7 @@ LayerResult GCode::process_layer( bool should_insert = is_case_b ? !curr_is_photo_head : curr_is_photo_head; if (should_insert) { gcode += this->retract(false, false, auto_lift_type, true); - m_writer->add_object_change_labels(gcode); + m_writer.add_object_change_labels(gcode); gcode += insert_timelapse_gcode(); has_insert_timelapse_gcode = true; @@ -6831,19 +6831,19 @@ LayerResult GCode::process_layer( // exclude objects if (m_enable_exclude_object) { if (is_BBL_Printer()) { - m_writer->set_object_start_str( + m_writer.set_object_start_str( std::string("; start printing object, unique label id: ") + std::to_string(instance_to_print.label_object_id) + "\n" + "M624 " + _encode_label_ids_to_base64({instance_to_print.label_object_id}) + "\n"); } else { const auto gflavor = print.config().gcode_flavor.value; if (gflavor == gcfKlipper) { - m_writer->set_object_start_str(std::string("EXCLUDE_OBJECT_START NAME=") + + m_writer.set_object_start_str(std::string("EXCLUDE_OBJECT_START NAME=") + get_instance_name(&instance_to_print.print_object, inst.id) + "\n"); } else if (gflavor == gcfMarlinLegacy || gflavor == gcfMarlinFirmware || gflavor == gcfRepRapFirmware) { std::string str = std::string("M486 S") + std::to_string(inst.unique_id) + "\n"; - m_writer->set_object_start_str(str); + m_writer.set_object_start_str(str); } } } @@ -6984,20 +6984,20 @@ LayerResult GCode::process_layer( } // exclude objects // Don't set m_gcode_label_objects_end if you don't had to write the m_gcode_label_objects_start. - if (!m_writer->is_object_start_str_empty()) { - m_writer->set_object_start_str(""); + if (!m_writer.is_object_start_str_empty()) { + m_writer.set_object_start_str(""); } else if (m_enable_exclude_object) { if (is_BBL_Printer()) { - m_writer->set_object_end_str(std::string("; stop printing object, unique label id: ") + + m_writer.set_object_end_str(std::string("; stop printing object, unique label id: ") + std::to_string(instance_to_print.label_object_id) + "\n" + "M625\n"); } else { const auto gflavor = print.config().gcode_flavor.value; if (gflavor == gcfKlipper) { - m_writer->set_object_end_str(std::string("EXCLUDE_OBJECT_END NAME=") + + m_writer.set_object_end_str(std::string("EXCLUDE_OBJECT_END NAME=") + get_instance_name(&instance_to_print.print_object, inst.id) + "\n"); } else if (gflavor == gcfMarlinLegacy || gflavor == gcfMarlinFirmware || gflavor == gcfRepRapFirmware) { - m_writer->set_object_end_str(std::string("M486 S-1\n")); + m_writer.set_object_end_str(std::string("M486 S-1\n")); } } } @@ -7062,17 +7062,17 @@ LayerResult GCode::process_layer( } if (m_enable_exclude_object) { if (is_BBL_Printer()) { - m_writer->set_object_start_str( + m_writer.set_object_start_str( std::string("; start printing object, unique label id: ") + std::to_string(instance_to_print.label_object_id) + "\n" + "M624 " + _encode_label_ids_to_base64({instance_to_print.label_object_id}) + "\n"); } else { const auto gflavor = print.config().gcode_flavor.value; if (gflavor == gcfKlipper) { - m_writer->set_object_start_str(std::string("EXCLUDE_OBJECT_START NAME=") + + m_writer.set_object_start_str(std::string("EXCLUDE_OBJECT_START NAME=") + get_instance_name(&instance_to_print.print_object, inst.id) + "\n"); } else if (gflavor == gcfMarlinLegacy || gflavor == gcfMarlinFirmware || gflavor == gcfRepRapFirmware) { - m_writer->set_object_start_str(std::string("M486 S") + std::to_string(inst.unique_id) + "\n"); + m_writer.set_object_start_str(std::string("M486 S") + std::to_string(inst.unique_id) + "\n"); } } } @@ -7284,20 +7284,20 @@ LayerResult GCode::process_layer( } // --- Shared instance footer (mirrors Orca's main instance loop) --- - if (!m_writer->is_object_start_str_empty()) { - m_writer->set_object_start_str(""); + if (!m_writer.is_object_start_str_empty()) { + m_writer.set_object_start_str(""); } else if (m_enable_exclude_object) { if (is_BBL_Printer()) { - m_writer->set_object_end_str(std::string("; stop printing object, unique label id: ") + + m_writer.set_object_end_str(std::string("; stop printing object, unique label id: ") + std::to_string(instance_to_print.label_object_id) + "\n" + "M625\n"); } else { const auto gflavor = print.config().gcode_flavor.value; if (gflavor == gcfKlipper) { - m_writer->set_object_end_str(std::string("EXCLUDE_OBJECT_END NAME=") + + m_writer.set_object_end_str(std::string("EXCLUDE_OBJECT_END NAME=") + get_instance_name(&instance_to_print.print_object, inst.id) + "\n"); } else if (gflavor == gcfMarlinLegacy || gflavor == gcfMarlinFirmware || gflavor == gcfRepRapFirmware) { - m_writer->set_object_end_str(std::string("M486 S-1\n")); + m_writer.set_object_end_str(std::string("M486 S-1\n")); } } } @@ -7310,7 +7310,7 @@ LayerResult GCode::process_layer( // wipe tower's add_object_end_labels may consume it into a local temp string and the // M625 would be lost for BBL printers. if (!layer_tools.mixed_sub_layer_groups.empty()) { - m_writer->add_object_end_labels(gcode); + m_writer.add_object_end_labels(gcode); m_nominal_z = print_z; m_need_change_layer_lift_z = true; } @@ -7339,7 +7339,7 @@ LayerResult GCode::process_layer( for (size_t instance_id = i_begin; instance_id < i_end && instance_id < obj->instances().size(); ++ instance_id) { if (! belt_brim_emitted.insert({ ltp_idx, instance_id }).second) continue; - if (m_writer->filament() == nullptr || m_writer->filament()->id() != brim0) + if (m_writer.filament() == nullptr || m_writer.filament()->id() != brim0) gcode += this->set_extruder(brim0, print_z); gcode += generate_object_brim(print, *obj, instance_id, first_layer, ltp.object_layer); } @@ -7386,7 +7386,7 @@ LayerResult GCode::process_layer( if (FILAMENT_CONFIG(retract_when_changing_layer)) { gcode += this->retract(false, false, auto_lift_type, true); } - m_writer->add_object_change_labels(gcode); + m_writer.add_object_change_labels(gcode); gcode += insert_timelapse_gcode(); has_insert_timelapse_gcode = true; } @@ -7408,7 +7408,7 @@ LayerResult GCode::process_layer( if (FILAMENT_CONFIG(retract_when_changing_layer)) { gcode += this->retract(false, false, auto_lift_type, true); } - m_writer->add_object_change_labels(gcode); + m_writer.add_object_change_labels(gcode); gcode += insert_timelapse_gcode(); } @@ -7420,7 +7420,7 @@ LayerResult GCode::process_layer( void GCode::apply_print_config(const PrintConfig &print_config) { - m_writer->apply_print_config(print_config); + m_writer.apply_print_config(print_config); m_config.apply(print_config); m_scaled_resolution = scaled(print_config.resolution.value); m_enable_exclude_object = m_config.exclude_object; @@ -7544,7 +7544,7 @@ void GCode::append_full_config(const Print &print, std::string &str) void GCode::set_extruders(const std::vector &extruder_ids) { - m_writer->set_extruders(extruder_ids); + m_writer.set_extruders(extruder_ids); // enable wipe path generation if any extruder has wipe enabled m_wipe.enable = false; @@ -7569,13 +7569,13 @@ void GCode::set_origin(const Vec2d &pointf) std::string GCode::preamble() { - std::string gcode = m_writer->preamble(); + std::string gcode = m_writer.preamble(); /* Perform a *silent* move to z_offset: we need this to initialize the Z position of our writer object so that any initial lift taking place before the first layer change will raise the extruder from the correct initial Z instead of 0. */ - m_writer->travel_to_z(m_config.z_offset.value); + m_writer.travel_to_z(m_config.z_offset.value); return gcode; } @@ -7586,28 +7586,28 @@ std::string GCode::change_layer(coordf_t print_z) std::string gcode; if (m_layer_count > 0) // Increment a progress bar indicator. - gcode += m_writer->update_progress(++ m_layer_index, m_layer_count); + gcode += m_writer.update_progress(++ m_layer_index, m_layer_count); //BBS coordf_t z = print_z + m_config.z_offset.value; // in unscaled coordinates - if (FILAMENT_CONFIG(retract_when_changing_layer) && m_writer->will_move_z(z)) { + if (FILAMENT_CONFIG(retract_when_changing_layer) && m_writer.will_move_z(z)) { LiftType lift_type = this->to_lift_type(ZHopType(FILAMENT_CONFIG(z_hop_types))); //BBS: force to use SpiralLift when change layer if lift type is auto gcode += this->retract(false, false, ZHopType(FILAMENT_CONFIG(z_hop_types)) == ZHopType::zhtAuto ? LiftType::SpiralLift : lift_type); } - m_writer->add_object_change_labels(gcode); + m_writer.add_object_change_labels(gcode); if (m_spiral_vase) { //BBS: force to normal lift immediately in spiral vase mode std::ostringstream comment; comment << "move to next layer (" << m_layer_index << ")"; - gcode += m_writer->travel_to_z(z, comment.str()); + gcode += m_writer.travel_to_z(z, comment.str()); } m_need_change_layer_lift_z = true; m_nominal_z = z; - m_writer->get_position().z() = z; + m_writer.get_position().z() = z; // forget last wiping path as wiping after raising Z is pointless // BBS. Dont forget wiping path to reduce stringing. @@ -7933,7 +7933,7 @@ std::string GCode::extrude_loop(const ExtrusionLoop& loop_ref, const Point3 ¢er3 = paths.front().polyline.points.front(); pt.rotate(angle, Point(center3.x(), center3.y())); // generate the travel move - gcode += m_writer->extrude_to_xy(this->point_to_gcode(pt), 0, "move inwards before travel", true); + gcode += m_writer.extrude_to_xy(this->point_to_gcode(pt), 0, "move inwards before travel", true); } return gcode; @@ -8295,7 +8295,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, bool slope_need_z_travel = false; if (sloped != nullptr && !sloped->is_flat()) { auto target_z = get_sloped_z(sloped->slope_begin.z_ratio); - slope_need_z_travel = m_writer->will_move_z(target_z); + slope_need_z_travel = m_writer.will_move_z(target_z); } // Move to first point of extrusion path // path is 2D. But in slope lift case, lift z is done in travel_to function. @@ -8323,14 +8323,14 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } if (path.z_contoured && !path.polyline.lines().empty()) { - double current_z = m_writer->get_position().z(); + double current_z = m_writer.get_position().z(); double first_z = unscale_(path.polyline.lines().begin()->a.z()) + m_nominal_z; if (GCodeFormatter::quantize_xyzf(first_z) != GCodeFormatter::quantize_xyzf(current_z)) { - gcode += m_writer->travel_to_z(first_z, "set Z for contouring", true); + gcode += m_writer.travel_to_z(first_z, "set Z for contouring", true); } } if (!path.z_contoured && sloped == nullptr) { - double current_z = m_writer->get_position().z(); + double current_z = m_writer.get_position().z(); if (GCodeFormatter::quantize_xyzf(current_z) != GCodeFormatter::quantize_xyzf(m_nominal_z)) { gcode += this->writer().travel_to_z(m_nominal_z, "reset Z after contouring", true); } @@ -8338,7 +8338,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // if needed, write the gcode_label_objects_end then gcode_label_objects_start // should be already done by travel_to, but just in case - m_writer->add_object_change_labels(gcode); + m_writer.add_object_change_labels(gcode); // compensate retraction gcode += this->unretract(); @@ -8356,12 +8356,12 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } else if (this->object_layer_over_raft() && m_config.first_layer_acceleration_over_raft.value > 0) { acceleration = m_config.first_layer_acceleration_over_raft.value; #endif - } else if (m_config.get_abs_value_at("bridge_acceleration", get_nozzle_config_index(m_writer->filament()->id())) > 0 && is_bridge(path.role())) { - acceleration = m_config.get_abs_value_at("bridge_acceleration", get_nozzle_config_index(m_writer->filament()->id())); - } else if (m_config.get_abs_value_at("sparse_infill_acceleration", get_nozzle_config_index(m_writer->filament()->id())) > 0 && (path.role() == erInternalInfill)) { - acceleration = m_config.get_abs_value_at("sparse_infill_acceleration", get_nozzle_config_index(m_writer->filament()->id())); - } else if (m_config.get_abs_value_at("internal_solid_infill_acceleration", get_nozzle_config_index(m_writer->filament()->id())) > 0 && (path.role() == erSolidInfill)) { - acceleration = m_config.get_abs_value_at("internal_solid_infill_acceleration", get_nozzle_config_index(m_writer->filament()->id())); + } else if (m_config.get_abs_value_at("bridge_acceleration", get_nozzle_config_index(m_writer.filament()->id())) > 0 && is_bridge(path.role())) { + acceleration = m_config.get_abs_value_at("bridge_acceleration", get_nozzle_config_index(m_writer.filament()->id())); + } else if (m_config.get_abs_value_at("sparse_infill_acceleration", get_nozzle_config_index(m_writer.filament()->id())) > 0 && (path.role() == erInternalInfill)) { + acceleration = m_config.get_abs_value_at("sparse_infill_acceleration", get_nozzle_config_index(m_writer.filament()->id())); + } else if (m_config.get_abs_value_at("internal_solid_infill_acceleration", get_nozzle_config_index(m_writer.filament()->id())) > 0 && (path.role() == erSolidInfill)) { + acceleration = m_config.get_abs_value_at("internal_solid_infill_acceleration", get_nozzle_config_index(m_writer.filament()->id())); } else if (NOZZLE_CONFIG(outer_wall_acceleration) > 0 && is_external_perimeter(path.role())) { acceleration = NOZZLE_CONFIG(outer_wall_acceleration); } else if (NOZZLE_CONFIG(inner_wall_acceleration) > 0 && is_internal_perimeter(path.role())) { @@ -8392,12 +8392,12 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } } - if (m_writer->get_gcode_flavor() == gcfKlipper) { - gcode += m_writer->set_accel_and_jerk(acceleration_i, jerk); + if (m_writer.get_gcode_flavor() == gcfKlipper) { + gcode += m_writer.set_accel_and_jerk(acceleration_i, jerk); } else { - gcode += m_writer->set_print_acceleration(acceleration_i); - gcode += m_writer->set_jerk_xy(jerk); + gcode += m_writer.set_print_acceleration(acceleration_i); + gcode += m_writer.set_jerk_xy(jerk); } // calculate effective extrusion length per distance unit (e_per_mm) @@ -8453,8 +8453,8 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } // Effective extrusion length per distance unit = (filament_flow_ratio/cross_section) * mm3_per_mm / print flow ratio - // m_writer->extruder()->e_per_mm3() below is (filament flow ratio / cross-sectional area) - double e_per_mm = m_writer->filament()->e_per_mm3() * _mm3_per_mm; + // m_writer.extruder()->e_per_mm3() below is (filament flow ratio / cross-sectional area) + double e_per_mm = m_writer.filament()->e_per_mm3() * _mm3_per_mm; e_per_mm /= filament_flow_ratio; // set speed @@ -8470,7 +8470,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, speed = std::min(speed, m_config.scarf_joint_speed.get_abs_value(speed)); } } else if(path.role() == erInternalBridgeInfill) { - speed = m_config.get_abs_value_at("internal_bridge_speed", get_nozzle_config_index(m_writer->filament()->id())); + speed = m_config.get_abs_value_at("internal_bridge_speed", get_nozzle_config_index(m_writer.filament()->id())); } else if (path.role() == erOverhangPerimeter || path.role() == erSupportTransition || path.role() == erBridgeInfill) { speed = NOZZLE_CONFIG(bridge_speed); } else if (path.role() == erInternalInfill) { @@ -8480,7 +8480,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } else if (path.role() == erTopSolidInfill) { speed = NOZZLE_CONFIG(top_surface_speed); } else if (path.role() == erIroning) { - const size_t filament_idx = get_filament_config_index(m_writer->filament()->id()); + const size_t filament_idx = get_filament_config_index(m_writer.filament()->id()); speed = m_config.filament_ironing_speed.is_nil(filament_idx) ? m_config.get_abs_value("ironing_speed") : m_config.filament_ironing_speed.get_at(filament_idx); @@ -8513,7 +8513,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // "first layers" the fan stays off for are a band along the belt. Mark where the // extrusion enters and leaves it, per segment, for the cooling buffer. const bool belt_band_tags = m_enable_cooling_markers && m_config.belt_printer.value; - const int belt_band_layers = belt_band_tags ? m_config.close_fan_the_first_x_layers.get_at(m_writer->filament()->id()) : 0; + const int belt_band_layers = belt_band_tags ? m_config.close_fan_the_first_x_layers.get_at(m_writer.filament()->id()) : 0; auto tag_belt_band = [this, &gcode, belt_band_tags, belt_band_layers, z = path_point_mm.z()](coord_t x, coord_t y) { if (! belt_band_tags) return; @@ -8713,7 +8713,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, //Orca: process custom gcode for extrusion role change if (path.role() != m_last_extrusion_role) { - const auto current_filament_id = m_writer->filament()->id(); + const auto current_filament_id = m_writer.filament()->id(); const std::string& machine_role_change_gcode = m_config.change_extrusion_role_gcode.value; const std::string& filament_role_change_gcode = m_config.filament_change_extrusion_role_gcode.get_at(current_filament_id); const std::string& process_role_change_gcode = m_config.process_change_extrusion_role_gcode.value; @@ -8788,7 +8788,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, if (m_multi_flow_segment_path_average_mm3_per_mm > 0) { sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer->filament()->id(), + m_writer.filament()->id(), m_multi_flow_segment_path_average_mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -8801,7 +8801,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // to issue a zero flow PA change command for this sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer->filament()->id(), + m_writer.filament()->id(), _mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -8900,7 +8900,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, return; // Inline M971 only when current extruder is the photo head; other nozzles may pass through nearby // coordinates but their physical position differs. - if (!m_writer->filament() || get_extruder_id(m_writer->filament()->id()) != m_farthest_point_timelapse.most_used_extruder) + if (!m_writer.filament() || get_extruder_id(m_writer.filament()->id()) != m_farthest_point_timelapse.most_used_extruder) return; // Compare in the global print frame to match farthest_gcode_pos, which is unscale(farthest_point) // without the per-extruder nozzle offset. Using point_to_gcode() here would subtract extruder_offset @@ -8939,7 +8939,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer->filament()->id(), + m_writer.filament()->id(), _mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -8954,7 +8954,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer->filament()->id(), + m_writer.filament()->id(), _mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -8969,7 +8969,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // ORCA: End of adaptive PA code segment } - gcode += m_writer->set_speed(F, "", comment); + gcode += m_writer.set_speed(F, "", comment); { if (m_enable_cooling_markers) { if (enable_overhang_bridge_fan) { @@ -8990,7 +8990,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, if (!m_config.enable_arc_fitting || path.polyline.fitting_result.empty() || m_config.spiral_mode || sloped != nullptr || path.z_contoured || this->should_disable_arc_fitting()) { double path_length = 0.; double total_length = sloped == nullptr ? 0. : path.polyline.length() * SCALING_FACTOR; - double saved_z = m_writer->get_position().z(); + double saved_z = m_writer.get_position().z(); for (const Line3& line : path.polyline.lines()) { std::string tempDescription = description; @@ -9024,12 +9024,12 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, if (z < 0.1) { throw RuntimeError("GCode: very low z"); } - gcode += m_writer->extrude_to_xyz(Vec3d(dest2d.x(), dest2d.y(), z), e, + gcode += m_writer.extrude_to_xyz(Vec3d(dest2d.x(), dest2d.y(), z), e, GCodeWriter::full_gcode_comment ? tempDescription : ""); } else if (sloped == nullptr) { // Normal extrusion - gcode += m_writer->extrude_to_xy( + gcode += m_writer.extrude_to_xy( this->point_to_gcode(line.b.to_point()), dE, GCodeWriter::full_gcode_comment ? tempDescription : "", path.is_force_no_extrusion()); @@ -9038,7 +9038,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, const auto [z_ratio, e_ratio] = sloped->interpolate(path_length / total_length); Vec2d dest2d = this->point_to_gcode(line.b.to_point()); Vec3d dest3d(dest2d(0), dest2d(1), get_sloped_z(z_ratio)); - gcode += m_writer->extrude_to_xyz( + gcode += m_writer.extrude_to_xyz( dest3d, dE * e_ratio, GCodeWriter::full_gcode_comment ? tempDescription : "", path.is_force_no_extrusion()); @@ -9069,7 +9069,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, tempDescription += Slic3r::format(" | Old Flow Value: %0.5f Length: %0.5f",oldE, line_length); } } - gcode += m_writer->extrude_to_xy( + gcode += m_writer.extrude_to_xy( this->point_to_gcode(line.b), dE, GCodeWriter::full_gcode_comment ? tempDescription : "", path.is_force_no_extrusion()); @@ -9093,7 +9093,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, tempDescription += Slic3r::format(" | Old Flow Value: %0.5f Length: %0.5f",oldE, arc_length); } } - gcode += m_writer->extrude_arc_to_xy( + gcode += m_writer.extrude_arc_to_xy( this->point_to_gcode(arc.end_point), center_offset, dE, @@ -9122,7 +9122,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, Polyline3 l(p); total_length = l.length() * SCALING_FACTOR; } - gcode += m_writer->set_speed(last_set_speed, "", comment); + gcode += m_writer.set_speed(last_set_speed, "", comment); Vec3d prev = this->point_to_gcode_quantized(new_points[0].p); bool pre_fan_enabled = false; bool cur_fan_enabled = false; @@ -9173,7 +9173,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer->filament()->id(), + m_writer.filament()->id(), _mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -9188,7 +9188,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer->filament()->id(), + m_writer.filament()->id(), _mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -9205,10 +9205,10 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // Ignore small speed variations - emit speed change if the delta between current and new is greater than 60mm/min / 1mm/sec // Reset speed to F if delta to F is less than 1mm/sec if ((std::abs(last_set_speed - new_speed) > 60)) { - gcode += m_writer->set_speed(new_speed, "", comment); + gcode += m_writer.set_speed(new_speed, "", comment); last_set_speed = new_speed; } else if ((std::abs(F - new_speed) <= 60)) { - gcode += m_writer->set_speed(F, "", comment); + gcode += m_writer.set_speed(F, "", comment); last_set_speed = F; } auto dE = e_per_mm * line_length; @@ -9235,16 +9235,16 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, if (z < 0.1) { throw RuntimeError("GCode: very low z"); } - gcode += m_writer->extrude_to_xyz(Vec3d(dest2d.x(), dest2d.y(), z), e, + gcode += m_writer.extrude_to_xyz(Vec3d(dest2d.x(), dest2d.y(), z), e, GCodeWriter::full_gcode_comment ? tempDescription : ""); } else if (sloped == nullptr) { // Normal extrusion - gcode += m_writer->extrude_to_xy(p.head<2>(), dE, GCodeWriter::full_gcode_comment ? tempDescription : ""); + gcode += m_writer.extrude_to_xy(p.head<2>(), dE, GCodeWriter::full_gcode_comment ? tempDescription : ""); } else { // Sloped extrusion const auto [z_ratio, e_ratio] = sloped->interpolate(path_length / total_length); Vec3d dest3d(p(0), p(1), get_sloped_z(z_ratio)); - gcode += m_writer->extrude_to_xyz(dest3d, dE * e_ratio, GCodeWriter::full_gcode_comment ? tempDescription : ""); + gcode += m_writer.extrude_to_xyz(dest3d, dE * e_ratio, GCodeWriter::full_gcode_comment ? tempDescription : ""); } // Inline farthest-point snapshot on the variable-speed emission path. Inert unless the @@ -9384,8 +9384,8 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string unsigned int acceleration_to_set = 0; if (this->on_first_layer()) { - unsigned int initial_layer_travel_acceleration = m_config.get_abs_value_at("initial_layer_travel_acceleration", get_nozzle_config_index(m_writer->filament()->id())); - double initial_layer_travel_jerk = m_config.get_abs_value_at("initial_layer_travel_jerk", get_nozzle_config_index(m_writer->filament()->id())); + unsigned int initial_layer_travel_acceleration = m_config.get_abs_value_at("initial_layer_travel_acceleration", get_nozzle_config_index(m_writer.filament()->id())); + double initial_layer_travel_jerk = m_config.get_abs_value_at("initial_layer_travel_jerk", get_nozzle_config_index(m_writer.filament()->id())); if (NOZZLE_CONFIG(default_acceleration) > 0 && initial_layer_travel_acceleration > 0) { acceleration_to_set = (unsigned int) floor(initial_layer_travel_acceleration + 0.5); @@ -9398,7 +9398,7 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string if (NOZZLE_CONFIG(default_acceleration) > 0) { if (role == erOverhangPerimeter && is_short_travel) { - const double bridge_acceleration = m_config.get_abs_value_at("bridge_acceleration", get_nozzle_config_index(m_writer->filament()->id())); + const double bridge_acceleration = m_config.get_abs_value_at("bridge_acceleration", get_nozzle_config_index(m_writer.filament()->id())); if (bridge_acceleration > 0) acceleration_to_set = (unsigned int) floor(bridge_acceleration + 0.5); @@ -9422,11 +9422,11 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string } } - if (m_writer->get_gcode_flavor() == gcfKlipper) { - gcode += m_writer->set_accel_and_jerk(acceleration_to_set, jerk_to_set); + if (m_writer.get_gcode_flavor() == gcfKlipper) { + gcode += m_writer.set_accel_and_jerk(acceleration_to_set, jerk_to_set); } else { - gcode += m_writer->set_travel_acceleration(acceleration_to_set); - gcode += m_writer->set_jerk_xy(jerk_to_set); + gcode += m_writer.set_travel_acceleration(acceleration_to_set); + gcode += m_writer.set_jerk_xy(jerk_to_set); } // if a retraction would be needed, try to use reduce_crossing_wall to plan a @@ -9434,7 +9434,7 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string if (m_config.reduce_crossing_wall && !m_avoid_crossing_perimeters.disabled_once() && m_layer != nullptr // A brim apron layer has no Layer to avoid crossing - && m_writer->is_current_position_clear()) + && m_writer.is_current_position_clear()) //BBS: don't generate detour travel paths when current position is unclea { travel = m_avoid_crossing_perimeters.travel_to(*this, point, &could_be_wipe_disabled); @@ -9482,7 +9482,7 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string // if needed, write the gcode_label_objects_end then gcode_label_objects_start - m_writer->add_object_change_labels(gcode); + m_writer.add_object_change_labels(gcode); // use G1 because we rely on paths being straight (G0 may make round paths) if (travel.size() >= 2) { @@ -9490,14 +9490,14 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string if (false/*m_spiral_vase*/) { // No lazy z lift for spiral vase mode for (size_t i = 1; i < travel.size(); ++i) { - gcode += m_writer->travel_to_xy(this->point_to_gcode(travel.points[i]), comment); + gcode += m_writer.travel_to_xy(this->point_to_gcode(travel.points[i]), comment); } } else { if (travel.size() == 2) { // No extra movements emitted by avoid_crossing_perimeters, simply move to the end point with z change const auto& dest2d = this->point_to_gcode(travel.points.back()); Vec3d dest3d(dest2d(0), dest2d(1), z == DBL_MAX ? m_nominal_z : z); - gcode += m_writer->travel_to_xyz(dest3d, comment, m_need_change_layer_lift_z); + gcode += m_writer.travel_to_xyz(dest3d, comment, m_need_change_layer_lift_z); m_need_change_layer_lift_z = false; } else { // Extra movements emitted by avoid_crossing_perimeters, lift the z to normal height at the beginning, then apply the z @@ -9507,16 +9507,16 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string // Lift to normal z at beginning Vec2d dest2d = this->point_to_gcode(travel.points[i]); Vec3d dest3d(dest2d(0), dest2d(1), m_nominal_z); - gcode += m_writer->travel_to_xyz(dest3d, comment, m_need_change_layer_lift_z); + gcode += m_writer.travel_to_xyz(dest3d, comment, m_need_change_layer_lift_z); m_need_change_layer_lift_z = false; } else if (z != DBL_MAX && i == travel.size() - 1) { // Apply z_ratio for the very last point Vec2d dest2d = this->point_to_gcode(travel.points[i]); Vec3d dest3d(dest2d(0), dest2d(1), z); - gcode += m_writer->travel_to_xyz(dest3d, comment); + gcode += m_writer.travel_to_xyz(dest3d, comment); } else { // For all points in between, no z change - gcode += m_writer->travel_to_xy(this->point_to_gcode(travel.points[i]), comment); + gcode += m_writer.travel_to_xy(this->point_to_gcode(travel.points[i]), comment); } } } @@ -9619,7 +9619,7 @@ bool GCode::needs_retraction(const Polyline &travel, ExtrusionRole role, LiftTyp if (clipped_travel.length() > travel_len_thresh) clipped_travel.points.back() = clipped_travel.points.front()+(clipped_travel.points.back() - clipped_travel.points.front()) * (travel_len_thresh / clipped_travel.length()); //BBS: translate to current plate coordinate system - clipped_travel.translate(Point::new_scale(double(m_origin.x() - m_writer->get_xy_offset().x()), double(m_origin.y() - m_writer->get_xy_offset().y()))); + clipped_travel.translate(Point::new_scale(double(m_origin.x() - m_writer.get_xy_offset().x()), double(m_origin.y() - m_writer.get_xy_offset().y()))); //BBS: force to retract when leave from external perimeter for a long travel //Better way is judging whether the travel move direction is same with last extrusion move. @@ -9671,18 +9671,18 @@ std::string GCode::retract(bool toolchange, bool is_last_retraction, LiftType li { std::string gcode; - if (m_writer->filament() == nullptr) + if (m_writer.filament() == nullptr) return gcode; // wipe (if it's enabled for this extruder and we have a stored wipe path and no-zero wipe distance) if (FILAMENT_CONFIG(wipe) && m_wipe.has_path() && scale_(FILAMENT_CONFIG(wipe_distance)) > SCALED_EPSILON) { Wipe::RetractionValues wipeRetractions = m_wipe.calculateWipeRetractionLengths(*this, toolchange); - gcode += toolchange ? m_writer->retract_for_toolchange(true, wipeRetractions.retraction_length_before_wipe) : - m_writer->retract(true, wipeRetractions.retraction_length_before_wipe); + gcode += toolchange ? m_writer.retract_for_toolchange(true, wipeRetractions.retraction_length_before_wipe) : + m_writer.retract(true, wipeRetractions.retraction_length_before_wipe); gcode += m_wipe.wipe(*this, wipeRetractions.retraction_length_during_wipe, toolchange, is_last_retraction); // Orca: wipeRetractions.retraction_length_after_wipe is not being used explicitly, - // the remaining retraction after wipe is handled by the subsequent m_writer->retract() call + // the remaining retraction after wipe is handled by the subsequent m_writer.retract() call } /* The parent class will decide whether we need to perform an actual retraction @@ -9691,14 +9691,14 @@ std::string GCode::retract(bool toolchange, bool is_last_retraction, LiftType li length is honored in case wipe path was too short. */ if ((!this->on_first_layer() || this->config().bottom_surface_pattern != InfillPattern::ipHilbertCurve) && (role != erTopSolidInfill || this->config().top_surface_pattern != InfillPattern::ipHilbertCurve)) - gcode += toolchange ? m_writer->retract_for_toolchange() : m_writer->retract(); + gcode += toolchange ? m_writer.retract_for_toolchange() : m_writer.retract(); - gcode += m_writer->reset_e(); + gcode += m_writer.reset_e(); // Orca: check if should + can lift (roughly from SuperSlicer) RetractLiftEnforceType retract_lift_type = RetractLiftEnforceType(EXTRUDER_CONFIG(retract_lift_enforce)); bool needs_lift = toolchange - || m_writer->filament()->retraction_length() > 0 + || m_writer.filament()->retraction_length() > 0 || m_config.use_firmware_retraction; bool last_fill_extrusion_role_top_infill = (this->m_last_notgapfill_extrusion_role == ExtrusionRole::erTopSolidInfill || this->m_last_notgapfill_extrusion_role == ExtrusionRole::erIroning); @@ -9721,9 +9721,9 @@ std::string GCode::retract(bool toolchange, bool is_last_retraction, LiftType li if (needs_lift && can_lift) { if (apply_instantly) - gcode += m_writer->eager_lift(lift_type); + gcode += m_writer.eager_lift(lift_type); else - gcode += m_writer->lazy_lift(lift_type, m_spiral_vase != nullptr); + gcode += m_writer.lazy_lift(lift_type, m_spiral_vase != nullptr); } return gcode; @@ -9744,9 +9744,9 @@ void GCode::update_layer_related_config(int layer_id){ m_config.filament_volume_map.values = volume_map; m_config.filament_nozzle_map.values = nozzle_map; - m_writer->config.filament_map.values = extruder_map; - m_writer->config.filament_volume_map.values = volume_map; - m_writer->config.filament_nozzle_map.values = nozzle_map; + m_writer.config.filament_map.values = extruder_map; + m_writer.config.filament_volume_map.values = volume_map; + m_writer.config.filament_nozzle_map.values = nozzle_map; } @@ -9813,11 +9813,11 @@ void GCode::update_placeholder_parser_with_variant_params() std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bool by_object, int toolchange_temp_override, bool defer_temp_wait) { int new_extruder_id = get_extruder_id(new_filament_id); - if (!m_writer->need_toolchange(new_filament_id)) + if (!m_writer.need_toolchange(new_filament_id)) return ""; // if we are running a single-extruder setup, just set the extruder and return nothing - if (!m_writer->multiple_extruders) { + if (!m_writer.multiple_extruders) { this->placeholder_parser().set("current_extruder", new_filament_id); // Orca: keep the global current-tool identity coherent even on the single-extruder path (see append_tcr). this->placeholder_parser().set("current_filament_id", (int) new_filament_id); @@ -9851,14 +9851,14 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo check_add_eol(gcode); } if (m_config.enable_pressure_advance.get_at(new_filament_id)) { - gcode += m_writer->set_pressure_advance(m_config.pressure_advance.get_at(new_filament_id)); + gcode += m_writer.set_pressure_advance(m_config.pressure_advance.get_at(new_filament_id)); // Orca: Adaptive PA // Reset Adaptive PA processor last PA value m_pa_processor->resetPreviousPA(m_config.pressure_advance.get_at(new_filament_id)); } - gcode += m_writer->toolchange(new_filament_id, new_extruder_id); - if (Extruder *fil = m_writer->filament()) + gcode += m_writer.toolchange(new_filament_id, new_extruder_id); + if (Extruder *fil = m_writer.filament()) fil->set_config_index((int)get_filament_config_index((int)fil->id())); return gcode; } @@ -9874,18 +9874,18 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo // BBS: insert skip object label before change filament while by object if (by_object) - m_writer->add_object_change_labels(gcode); + m_writer.add_object_change_labels(gcode); bool add_change_filament_624 = false; - if (m_writer->filament() != nullptr) { + if (m_writer.filament() != nullptr) { // Process the custom filament_end_gcode. set_extruder() is only called if there is no wipe tower // so it should not be injected twice. - unsigned int old_filament_id = m_writer->filament()->id(); + unsigned int old_filament_id = m_writer.filament()->id(); const std::string &filament_end_gcode = m_config.filament_end_gcode.get_at(old_filament_id); if (! filament_end_gcode.empty()) { DynamicConfig config; config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index)); - config.set_key_value("layer_z", new ConfigOptionFloat(m_writer->get_position().z() - m_config.z_offset.value)); + config.set_key_value("layer_z", new ConfigOptionFloat(m_writer.get_position().z() - m_config.z_offset.value)); config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); config.set_key_value("filament_extruder_id", new ConfigOptionInt(int(get_extruder_id(old_filament_id)))); if (!m_filament_instances_code.empty()) { @@ -9901,7 +9901,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo // If ooze prevention is enabled, park current extruder in the nearest // standby point and set it to the standby temperature. - if (m_ooze_prevention.enable && m_writer->filament() != nullptr) + if (m_ooze_prevention.enable && m_writer.filament() != nullptr) gcode += m_ooze_prevention.pre_toolchange(*this); // BBS @@ -9932,10 +9932,10 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo if (toolchange_temp_override <= 0 && (m_layer == nullptr || preheat_temp <= 0)) preheat_temp = m_config.nozzle_temperature_initial_layer.get_at(new_fi); if (preheat_temp > 0) - gcode += m_writer->set_temperature(preheat_temp, false, new_filament_id); + gcode += m_writer.set_temperature(preheat_temp, false, new_filament_id); } - Vec3d nozzle_pos = m_writer->get_position(); + Vec3d nozzle_pos = m_writer.get_position(); float old_retract_length, old_retract_length_toolchange, wipe_volume; int old_filament_temp, old_filament_e_feedrate; @@ -9943,16 +9943,16 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo //BBS: add handling for filament change in start gcode int old_filament_id = -1; int old_extruder_id = -1; - if (m_writer->filament() != nullptr || m_start_gcode_filament != -1) { + if (m_writer.filament() != nullptr || m_start_gcode_filament != -1) { std::vector flush_matrix(cast(get_flush_volumes_matrix(m_config.flush_volumes_matrix.values, new_extruder_id, m_config.nozzle_diameter.values.size()))); const unsigned int number_of_extruders = (unsigned int) (m_config.filament_colour.values.size()); // if is multi_extruder only use the fist extruder matrix - if (m_writer->filament() != nullptr) - assert(m_writer->filament()->id() < number_of_extruders); + if (m_writer.filament() != nullptr) + assert(m_writer.filament()->id() < number_of_extruders); else assert(m_start_gcode_filament < number_of_extruders); - old_filament_id = m_writer->filament() != nullptr ? m_writer->filament()->id() : m_start_gcode_filament; - old_extruder_id = m_writer->filament() != nullptr ? m_writer->filament()->extruder_id() : get_extruder_id(m_start_gcode_filament); + old_filament_id = m_writer.filament() != nullptr ? m_writer.filament()->id() : m_start_gcode_filament; + old_extruder_id = m_writer.filament() != nullptr ? m_writer.filament()->extruder_id() : get_extruder_id(m_start_gcode_filament); // Resolving the old filament at the current layer is safe: the per-layer nozzle maps are // gap-filled carry-forward, so its current-layer column matches the nozzle it occupies. @@ -9965,7 +9965,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo float grab_purge_volume = m_config.grab_length.get_at(new_extruder_id) * 2.4; if (old_extruder_id != new_extruder_id) { //calc flush volume between the same extruder id - int old_filament_id_in_new_extruder = m_writer->filament(new_extruder_id) != nullptr ? m_writer->filament(new_extruder_id)->id() : -1; + int old_filament_id_in_new_extruder = m_writer.filament(new_extruder_id) != nullptr ? m_writer.filament(new_extruder_id)->id() : -1; if (old_filament_id_in_new_extruder == -1) wipe_volume = 0; else { @@ -10030,7 +10030,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo (old_filament_id != -1) ? (float) m_config.filament_retract_length_nc.get_at(get_filament_config_index(old_filament_id)) : 0.f)); // Current parked-retract length of the incoming filament's extruder. dyn_config.set_key_value("new_extruder_retracted_length", - new ConfigOptionFloat(m_writer->get_extruder_retracted_length((int) new_filament_id))); + new ConfigOptionFloat(m_writer.get_extruder_retracted_length((int) new_filament_id))); dyn_config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index)); dyn_config.set_key_value("layer_z", new ConfigOptionFloat(print_z)); dyn_config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); @@ -10149,13 +10149,13 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo //BBS: gcode writer doesn't know where the extruder is and whether fan speed is changed after inserting tool change gcode //Set this flag so that normal lift will be used the first time after tool change. gcode += ";_FORCE_RESUME_FAN_SPEED\n"; - m_writer->set_current_position_clear(false); + m_writer.set_current_position_clear(false); //BBS: check whether custom gcode changes the z position. Update if changed double temp_z_after_tool_change; if (GCodeProcessor::get_last_z_from_gcode(toolchange_gcode_parsed, temp_z_after_tool_change)) { - Vec3d pos = m_writer->get_position(); + Vec3d pos = m_writer.get_position(); pos(2) = temp_z_after_tool_change; - m_writer->set_position(pos); + m_writer.set_position(pos); } } } @@ -10163,15 +10163,15 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo // BBS. Reset old extruder E-value. // Keep retract length because Custom GCode will guarantee retract length be the same as toolchange if (m_config.single_extruder_multi_material) { - m_writer->reset_e(); + m_writer.reset_e(); } //BBS: don't add T[next extruder] if there is no T cmd on filament change //We inform the writer about what is happening, but we may not use the resulting gcode. - std::string toolchange_command = m_writer->toolchange(new_filament_id, next_nozzle_id); - if (Extruder *fil = m_writer->filament()) + std::string toolchange_command = m_writer.toolchange(new_filament_id, next_nozzle_id); + if (Extruder *fil = m_writer.filament()) fil->set_config_index((int)get_filament_config_index((int)fil->id())); - if (!custom_gcode_changes_tool(toolchange_gcode_parsed, m_writer->toolchange_prefix(), new_filament_id)) + if (!custom_gcode_changes_tool(toolchange_gcode_parsed, m_writer.toolchange_prefix(), new_filament_id)) gcode += toolchange_command; else { // user provided his own toolchange gcode, no need to do anything @@ -10183,7 +10183,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo int temp = (m_layer_index <= 0 ? m_config.nozzle_temperature_initial_layer.get_at(new_fi) : m_config.nozzle_temperature.get_at(new_fi)); - gcode += m_writer->set_temperature(temp, false); + gcode += m_writer.set_temperature(temp, false); } this->placeholder_parser().set("current_extruder", new_filament_id); @@ -10245,7 +10245,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo gcode += m_ooze_prevention.post_toolchange(*this); if (m_config.enable_pressure_advance.get_at(new_filament_id)) { - gcode += m_writer->set_pressure_advance(m_config.pressure_advance.get_at(new_filament_id)); + gcode += m_writer.set_pressure_advance(m_config.pressure_advance.get_at(new_filament_id)); // Orca: Adaptive PA // Reset Adaptive PA processor last PA value m_pa_processor->resetPreviousPA(m_config.pressure_advance.get_at(new_filament_id)); @@ -10386,7 +10386,7 @@ Vec3d GCode::point_to_gcode(const Point3& point) const Point GCode::gcode_to_point(const Vec2d &point) const { Vec2d pt = point - m_origin; - if (const Extruder *extruder = m_writer->filament(); extruder) + if (const Extruder *extruder = m_writer.filament(); extruder) // This function may be called at the very start from toolchange G-code when the extruder is not assigned yet. pt += m_config.extruder_offset.get_at(extruder->extruder_id()); return scaled(pt); diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 17221101fd..80b95d2937 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -218,8 +218,7 @@ public: m_last_obj_copy(nullptr, Point(std::numeric_limits::max(), std::numeric_limits::max())), // BBS m_toolchange_count(0), - m_nominal_z(0.), - m_writer(std::make_unique()) + m_nominal_z(0.) {} virtual ~GCode() = default; @@ -229,7 +228,7 @@ public: void do_export(Print* print, const char* path, GCodeProcessorResult* result = nullptr, ThumbnailsGeneratorCallback thumbnail_cb = nullptr); void export_layer_filaments(GCodeProcessorResult* result); //BBS: set offset for gcode writer - void set_gcode_offset(double x, double y) { m_gcode_offset = Vec2d(x, y); m_writer->set_xy_offset(x, y); m_processor.set_xy_offset(x, y);} + void set_gcode_offset(double x, double y) { m_writer.set_xy_offset(x, y); m_processor.set_xy_offset(x, y);} // Exported for the helper classes (OozePrevention, Wipe) and for the Perl binding for unit tests. const Vec2d& origin() const { return m_origin; } @@ -243,8 +242,8 @@ public: Vec3d point_to_gcode_quantized(const Point3& point) const; const FullPrintConfig &config() const { return m_config; } const Layer* layer() const { return m_layer; } - GCodeWriter& writer() { return *m_writer; } - const GCodeWriter& writer() const { return *m_writer; } + GCodeWriter& writer() { return m_writer; } + const GCodeWriter& writer() const { return m_writer; } PlaceholderParser& placeholder_parser() { return m_placeholder_parser_integration.parser; } const PlaceholderParser& placeholder_parser() const { return m_placeholder_parser_integration.parser; } // Process a template through the placeholder parser, collect error messages to be reported @@ -267,7 +266,7 @@ public: bool needs_retraction(const Polyline& travel, ExtrusionRole role, LiftType& lift_type); std::string retract(bool toolchange = false, bool is_last_retraction = false, LiftType lift_type = LiftType::NormalLift, bool apply_instantly = false, ExtrusionRole role = erNone); // extra_retract forwards a PETG pre-extrusion over-extrusion; default 0 -> identical to the plain deretract. - std::string unretract(float extra_retract = 0.f) { return m_writer->unlift() + m_writer->unretract(extra_retract); } + std::string unretract(float extra_retract = 0.f) { return m_writer.unlift() + m_writer.unretract(extra_retract); } std::string set_extruder(unsigned int extruder_id, double print_z, bool by_object=false, int toolchange_temp_override = -1, bool defer_temp_wait = false); bool is_BBL_Printer(); WipeTowerType wipe_tower_type(); @@ -384,7 +383,7 @@ protected: // represent a G2/G3 arc. Belt printers get this through BeltKinematics // rather than through an override of their own. virtual bool should_disable_arc_fitting() const - { return ! m_writer->kinematics().supports_arc_moves(); } + { return ! m_writer.kinematics().supports_arc_moves(); } void _do_export(Print &print, GCodeOutputStream &file, ThumbnailsGeneratorCallback thumbnail_cb); @@ -660,7 +659,7 @@ protected: DynamicConfig m_calib_config; // scaled G-code resolution double m_scaled_resolution; - std::unique_ptr m_writer; + GCodeWriter m_writer; struct PlaceholderParserIntegration { void reset(); @@ -786,7 +785,6 @@ protected: // checks short-circuit to the legacy Layer::id() == 0 path. std::unique_ptr m_first_layer_plane; // Plate origin, kept so a writer replaced during export can be given it again. - Vec2d m_gcode_offset{ Vec2d::Zero() }; std::unique_ptr m_pressure_equalizer; diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index 20357a1d9e..526ef0898d 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -17,7 +17,6 @@ class FirstLayerPlane; class GCodeWriter { public: - virtual ~GCodeWriter() = default; GCodeConfig config; bool multiple_extruders; @@ -84,27 +83,27 @@ public: std::string set_speed(double F, const std::string &comment = std::string(), const std::string &cooling_marker = std::string()); // SoftFever NOTE: the returned speed is mm/minute double get_current_speed() const { return m_current_speed;} - virtual std::string travel_to_xy(const Vec2d &point, const std::string &comment = std::string()); - virtual std::string travel_to_xyz(const Vec3d &point, const std::string &comment = std::string(), bool force_z = false); + std::string travel_to_xy(const Vec2d &point, const std::string &comment = std::string()); + std::string travel_to_xyz(const Vec3d &point, const std::string &comment = std::string(), bool force_z = false); std::string travel_to_z(double z, const std::string &comment = std::string(), bool force = false); bool will_move_z(double z) const; - virtual std::string extrude_to_xy(const Vec2d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false); + std::string extrude_to_xy(const Vec2d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false); //BBS: generate G2 or G3 extrude which moves by arc std::string extrude_arc_to_xy(const Vec2d &point, const Vec2d ¢er_offset, double dE, const bool is_ccw, const std::string &comment = std::string(), bool force_no_extrusion = false); // Linear approximation of an arc, used when the machine mapping cannot // express a G2/G3. Must be called before m_pos is updated: center_offset is // relative to the current position. std::string extrude_arc_as_polyline(const Vec2d &point, const Vec2d ¢er_offset, double dE, const bool is_ccw, const std::string &comment = std::string(), bool force_no_extrusion = false); - virtual std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false); + std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false); std::string retract(bool before_wipe = false, double retract_length = 0); std::string retract_for_toolchange(bool before_wipe = false, double retract_length = 0); // extra_retract adds a small over-extrusion to the deretract move (PETG pre-extrusion). // Default 0 -> byte-identical to the plain deretract. std::string unretract(float extra_retract = 0.f); // do lift instantly - virtual std::string eager_lift(const LiftType type); + std::string eager_lift(const LiftType type); // record a lift request, do realy lift in next travel - virtual std::string lazy_lift(LiftType lift_type = LiftType::NormalLift, bool spiral_vase = false); + std::string lazy_lift(LiftType lift_type = LiftType::NormalLift, bool spiral_vase = false); std::string unlift(); const Vec3d& get_position() const { return m_pos; } Vec3d& get_position() { return m_pos; } @@ -184,7 +183,7 @@ protected: bool m_is_current_pos_clear = false; double m_current_speed; - virtual std::string _travel_to_z(double z, const std::string &comment); + std::string _travel_to_z(double z, const std::string &comment); // Whether a destination gets first-layer treatment. With an active plane // evaluator, distance from the plane decides; otherwise the layer-coarse From e8a499df7bc87bd91e82df5e5f358b99cc614567 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sun, 4 Oct 2026 19:21:22 -0500 Subject: [PATCH 182/224] Belt: register the raw G-code toggle as a Preview shortcut Main's assignable shortcuts replaced the canvas key switch that carried the belt "show designed / show raw G-code" toggle on B. Register it as ToggleBeltRawGcode, bound to B in the Preview (B is only taken on the Plater, by the mesh boolean gizmo), so it can be rebound and is listed in the shortcuts dialog. The legend checkbox shows whatever key is bound. --- src/slic3r/GUI/GCodeViewer.cpp | 7 +++++-- src/slic3r/GUI/GLCanvas3D.cpp | 12 +++++++++++- src/slic3r/GUI/Shortcuts.cpp | 1 + src/slic3r/GUI/Shortcuts.hpp | 2 +- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 5e21b6c617..26c3e2e111 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -13,6 +13,7 @@ #include "libslic3r/Geometry/ConvexHull.hpp" #include "GUI_App.hpp" +#include "Shortcuts.hpp" #include "MainFrame.hpp" #include "Plater.hpp" #include "Camera.hpp" @@ -5148,8 +5149,10 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv // 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; - if (ImGui::Checkbox(_u8L("Show raw G-code (belt only) [B]").c_str(), &show_raw)) { + 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 diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 55a3728c2e..4ec3982e52 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -3804,6 +3804,16 @@ 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; @@ -10196,7 +10206,7 @@ void GLCanvas3D::_render_canvas_toolbar() }; // Belt printers, G-code preview only: toggle the designed (upright) view vs the raw - // machine-frame G-code. Same state as hotkey B and the legend checkbox; the reload is + // 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)")), diff --git a/src/slic3r/GUI/Shortcuts.cpp b/src/slic3r/GUI/Shortcuts.cpp index 8773c6f364..2395d31778 100644 --- a/src/slic3r/GUI/Shortcuts.cpp +++ b/src/slic3r/GUI/Shortcuts.cpp @@ -149,6 +149,7 @@ constexpr std::array 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), diff --git a/src/slic3r/GUI/Shortcuts.hpp b/src/slic3r/GUI/Shortcuts.hpp index 0e61554e2f..6a58d64971 100644 --- a/src/slic3r/GUI/Shortcuts.hpp +++ b/src/slic3r/GUI/Shortcuts.hpp @@ -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, + ShowLabels, ShowWireframe, ToggleGcodeWindow, ToggleOneLayerMode, ToggleBeltRawGcode, // Application Preferences, Search, SwitchView, CollapseSidebar, ReloadDevicePage, KeyboardShortcuts, // Speed Dial From 1b4153f0cded628d02ab5442c0bf9dc675ae428c Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sun, 4 Oct 2026 19:22:42 -0500 Subject: [PATCH 183/224] Precise Seam: slice modifiers in the belt slicing frame slice_single_volume_regions() sliced Precise Seam modifiers with trafo_centered(), but a belt printer slices its layers with trafo_sliced(): the belt rotation, any pre-slice remap and the lift off the plate on top. On a belt print the modifier regions landed in the unrotated frame, away from the walls they were meant to place the seam on. Slice them with trafo_sliced(), as the support volumes and the seam enforcers already are. It equals trafo_centered() off a belt printer. --- src/libslic3r/PrintObjectSlice.cpp | 5 ++-- tests/fff_print/test_precise_seam.cpp | 42 +++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 6c2c9f9fd9..88f44466ff 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -1832,10 +1832,11 @@ std::vector PrintObject::slice_single_volume_regions(const ModelVolu { if (volume == nullptr) return {}; - // Match the existing slicing heights and centered transform without flattening holes. + // Match the existing slicing heights and the frame the layers were sliced in (belt + // pre-slice transforms included) without flattening holes. const std::vector zs = zs_from_layers(this->layers()); MeshSlicingParamsEx params; - params.trafo = this->trafo_centered(); + params.trafo = this->trafo_sliced(); const Print *print = this->print(); return slice_volume(*volume, zs, params, [print]() { print->throw_if_canceled(); }); } diff --git a/tests/fff_print/test_precise_seam.cpp b/tests/fff_print/test_precise_seam.cpp index 2f48c626cc..6d28b57b6f 100644 --- a/tests/fff_print/test_precise_seam.cpp +++ b/tests/fff_print/test_precise_seam.cpp @@ -1426,3 +1426,45 @@ TEST_CASE("Full containment warns for Blocked but not for Enforced", "[PreciseSe else CHECK(fixture.warning().empty()); } + +TEST_CASE("Belt printers slice Precise Seam modifiers in the frame the object was sliced in", "[PreciseSeam][belt]") +{ + Model model; + Print print; + Test::init_print({Test::cube(20)}, print, model, { + { "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 }, + // Keep the first layer's islands the raw slice, like the modifier's. + { "elefant_foot_compensation", 0 }, + }); + print.process(); + const PrintObject *object = print.get_object(0); + REQUIRE(object->layer_count() > 0); + + // A modifier with the object's own mesh and placement must slice to the object's own islands + // on every layer. Sliced without the belt rotation it would give 20 mm squares on the lower + // layers and nothing above 20 mm, while the tilted cube reaches about 28 mm. + Model modifiers; + ModelVolume *modifier = modifiers.add_object()->add_volume(*model.objects.front()->volumes.front()); + modifier->set_type(ModelVolumeType::PRECISE_SEAM_BLOCKED); + const std::vector slices = object->slice_single_volume_regions(modifier); + REQUIRE(slices.size() == object->layer_count()); + + const double tolerance = scale_(0.05) * scale_(20.); + for (size_t i = 0; i < slices.size(); ++i) { + CAPTURE(i, object->get_layer(int(i))->slice_z); + const ExPolygons &islands = object->get_layer(int(i))->lslices; + double object_area = 0., modifier_area = 0.; + for (const ExPolygon &island : islands) + object_area += island.area(); + for (const ExPolygon ®ion : slices[i]) + modifier_area += region.area(); + CHECK(std::abs(modifier_area - object_area) < tolerance); + CHECK(get_extents(slices[i]).inflated(scale_(0.05)).contains(get_extents(islands))); + } +} From 56f5ff8128df7a83ba2a947d4c8b71f0c3037d9a Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sun, 4 Oct 2026 19:28:36 -0500 Subject: [PATCH 184/224] Belt profiles: inherit what they repeat and pass main's profile checks main's profile checks reject the belt bundles: their filaments override variant keys with one value under the library's six-variant presets, the copied vendor commons carry keys the slicer no longer reads, the IR3 V2 and BabyBelt Pro machine limits miss the silent-mode entry, and the Custom belt base pins printer_extruder_id to one entry under a three-variant list. - The BabyBelt Pro and IR3 V2 filaments name the one extruder variant their printers have, as the other vendors' filaments built on the library do, and drop every override that only repeats the library value (diameter, density, temperature range, most of the fan settings...). The eSUN filaments inherit the narrowed list. - The Custom belt base inherits printer_extruder_id from its base. - normalize drops silent_mode, adaptive_layer_height and tree_support_with_infill; fix-variant gives the machine limits their silent-mode entry, which they read from the single value before. - The IR3 V2 drops two limits equal to its base in both modes. - Custom's index is regenerated and all three vendor versions bumped. Flattening every belt preset before and after, nothing a belt printer reads changes: printer_extruder_id is one id per variant, all 1. Printcepts and IdeaFormer keep their own machine and process bases: only filaments can inherit across vendor bundles (from OrcaFilamentLibrary), so they cannot build on the Custom belt printer. --- resources/profiles/Custom.json | 18 +++---- .../Custom/machine/fdm_belt_common.json | 3 -- resources/profiles/IdeaFormer.json | 2 +- .../Generic PETG @IdeaFormer IR3 V2.json | 39 ++------------ .../Generic PLA @IdeaFormer IR3 V2.json | 51 ++----------------- .../filament/eSUN PLA @IdeaFormer IR3 V2.json | 2 +- .../machine/IdeaFormer IR3 V2 0.4 nozzle.json | 19 ++++--- .../machine/fdm_klipper_common.json | 1 - .../machine/fdm_machine_common.json | 1 - .../process/fdm_process_common.json | 2 - resources/profiles/Printcepts.json | 2 +- .../filament/Generic PETG @BabyBelt Pro.json | 39 ++------------ .../filament/Generic PLA @BabyBelt Pro.json | 51 ++----------------- .../filament/eSUN PLA @BabyBelt Pro.json | 2 +- .../machine/BabyBelt Pro 0.4 nozzle.json | 1 + .../machine/fdm_klipper_common.json | 1 - .../machine/fdm_machine_common.json | 1 - .../process/fdm_process_common.json | 2 - 18 files changed, 39 insertions(+), 198 deletions(-) diff --git a/resources/profiles/Custom.json b/resources/profiles/Custom.json index 800264abe2..ab2084d898 100644 --- a/resources/profiles/Custom.json +++ b/resources/profiles/Custom.json @@ -1,6 +1,6 @@ { "name": "Custom Printer", - "version": "02.04.00.07", + "version": "02.04.00.08", "force_update": "0", "description": "My configurations", "machine_model_list": [ @@ -54,6 +54,10 @@ "name": "0.08mm Extra Fine @MyKlipper", "sub_path": "process/0.08mm Extra Fine @MyKlipper.json" }, + { + "name": "0.12mm Fine @MyBeltPrinter", + "sub_path": "process/0.12mm Fine @MyBeltPrinter.json" + }, { "name": "0.12mm Fine @MyKlipper", "sub_path": "process/0.12mm Fine @MyKlipper.json" @@ -66,10 +70,6 @@ "name": "0.16mm Optimal @MyKlipper", "sub_path": "process/0.16mm Optimal @MyKlipper.json" }, - { - "name": "0.12mm Fine @MyBeltPrinter", - "sub_path": "process/0.12mm Fine @MyBeltPrinter.json" - }, { "name": "0.20mm Standard @MyBeltPrinter", "sub_path": "process/0.20mm Standard @MyBeltPrinter.json" @@ -282,6 +282,10 @@ "name": "fdm_toolchanger_common", "sub_path": "machine/fdm_toolchanger_common.json" }, + { + "name": "MyRepetier 0.4 nozzle", + "sub_path": "machine/MyRepetier 0.4 nozzle.json" + }, { "name": "MyRRF 0.4 nozzle", "sub_path": "machine/MyRRF 0.4 nozzle.json" @@ -302,10 +306,6 @@ "name": "MyBeltPrinter 0.8 nozzle", "sub_path": "machine/MyBeltPrinter 0.8 nozzle.json" }, - { - "name": "MyRepetier 0.4 nozzle", - "sub_path": "machine/MyRepetier 0.4 nozzle.json" - }, { "name": "MyToolChanger 0.2 nozzle", "sub_path": "machine/MyToolChanger 0.2 nozzle.json" diff --git a/resources/profiles/Custom/machine/fdm_belt_common.json b/resources/profiles/Custom/machine/fdm_belt_common.json index 3c45b05515..19765b8d4e 100644 --- a/resources/profiles/Custom/machine/fdm_belt_common.json +++ b/resources/profiles/Custom/machine/fdm_belt_common.json @@ -85,9 +85,6 @@ "gcode_remap_x": "rev_x", "gcode_remap_y": "pos_z", "gcode_remap_z": "pos_y", - "printer_extruder_id": [ - "1" - ], "belt_printer": "1", "belt_slice_rotation": "x", "belt_slice_rotation_angle": "45", diff --git a/resources/profiles/IdeaFormer.json b/resources/profiles/IdeaFormer.json index ee076dfffb..ebed0bc9e0 100644 --- a/resources/profiles/IdeaFormer.json +++ b/resources/profiles/IdeaFormer.json @@ -1,6 +1,6 @@ { "name": "IdeaFormer", - "version": "02.00.00.05", + "version": "02.00.00.06", "force_update": "0", "description": "IdeaFormer belt printer configurations", "machine_model_list": [ diff --git a/resources/profiles/IdeaFormer/filament/Generic PETG @IdeaFormer IR3 V2.json b/resources/profiles/IdeaFormer/filament/Generic PETG @IdeaFormer IR3 V2.json index 709d0e9897..28da3959f4 100644 --- a/resources/profiles/IdeaFormer/filament/Generic PETG @IdeaFormer IR3 V2.json +++ b/resources/profiles/IdeaFormer/filament/Generic PETG @IdeaFormer IR3 V2.json @@ -5,6 +5,9 @@ "from": "system", "setting_id": "n4zaXcUUzTqAxq5f", "instantiation": "true", + "filament_extruder_variant": [ + "Direct Drive Standard" + ], "compatible_printers": [ "IdeaFormer IR3 V2 0.4 nozzle" ], @@ -17,54 +20,24 @@ "filament_settings_id": [ "Generic PETG @IdeaFormer IR3 V2" ], - "filament_diameter": [ - "1.75" - ], - "filament_density": [ - "1.27" - ], "filament_flow_ratio": [ "0.95" ], "filament_cost": [ "25" ], - "filament_max_volumetric_speed": [ - "10" - ], "nozzle_temperature": [ "240" ], "nozzle_temperature_initial_layer": [ "245" ], - "nozzle_temperature_range_low": [ - "220" - ], - "nozzle_temperature_range_high": [ - "260" - ], - "temperature_vitrification": [ - "70" - ], - "hot_plate_temp": [ - "80" - ], - "hot_plate_temp_initial_layer": [ - "80" - ], "cool_plate_temp": [ "80" ], "cool_plate_temp_initial_layer": [ "80" ], - "textured_plate_temp": [ - "80" - ], - "textured_plate_temp_initial_layer": [ - "80" - ], "fan_min_speed": [ "40" ], @@ -77,9 +50,6 @@ "overhang_fan_speed": [ "80" ], - "close_fan_the_first_x_layers": [ - "3" - ], "full_fan_speed_layer": [ "8" ], @@ -92,9 +62,6 @@ "fan_cooling_layer_time": [ "100" ], - "reduce_fan_stop_start_freq": [ - "1" - ], "filament_retraction_length": [ "2" ], diff --git a/resources/profiles/IdeaFormer/filament/Generic PLA @IdeaFormer IR3 V2.json b/resources/profiles/IdeaFormer/filament/Generic PLA @IdeaFormer IR3 V2.json index 10a8ed97f9..da5d4af50a 100644 --- a/resources/profiles/IdeaFormer/filament/Generic PLA @IdeaFormer IR3 V2.json +++ b/resources/profiles/IdeaFormer/filament/Generic PLA @IdeaFormer IR3 V2.json @@ -5,6 +5,9 @@ "from": "system", "setting_id": "1xjycsEAFh6KQIhp", "instantiation": "true", + "filament_extruder_variant": [ + "Direct Drive Standard" + ], "compatible_printers": [ "IdeaFormer IR3 V2 0.4 nozzle" ], @@ -17,36 +20,9 @@ "filament_settings_id": [ "Generic PLA @IdeaFormer IR3 V2" ], - "filament_diameter": [ - "1.75" - ], - "filament_density": [ - "1.24" - ], - "filament_flow_ratio": [ - "0.98" - ], - "filament_cost": [ - "20" - ], - "filament_max_volumetric_speed": [ - "12" - ], "nozzle_temperature": [ "215" ], - "nozzle_temperature_initial_layer": [ - "220" - ], - "nozzle_temperature_range_low": [ - "190" - ], - "nozzle_temperature_range_high": [ - "240" - ], - "temperature_vitrification": [ - "45" - ], "hot_plate_temp": [ "75" ], @@ -65,18 +41,6 @@ "textured_plate_temp_initial_layer": [ "75" ], - "fan_min_speed": [ - "100" - ], - "fan_max_speed": [ - "100" - ], - "overhang_fan_threshold": [ - "50%" - ], - "overhang_fan_speed": [ - "100" - ], "close_fan_the_first_x_layers": [ "3" ], @@ -86,15 +50,6 @@ "slow_down_min_speed": [ "20" ], - "slow_down_layer_time": [ - "4" - ], - "fan_cooling_layer_time": [ - "100" - ], - "reduce_fan_stop_start_freq": [ - "1" - ], "filament_retraction_length": [ "1.5" ], diff --git a/resources/profiles/IdeaFormer/filament/eSUN PLA @IdeaFormer IR3 V2.json b/resources/profiles/IdeaFormer/filament/eSUN PLA @IdeaFormer IR3 V2.json index 1dbaf8b2cf..b39a4c22fc 100644 --- a/resources/profiles/IdeaFormer/filament/eSUN PLA @IdeaFormer IR3 V2.json +++ b/resources/profiles/IdeaFormer/filament/eSUN PLA @IdeaFormer IR3 V2.json @@ -2,9 +2,9 @@ "type": "filament", "name": "eSUN PLA @IdeaFormer IR3 V2", "inherits": "Generic PLA @IdeaFormer IR3 V2", - "filament_id": "OFkrxQC4", "from": "system", "setting_id": "XqkviBmFHEglXueX", + "filament_id": "OFkrxQC4", "instantiation": "true", "compatible_printers": [ "IdeaFormer IR3 V2 0.4 nozzle" diff --git a/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json b/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json index ce02ce82d1..f993631921 100644 --- a/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json +++ b/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json @@ -27,49 +27,56 @@ ], "default_print_profile": "0.20mm Standard @IdeaFormer IR3 V2", "use_relative_e_distances": "1", - "machine_max_acceleration_e": [ - "5000" - ], "machine_max_acceleration_extruding": [ + "5000", "5000" ], "machine_max_acceleration_retracting": [ + "1000", "1000" ], "machine_max_acceleration_travel": [ + "9000", "9000" ], "machine_max_acceleration_x": [ + "5000", "5000" ], "machine_max_acceleration_y": [ + "5000", "5000" ], "machine_max_acceleration_z": [ + "100", "100" ], - "machine_max_jerk_e": [ - "2.5" - ], "machine_max_jerk_x": [ + "10", "10" ], "machine_max_jerk_y": [ + "10", "10" ], "machine_max_jerk_z": [ + "0.4", "0.4" ], "machine_max_speed_e": [ + "60", "60" ], "machine_max_speed_x": [ + "500", "500" ], "machine_max_speed_y": [ + "500", "500" ], "machine_max_speed_z": [ + "20", "20" ], "retraction_length": [ diff --git a/resources/profiles/IdeaFormer/machine/fdm_klipper_common.json b/resources/profiles/IdeaFormer/machine/fdm_klipper_common.json index 36f3fe13c6..5297f646ff 100644 --- a/resources/profiles/IdeaFormer/machine/fdm_klipper_common.json +++ b/resources/profiles/IdeaFormer/machine/fdm_klipper_common.json @@ -117,7 +117,6 @@ "30" ], "z_hop_types": "Normal Lift", - "silent_mode": "0", "single_extruder_multi_material": "1", "change_filament_gcode": "", "wipe": [ diff --git a/resources/profiles/IdeaFormer/machine/fdm_machine_common.json b/resources/profiles/IdeaFormer/machine/fdm_machine_common.json index 963f39c9d8..bf8f2249cf 100644 --- a/resources/profiles/IdeaFormer/machine/fdm_machine_common.json +++ b/resources/profiles/IdeaFormer/machine/fdm_machine_common.json @@ -14,7 +14,6 @@ "0x0" ], "gcode_flavor": "marlin", - "silent_mode": "0", "machine_max_acceleration_e": [ "5000" ], diff --git a/resources/profiles/IdeaFormer/process/fdm_process_common.json b/resources/profiles/IdeaFormer/process/fdm_process_common.json index b8e882770a..0d558129af 100644 --- a/resources/profiles/IdeaFormer/process/fdm_process_common.json +++ b/resources/profiles/IdeaFormer/process/fdm_process_common.json @@ -3,7 +3,6 @@ "name": "fdm_process_common", "from": "system", "instantiation": "false", - "adaptive_layer_height": "0", "reduce_crossing_wall": "0", "max_travel_detour_distance": "0", "bottom_surface_pattern": "monotonic", @@ -83,7 +82,6 @@ "support_object_xy_distance": "0.35", "tree_support_branch_angle": "30", "tree_support_wall_count": "0", - "tree_support_with_infill": "0", "detect_thin_wall": "0", "top_surface_pattern": "monotonicline", "top_shell_thickness": "0.8", diff --git a/resources/profiles/Printcepts.json b/resources/profiles/Printcepts.json index cdeda8f04d..70338174d6 100644 --- a/resources/profiles/Printcepts.json +++ b/resources/profiles/Printcepts.json @@ -1,6 +1,6 @@ { "name": "Printcepts", - "version": "01.00.00.03", + "version": "01.00.00.04", "force_update": "0", "description": "Printcepts belt printer configurations", "machine_model_list": [ diff --git a/resources/profiles/Printcepts/filament/Generic PETG @BabyBelt Pro.json b/resources/profiles/Printcepts/filament/Generic PETG @BabyBelt Pro.json index 5bfd8e7f2d..9b71a420b6 100644 --- a/resources/profiles/Printcepts/filament/Generic PETG @BabyBelt Pro.json +++ b/resources/profiles/Printcepts/filament/Generic PETG @BabyBelt Pro.json @@ -5,6 +5,9 @@ "from": "system", "setting_id": "gCzHpDNgVwQR6tgk", "instantiation": "true", + "filament_extruder_variant": [ + "Direct Drive Standard" + ], "compatible_printers": [ "BabyBelt Pro 0.4 nozzle" ], @@ -17,54 +20,24 @@ "filament_settings_id": [ "Generic PETG @BabyBelt Pro" ], - "filament_diameter": [ - "1.75" - ], - "filament_density": [ - "1.27" - ], "filament_flow_ratio": [ "0.95" ], "filament_cost": [ "25" ], - "filament_max_volumetric_speed": [ - "10" - ], "nozzle_temperature": [ "240" ], "nozzle_temperature_initial_layer": [ "245" ], - "nozzle_temperature_range_low": [ - "220" - ], - "nozzle_temperature_range_high": [ - "260" - ], - "temperature_vitrification": [ - "70" - ], - "hot_plate_temp": [ - "80" - ], - "hot_plate_temp_initial_layer": [ - "80" - ], "cool_plate_temp": [ "80" ], "cool_plate_temp_initial_layer": [ "80" ], - "textured_plate_temp": [ - "80" - ], - "textured_plate_temp_initial_layer": [ - "80" - ], "fan_min_speed": [ "40" ], @@ -77,9 +50,6 @@ "overhang_fan_speed": [ "80" ], - "close_fan_the_first_x_layers": [ - "3" - ], "full_fan_speed_layer": [ "8" ], @@ -92,9 +62,6 @@ "fan_cooling_layer_time": [ "100" ], - "reduce_fan_stop_start_freq": [ - "1" - ], "filament_retraction_length": [ "2" ], diff --git a/resources/profiles/Printcepts/filament/Generic PLA @BabyBelt Pro.json b/resources/profiles/Printcepts/filament/Generic PLA @BabyBelt Pro.json index 4637f21273..3345b5f509 100644 --- a/resources/profiles/Printcepts/filament/Generic PLA @BabyBelt Pro.json +++ b/resources/profiles/Printcepts/filament/Generic PLA @BabyBelt Pro.json @@ -5,6 +5,9 @@ "from": "system", "setting_id": "24PpcnhVx9v5f4fD", "instantiation": "true", + "filament_extruder_variant": [ + "Direct Drive Standard" + ], "compatible_printers": [ "BabyBelt Pro 0.4 nozzle" ], @@ -17,36 +20,9 @@ "filament_settings_id": [ "Generic PLA @BabyBelt Pro" ], - "filament_diameter": [ - "1.75" - ], - "filament_density": [ - "1.24" - ], - "filament_flow_ratio": [ - "0.98" - ], - "filament_cost": [ - "20" - ], - "filament_max_volumetric_speed": [ - "12" - ], "nozzle_temperature": [ "215" ], - "nozzle_temperature_initial_layer": [ - "220" - ], - "nozzle_temperature_range_low": [ - "190" - ], - "nozzle_temperature_range_high": [ - "240" - ], - "temperature_vitrification": [ - "45" - ], "hot_plate_temp": [ "75" ], @@ -65,18 +41,6 @@ "textured_plate_temp_initial_layer": [ "75" ], - "fan_min_speed": [ - "100" - ], - "fan_max_speed": [ - "100" - ], - "overhang_fan_threshold": [ - "50%" - ], - "overhang_fan_speed": [ - "100" - ], "close_fan_the_first_x_layers": [ "3" ], @@ -86,15 +50,6 @@ "slow_down_min_speed": [ "20" ], - "slow_down_layer_time": [ - "4" - ], - "fan_cooling_layer_time": [ - "100" - ], - "reduce_fan_stop_start_freq": [ - "1" - ], "filament_retraction_length": [ "1.5" ], diff --git a/resources/profiles/Printcepts/filament/eSUN PLA @BabyBelt Pro.json b/resources/profiles/Printcepts/filament/eSUN PLA @BabyBelt Pro.json index d405deaef1..1f93703e1c 100644 --- a/resources/profiles/Printcepts/filament/eSUN PLA @BabyBelt Pro.json +++ b/resources/profiles/Printcepts/filament/eSUN PLA @BabyBelt Pro.json @@ -2,9 +2,9 @@ "type": "filament", "name": "eSUN PLA @BabyBelt Pro", "inherits": "Generic PLA @BabyBelt Pro", - "filament_id": "OFkrxQC4", "from": "system", "setting_id": "EH3X7oE0DU5tSpjW", + "filament_id": "OFkrxQC4", "instantiation": "true", "compatible_printers": [ "BabyBelt Pro 0.4 nozzle" diff --git a/resources/profiles/Printcepts/machine/BabyBelt Pro 0.4 nozzle.json b/resources/profiles/Printcepts/machine/BabyBelt Pro 0.4 nozzle.json index 620498e0d4..84218476cb 100644 --- a/resources/profiles/Printcepts/machine/BabyBelt Pro 0.4 nozzle.json +++ b/resources/profiles/Printcepts/machine/BabyBelt Pro 0.4 nozzle.json @@ -56,6 +56,7 @@ "20000" ], "machine_max_junction_deviation": [ + "0.01", "0.01" ], "machine_max_speed_x": [ diff --git a/resources/profiles/Printcepts/machine/fdm_klipper_common.json b/resources/profiles/Printcepts/machine/fdm_klipper_common.json index 36f3fe13c6..5297f646ff 100644 --- a/resources/profiles/Printcepts/machine/fdm_klipper_common.json +++ b/resources/profiles/Printcepts/machine/fdm_klipper_common.json @@ -117,7 +117,6 @@ "30" ], "z_hop_types": "Normal Lift", - "silent_mode": "0", "single_extruder_multi_material": "1", "change_filament_gcode": "", "wipe": [ diff --git a/resources/profiles/Printcepts/machine/fdm_machine_common.json b/resources/profiles/Printcepts/machine/fdm_machine_common.json index 963f39c9d8..bf8f2249cf 100644 --- a/resources/profiles/Printcepts/machine/fdm_machine_common.json +++ b/resources/profiles/Printcepts/machine/fdm_machine_common.json @@ -14,7 +14,6 @@ "0x0" ], "gcode_flavor": "marlin", - "silent_mode": "0", "machine_max_acceleration_e": [ "5000" ], diff --git a/resources/profiles/Printcepts/process/fdm_process_common.json b/resources/profiles/Printcepts/process/fdm_process_common.json index b8e882770a..0d558129af 100644 --- a/resources/profiles/Printcepts/process/fdm_process_common.json +++ b/resources/profiles/Printcepts/process/fdm_process_common.json @@ -3,7 +3,6 @@ "name": "fdm_process_common", "from": "system", "instantiation": "false", - "adaptive_layer_height": "0", "reduce_crossing_wall": "0", "max_travel_detour_distance": "0", "bottom_surface_pattern": "monotonic", @@ -83,7 +82,6 @@ "support_object_xy_distance": "0.35", "tree_support_branch_angle": "30", "tree_support_wall_count": "0", - "tree_support_with_infill": "0", "detect_thin_wall": "0", "top_surface_pattern": "monotonicline", "top_shell_thickness": "0.8", From 7eb9aa383269a187820aede772a532ab973d4bcf Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sun, 4 Oct 2026 22:57:53 -0500 Subject: [PATCH 185/224] Profile validator: accept a belt printer's tool change without a tower The validator slices every printer with two filaments and the prime tower forced on, then requires the tower's CP TOOLCHANGE START block as proof that change_filament_gcode ran. A belt printer has no wipe tower: it purges into a prism object on the belt, so Print::has_wipe_tower() is false and the change is the plain T command set_extruder() emits. All six belt printers failed the sweep on that alone, although each one changes filament throughout the slice. Look for the T1 line on belt printers. --- src/dev-utils/OrcaSlicer_profile_validator.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/dev-utils/OrcaSlicer_profile_validator.cpp b/src/dev-utils/OrcaSlicer_profile_validator.cpp index 140716c896..f4ce09a3e4 100644 --- a/src/dev-utils/OrcaSlicer_profile_validator.cpp +++ b/src/dev-utils/OrcaSlicer_profile_validator.cpp @@ -519,11 +519,16 @@ int slice_all_printers(const std::string &vendor, const std::string &outdir) const std::string filament_name = bundle.filaments.get_selected_preset_name(); const std::string what = "Printer \"" + printer + "\""; const std::string file_base = sanitize_filename(vendor_name) + "__" + sanitize_filename(printer); + // A belt printer has no wipe tower (it purges into a prism object on the belt), so its + // filament change is the plain tool change set_extruder() emits rather than the tower's block. + const bool belt = bundle.printers.get_selected_preset().config.opt_bool("belt_printer"); + const std::string marker = belt ? "\nT1\n" : "CP TOOLCHANGE START"; if (const std::string out = slice_selection(bundle, what, false, outdir, file_base); out.empty()) ++failures; - else if (out.find("CP TOOLCHANGE START") == std::string::npos) { - // The filament change never rode the tower, so change_filament_gcode was not exercised. - BOOST_LOG_TRIVIAL(error) << what << " sliced but the filament change never fired (no CP TOOLCHANGE START)"; + else if (out.find(marker) == std::string::npos) { + // The filament change never fired, so change_filament_gcode was not exercised. + BOOST_LOG_TRIVIAL(error) << what << " sliced but the filament change never fired (no " + << (belt ? "T1" : "CP TOOLCHANGE START") << ")"; ++failures; } cover(bundle.prints.get_selected_preset()); From 2cb72dce0affa9b82e94b89f12e8c37a12b9dae3 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Mon, 5 Oct 2026 13:42:47 -0500 Subject: [PATCH 186/224] Check profiles: leave engine PRs' slice sweep to Build all The slice sweep here runs the nightly validator, built from main, so it cannot expand custom G-code that uses a setting the PR adds to the engine and reports it as an undefined placeholder. The belt printer PR fails on exactly that: the BabyBelt Pro start G-code passes [belt_slice_rotation_angle] to its firmware, and main has no such setting. A PR that changes src/ also runs Build all, whose Slice check runs the same sweep with the validator built from the PR (it passes on that PR). So the sweep here now runs only for PRs that leave src/ alone, which are the profile-only PRs it exists for and the ones pr-merge-bot gates on. If the base commit cannot be fetched, the sweep runs as before. --- .github/workflows/check_profiles.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/check_profiles.yml b/.github/workflows/check_profiles.yml index 2a6ed8f0a4..422513442e 100644 --- a/.github/workflows/check_profiles.yml +++ b/.github/workflows/check_profiles.yml @@ -74,12 +74,26 @@ jobs: set +e ./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -l 2 2>&1 | tee ${{ runner.temp }}/validate_system.log exit ${PIPESTATUS[0]} + # The validator above is the nightly build of main, so it cannot slice profiles that use + # settings a PR adds to the engine: it reports their placeholders as undefined. A PR that + # changes src/ also runs Build all, whose Slice check runs this same sweep with the + # validator built from the PR, so the sweep below only runs for the other PRs. + - name: Detect engine changes + id: engine_changes + if: ${{ github.event_name == 'pull_request' }} + run: | + base=${{ github.event.pull_request.base.sha }} + if git fetch --no-tags --depth=1 origin "$base" && ! git diff --quiet "$base" HEAD -- src/; then + echo "changed=true" >> "$GITHUB_OUTPUT" + echo "::notice::This PR changes src/, so Build all's Slice check slices the profiles with the PR-built validator." + fi # Slice a two-colour cube through every printer, and through every system process/filament whose # templates no printer's own slice reaches, so every custom g-code and filename_format shipped is # expanded (names in {if} branches not taken included) - catches undefined-placeholder / # invalid-flow bugs the static checks above cannot see. - name: validate slice (expand custom g-code) id: validate_slice + if: ${{ steps.engine_changes.outputs.changed != 'true' }} continue-on-error: true run: | set +e From 98c5eed4a67f17f6bd78148191b5d242ef80b60e Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Mon, 5 Oct 2026 14:19:53 -0500 Subject: [PATCH 187/224] Preview: reuse a belt print's toolpaths until the view is toggled load_as_gcode() skips the conversion and GPU upload when it is handed the result it already shows, but belt printers were exempt from that cache, because the designed/raw view changes the toolpath geometry for the same result. So every preview reload of a belt print, switching back to the Preview tab for one, converted and uploaded every toolpath again. Remember the view the result was converted for and reuse it while both match. Toggling the view (B, the legend checkbox, the toolbar menu) still converts again. The print config the back-transform reads cannot change without a new G-code result, so the result id covers it as it does on any other printer. Suggested by raistlin7447 on #14394. --- src/slic3r/GUI/GCodeViewer.cpp | 11 ++++++----- src/slic3r/GUI/GCodeViewer.hpp | 2 ++ src/slic3r/GUI/GUI_Preview.cpp | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 03fcb8699d..c886b7d65a 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1390,11 +1390,11 @@ 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. - // Belt printers are exempt: the toolpath geometry fed to libvgcode depends on - // the current designed/raw view state (back-transform applied in convert), so - // re-running the conversion is required for the upright view and for toggling - // it (hotkey B) to take effect even when the G-code itself is unchanged. - if (m_last_result_id == gcode_result.id && wxGetApp().is_editor() && !print.config().belt_printer.value) { + // 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 = !print.config().belt_printer.value || m_last_belt_show_designed == m_belt_show_designed; + if (m_last_result_id == gcode_result.id && wxGetApp().is_editor() && same_belt_view) { //BBS: add logs BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": the same id %1%, return directly, result %2% ") % m_last_result_id % (&gcode_result); @@ -1688,6 +1688,7 @@ 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) diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 656d719130..06d275a187 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -199,6 +199,8 @@ private: std::vector 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(EMoveType::Count)> m_move_type_counts{}; diff --git a/src/slic3r/GUI/GUI_Preview.cpp b/src/slic3r/GUI/GUI_Preview.cpp index 3a174c2aac..f52bd20cc2 100644 --- a/src/slic3r/GUI/GUI_Preview.cpp +++ b/src/slic3r/GUI/GUI_Preview.cpp @@ -373,7 +373,7 @@ 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, - // and belt printers are exempt from the same-result-id load cache so the re-convert runs). + // 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; From 9dbd0307f8cdc705b44dedd204e4b9d68e0b5e44 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Mon, 5 Oct 2026 20:43:18 -0500 Subject: [PATCH 188/224] Add the includes clang-tidy's include cleaner asks for on the belt files The clang-tidy job on #14394 fails on 129 misc-include-cleaner findings: the belt sources and tests use std::, Eigen, Point/PrintConfig and BeltBrim symbols without including the header that provides them, which only compiled because the precompiled header supplied it. Every include the job names is added, in each file's existing include style ("../" in the GCode/ and Support/ subdirectories, quoted libslic3r/ paths in the GUI and tests). No code changes. Verified with scripts/clang_tidy_diff.py -p build-tidy --base eb5b9a77b9 (SLIC3R_PCH=OFF compile database, clang-tidy 22.1.8): no findings left. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01L6Kg5igmmMU2YLoK6HrsWV --- src/libslic3r/BeltBrim.cpp | 14 ++++++++++++++ src/libslic3r/BeltBrim.hpp | 1 + src/libslic3r/BeltGCode.cpp | 4 ++++ src/libslic3r/BeltGCode.hpp | 2 ++ src/libslic3r/BeltPurge.cpp | 7 +++++++ src/libslic3r/BeltSliceStrategy.cpp | 4 ++++ src/libslic3r/BeltTransform.cpp | 9 +++++++++ src/libslic3r/BeltTransform.hpp | 1 + src/libslic3r/FirstLayerPlane.cpp | 4 ++++ src/libslic3r/GCode.cpp | 1 + src/libslic3r/GCode.hpp | 2 ++ src/libslic3r/GCode/BeltBackTransform.cpp | 2 ++ src/libslic3r/GCode/BeltKinematics.cpp | 2 ++ src/libslic3r/GCode/BeltKinematics.hpp | 1 + src/libslic3r/GCode/MachineFrameTransform.cpp | 3 +++ src/libslic3r/GCode/MachineKinematics.cpp | 1 + src/libslic3r/GCode/ToolOrdering.cpp | 1 + src/libslic3r/GCodeWriter.cpp | 1 + src/libslic3r/Print.cpp | 1 + src/libslic3r/PrintObject.cpp | 1 + src/libslic3r/Support/BeltFloorContext.cpp | 8 ++++++++ src/libslic3r/Support/BeltFloorContext.hpp | 3 +++ src/libslic3r/Support/SupportCommon.hpp | 2 ++ src/libslic3r/Support/TreeModelVolumes.cpp | 1 + src/libslic3r/calib.cpp | 2 ++ src/libslic3r/calib.hpp | 1 + src/slic3r/GUI/3DBed.cpp | 2 ++ src/slic3r/GUI/BeltPurgeTower.cpp | 7 +++++++ src/slic3r/GUI/ConfigManipulation.cpp | 1 + src/slic3r/GUI/GCodeViewer.cpp | 1 + src/slic3r/GUI/GUI_App.cpp | 1 + src/slic3r/GUI/GUI_App.hpp | 1 + src/slic3r/GUI/Jobs/ArrangeJob.cpp | 3 +++ src/slic3r/GUI/Plater.cpp | 1 + src/slic3r/GUI/calib_dlg.hpp | 1 + tests/fff_print/test_gcode_processor.cpp | 3 +++ tests/fff_print/test_gcodewriter.cpp | 1 + tests/fff_print/test_precise_seam.cpp | 2 ++ tests/fff_print/test_skirt_brim.cpp | 7 +++++++ tests/libslic3r/test_arrange.cpp | 1 + tests/libslic3r/test_belt_brim.cpp | 12 ++++++++++++ 41 files changed, 123 insertions(+) diff --git a/src/libslic3r/BeltBrim.cpp b/src/libslic3r/BeltBrim.cpp index 45ef5d14c9..d77f163467 100644 --- a/src/libslic3r/BeltBrim.cpp +++ b/src/libslic3r/BeltBrim.cpp @@ -8,8 +8,22 @@ #include "Print.hpp" #include "ShortestPath.hpp" #include "Support/BeltFloorContext.hpp" +#include "BoundingBox.hpp" +#include "ExPolygon.hpp" +#include "ExtrusionEntity.hpp" +#include "ExtrusionEntityCollection.hpp" +#include "Point.hpp" +#include "Polyline.hpp" +#include "PrintConfig.hpp" +#include "libslic3r.h" #include +#include +#include +#include +#include +#include +#include namespace Slic3r { diff --git a/src/libslic3r/BeltBrim.hpp b/src/libslic3r/BeltBrim.hpp index d6f0980bc8..ac4986c311 100644 --- a/src/libslic3r/BeltBrim.hpp +++ b/src/libslic3r/BeltBrim.hpp @@ -5,6 +5,7 @@ #include "ExtrusionEntityCollection.hpp" #include "Point.hpp" #include "Polyline.hpp" +#include "libslic3r.h" #include #include diff --git a/src/libslic3r/BeltGCode.cpp b/src/libslic3r/BeltGCode.cpp index 20b19e41b0..a78c0cbfa6 100644 --- a/src/libslic3r/BeltGCode.cpp +++ b/src/libslic3r/BeltGCode.cpp @@ -3,6 +3,10 @@ #include "GCode/BeltKinematics.hpp" #include "BeltTransform.hpp" #include "Print.hpp" +#include "Point.hpp" +#include "PrintConfig.hpp" +#include "libslic3r.h" +#include namespace Slic3r { diff --git a/src/libslic3r/BeltGCode.hpp b/src/libslic3r/BeltGCode.hpp index c643c6b8b0..1631a7dc9c 100644 --- a/src/libslic3r/BeltGCode.hpp +++ b/src/libslic3r/BeltGCode.hpp @@ -1,6 +1,8 @@ #pragma once #include "GCode.hpp" +#include "Point.hpp" +#include "Print.hpp" namespace Slic3r { diff --git a/src/libslic3r/BeltPurge.cpp b/src/libslic3r/BeltPurge.cpp index c4cbc83425..3b5c5571a6 100644 --- a/src/libslic3r/BeltPurge.cpp +++ b/src/libslic3r/BeltPurge.cpp @@ -27,12 +27,19 @@ #include "format.hpp" #include "LocalesUtils.hpp" #include "libslic3r.h" +#include "BeltBrim.hpp" +#include "PrintBase.hpp" #include #include #include #include +#include +#include +#include +#include +#include namespace Slic3r { diff --git a/src/libslic3r/BeltSliceStrategy.cpp b/src/libslic3r/BeltSliceStrategy.cpp index 74d8566000..8892b4e5de 100644 --- a/src/libslic3r/BeltSliceStrategy.cpp +++ b/src/libslic3r/BeltSliceStrategy.cpp @@ -1,7 +1,11 @@ #include "BeltSliceStrategy.hpp" #include "Model.hpp" +#include "BeltTransform.hpp" +#include "Point.hpp" +#include "PrintConfig.hpp" #include +#include namespace Slic3r { diff --git a/src/libslic3r/BeltTransform.cpp b/src/libslic3r/BeltTransform.cpp index db23e13417..0a59507ff2 100644 --- a/src/libslic3r/BeltTransform.cpp +++ b/src/libslic3r/BeltTransform.cpp @@ -1,7 +1,16 @@ #include "BeltTransform.hpp" #include "Model.hpp" +#include "BoundingBox.hpp" +#include "Config.hpp" +#include "Geometry.hpp" +#include "Point.hpp" +#include "PrintConfig.hpp" +#include "libslic3r.h" #include +#include +#include +#include namespace Slic3r { diff --git a/src/libslic3r/BeltTransform.hpp b/src/libslic3r/BeltTransform.hpp index ad5b6d6ffd..2fe725e1df 100644 --- a/src/libslic3r/BeltTransform.hpp +++ b/src/libslic3r/BeltTransform.hpp @@ -5,6 +5,7 @@ #include "BoundingBox.hpp" #include "PrintConfig.hpp" #include "Geometry.hpp" +#include "Config.hpp" #include diff --git a/src/libslic3r/FirstLayerPlane.cpp b/src/libslic3r/FirstLayerPlane.cpp index f668ea774e..7642d42267 100644 --- a/src/libslic3r/FirstLayerPlane.cpp +++ b/src/libslic3r/FirstLayerPlane.cpp @@ -1,5 +1,9 @@ #include "FirstLayerPlane.hpp" #include "BeltTransform.hpp" +#include "BoundingBox.hpp" +#include "Point.hpp" +#include "PrintConfig.hpp" +#include "libslic3r.h" #include #include diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index a911ad2534..7fefed0f87 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -106,6 +106,7 @@ #include #include "calib.hpp" #include "libslic3r_version.h" +#include "GCode/BeltKinematics.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) diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 76e5fc9a7e..3a08ffccec 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -48,8 +48,10 @@ #include #include #include +#include #include "BoundingBox.hpp" #include "Polyline.hpp" +#include "BeltBrim.hpp" namespace Slic3r { class ExtrusionEntityCollection; } diff --git a/src/libslic3r/GCode/BeltBackTransform.cpp b/src/libslic3r/GCode/BeltBackTransform.cpp index 6a25bbd408..4b247323cd 100644 --- a/src/libslic3r/GCode/BeltBackTransform.cpp +++ b/src/libslic3r/GCode/BeltBackTransform.cpp @@ -1,5 +1,7 @@ #include "BeltBackTransform.hpp" #include "../BeltTransform.hpp" +#include "../Point.hpp" +#include "../PrintConfig.hpp" namespace Slic3r { diff --git a/src/libslic3r/GCode/BeltKinematics.cpp b/src/libslic3r/GCode/BeltKinematics.cpp index 155e18d32d..c36c088658 100644 --- a/src/libslic3r/GCode/BeltKinematics.cpp +++ b/src/libslic3r/GCode/BeltKinematics.cpp @@ -2,6 +2,8 @@ #include "../BeltTransform.hpp" #include "../PrintConfig.hpp" #include "../GCodeWriter.hpp" +#include "../Point.hpp" +#include namespace Slic3r { diff --git a/src/libslic3r/GCode/BeltKinematics.hpp b/src/libslic3r/GCode/BeltKinematics.hpp index f145ecf3e9..8f10bd1074 100644 --- a/src/libslic3r/GCode/BeltKinematics.hpp +++ b/src/libslic3r/GCode/BeltKinematics.hpp @@ -4,6 +4,7 @@ #include "MachineKinematics.hpp" #include "BeltBackTransform.hpp" #include "MachineFrameTransform.hpp" +#include "../Point.hpp" namespace Slic3r { diff --git a/src/libslic3r/GCode/MachineFrameTransform.cpp b/src/libslic3r/GCode/MachineFrameTransform.cpp index af09ba7353..9159dbca67 100644 --- a/src/libslic3r/GCode/MachineFrameTransform.cpp +++ b/src/libslic3r/GCode/MachineFrameTransform.cpp @@ -1,5 +1,8 @@ #include "MachineFrameTransform.hpp" #include "../Geometry.hpp" +#include "../Point.hpp" +#include "../PrintConfig.hpp" +#include "../libslic3r.h" #include diff --git a/src/libslic3r/GCode/MachineKinematics.cpp b/src/libslic3r/GCode/MachineKinematics.cpp index 05f5375aef..063020fe8a 100644 --- a/src/libslic3r/GCode/MachineKinematics.cpp +++ b/src/libslic3r/GCode/MachineKinematics.cpp @@ -1,4 +1,5 @@ #include "MachineKinematics.hpp" +#include "../Point.hpp" namespace Slic3r { diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index 38e03717d2..10b34123b6 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -18,6 +18,7 @@ #include "Utils.hpp" #include "format.hpp" #include "I18N.hpp" +#include "../BeltBrim.hpp" #include #include diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index 0065ddb1cc..a6e81dfe1e 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #ifdef __APPLE__ #include diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 4762116c85..9dc9ff3cd3 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -106,6 +106,7 @@ #include "Format/STEP.hpp" #include "PlaceholderParser.hpp" #include "SurfaceCollection.hpp" +#include "BeltBrim.hpp" namespace fs = boost::filesystem; diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 352416cd13..417b472bd0 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -83,6 +83,7 @@ #include "Fill/Lightning/Generator.hpp" #include "SurfaceCollection.hpp" #include "TriangleMesh.hpp" +#include "BeltBrim.hpp" namespace Slic3r { enum class EnforcerBlockerType : int8_t; } diff --git a/src/libslic3r/Support/BeltFloorContext.cpp b/src/libslic3r/Support/BeltFloorContext.cpp index eb6cf3670c..26078413dd 100644 --- a/src/libslic3r/Support/BeltFloorContext.cpp +++ b/src/libslic3r/Support/BeltFloorContext.cpp @@ -1,7 +1,15 @@ #include "BeltFloorContext.hpp" +#include "../Point.hpp" +#include "../Polygon.hpp" +#include "../PrintConfig.hpp" +#include "../Slicing.hpp" +#include "../libslic3r.h" #include #include +#include +#include +#include namespace Slic3r { diff --git a/src/libslic3r/Support/BeltFloorContext.hpp b/src/libslic3r/Support/BeltFloorContext.hpp index 3a45a6eba8..629dfce4b6 100644 --- a/src/libslic3r/Support/BeltFloorContext.hpp +++ b/src/libslic3r/Support/BeltFloorContext.hpp @@ -5,6 +5,9 @@ #include "../Polygon.hpp" #include "../Slicing.hpp" #include "../PrintConfig.hpp" +#include +#include +#include namespace Slic3r { diff --git a/src/libslic3r/Support/SupportCommon.hpp b/src/libslic3r/Support/SupportCommon.hpp index 0ab4034b89..ca91ecc5ae 100644 --- a/src/libslic3r/Support/SupportCommon.hpp +++ b/src/libslic3r/Support/SupportCommon.hpp @@ -7,6 +7,8 @@ #include "libslic3r/ExtrusionEntity.hpp" #include "SupportLayer.hpp" #include "SupportParameters.hpp" +#include "../Point.hpp" +#include "../libslic3r.h" #include #include #include diff --git a/src/libslic3r/Support/TreeModelVolumes.cpp b/src/libslic3r/Support/TreeModelVolumes.cpp index 897e9a4e4e..f3e322154a 100644 --- a/src/libslic3r/Support/TreeModelVolumes.cpp +++ b/src/libslic3r/Support/TreeModelVolumes.cpp @@ -20,6 +20,7 @@ #include "../Utils.hpp" #include "../format.hpp" #include "libslic3r/libslic3r.h" +#include "../PrintConfig.hpp" #include #include diff --git a/src/libslic3r/calib.cpp b/src/libslic3r/calib.cpp index 19c6019a33..cdf105fa69 100644 --- a/src/libslic3r/calib.cpp +++ b/src/libslic3r/calib.cpp @@ -19,6 +19,8 @@ #include #include #include +#include +#include #include "clonable_ptr.hpp" namespace Slic3r { diff --git a/src/libslic3r/calib.hpp b/src/libslic3r/calib.hpp index df3b0e39a4..1f1c7844dc 100644 --- a/src/libslic3r/calib.hpp +++ b/src/libslic3r/calib.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #define calib_pressure_advance_dd #include "GCodeWriter.hpp" diff --git a/src/slic3r/GUI/3DBed.cpp b/src/slic3r/GUI/3DBed.cpp index 5ab4893ee1..b28f9c5d1c 100644 --- a/src/slic3r/GUI/3DBed.cpp +++ b/src/slic3r/GUI/3DBed.cpp @@ -39,6 +39,8 @@ #if BOOST_VERSION >= 107800 #include +#include +#include #else #include #endif diff --git a/src/slic3r/GUI/BeltPurgeTower.cpp b/src/slic3r/GUI/BeltPurgeTower.cpp index e90491a972..c18d549db1 100644 --- a/src/slic3r/GUI/BeltPurgeTower.cpp +++ b/src/slic3r/GUI/BeltPurgeTower.cpp @@ -13,6 +13,9 @@ #include "libslic3r/TriangleMesh.hpp" #include "libslic3r/Geometry.hpp" #include "libslic3r/BoundingBox.hpp" +#include "libslic3r/Config.hpp" +#include "libslic3r/Point.hpp" +#include "libslic3r/libslic3r.h" #include #include @@ -21,6 +24,10 @@ #include #include +#include +#include +#include +#include namespace Slic3r { namespace GUI { diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index c549ee5122..0d1c141fa8 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -17,6 +17,7 @@ #include "slic3r/GUI/GUI.hpp" #include "libslic3r/libslic3r.h" #include "slic3r/GUI/Field.hpp" +#include "libslic3r/BeltBrim.hpp" #include #include #include diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index c886b7d65a..cec7f9507e 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -89,6 +89,7 @@ #include #include #include +#include #include "libslic3r/AppConfig.hpp" #include "libslic3r/BoundingBox.hpp" #include "libslic3r/CutUtils.hpp" diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 91af95c6f1..6da56e3d90 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -121,6 +121,7 @@ // This is the only place where we want to allow that, so define an override macro. #define SLIC3R_ALLOW_LIBSLIC3R_I18N_IN_SLIC3R #include "libslic3r/I18N.hpp" +#include "libslic3r/Point.hpp" #undef SLIC3R_ALLOW_LIBSLIC3R_I18N_IN_SLIC3R #include "slic3r/GUI/I18N.hpp" diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index afdef711f6..8b9ad1bae7 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -30,6 +30,7 @@ #include "slic3r/GUI/Jobs/UpgradeNetworkJob.hpp" #include "slic3r/GUI/HttpServer.hpp" #include "../Utils/PrintHost.hpp" +#include "libslic3r/Point.hpp" #include #include diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.cpp b/src/slic3r/GUI/Jobs/ArrangeJob.cpp index 3044b83bb7..8f9d013bb6 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.cpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.cpp @@ -38,7 +38,10 @@ #include "libslic3r/LifecycleEvents.hpp" #include #include "libslic3r/Geometry.hpp" +#include "libslic3r/BoundingBox.hpp" +#include "libslic3r/Config.hpp" #include +#include #define SAVE_ARRANGE_POLY 0 diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 6fb1a1e008..3684020f8a 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -267,6 +267,7 @@ #include #include // Needs to be last because reasons :-/ #include +#include #include "WipeTowerDialog.hpp" #include "MixedFilamentDialog.hpp" #include "TextureImportDialog.hpp" diff --git a/src/slic3r/GUI/calib_dlg.hpp b/src/slic3r/GUI/calib_dlg.hpp index de6aedde0d..eb0c5c9c01 100644 --- a/src/slic3r/GUI/calib_dlg.hpp +++ b/src/slic3r/GUI/calib_dlg.hpp @@ -17,6 +17,7 @@ #include #include #include +#include #include "libslic3r/calib.hpp" namespace Slic3r { namespace GUI { diff --git a/tests/fff_print/test_gcode_processor.cpp b/tests/fff_print/test_gcode_processor.cpp index 3998b65547..2d3aab2909 100644 --- a/tests/fff_print/test_gcode_processor.cpp +++ b/tests/fff_print/test_gcode_processor.cpp @@ -8,6 +8,9 @@ #include #include +#include +#include +#include using namespace Slic3r; using Catch::Matchers::WithinAbs; diff --git a/tests/fff_print/test_gcodewriter.cpp b/tests/fff_print/test_gcodewriter.cpp index fae002b35a..b0f36c33d9 100644 --- a/tests/fff_print/test_gcodewriter.cpp +++ b/tests/fff_print/test_gcodewriter.cpp @@ -8,6 +8,7 @@ #include "libslic3r/Point.hpp" #include "libslic3r/Config.hpp" #include "libslic3r/Extruder.hpp" +#include "libslic3r/Geometry.hpp" #include "libslic3r/libslic3r.h" #include #include diff --git a/tests/fff_print/test_precise_seam.cpp b/tests/fff_print/test_precise_seam.cpp index 5012ca0a00..d5e2709cdd 100644 --- a/tests/fff_print/test_precise_seam.cpp +++ b/tests/fff_print/test_precise_seam.cpp @@ -23,9 +23,11 @@ #include "libslic3r/GCode/SeamPlacer.hpp" #include "libslic3r/GCodeReader.hpp" #include "libslic3r/TriangleMesh.hpp" +#include "libslic3r/ExPolygon.hpp" #include #include #include +#include using namespace Slic3r; diff --git a/tests/fff_print/test_skirt_brim.cpp b/tests/fff_print/test_skirt_brim.cpp index 4c7cb8d391..537aac8528 100644 --- a/tests/fff_print/test_skirt_brim.cpp +++ b/tests/fff_print/test_skirt_brim.cpp @@ -30,6 +30,9 @@ #include #include #include +#include +#include +#include #include "test_helpers.hpp" // get access to init_print, etc #include "libslic3r/BoundingBox.hpp" @@ -38,6 +41,10 @@ #include "libslic3r/ExtrusionEntityCollection.hpp" #include "libslic3r/ObjectID.hpp" #include "libslic3r/Print.hpp" +#include "libslic3r/BeltBrim.hpp" +#include "libslic3r/Config.hpp" +#include "libslic3r/ExtrusionEntity.hpp" +#include "libslic3r/TriangleMesh.hpp" using namespace Slic3r::Test; using namespace Slic3r; diff --git a/tests/libslic3r/test_arrange.cpp b/tests/libslic3r/test_arrange.cpp index c85aa625fe..d8c675954a 100644 --- a/tests/libslic3r/test_arrange.cpp +++ b/tests/libslic3r/test_arrange.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include "libslic3r/Arrange.hpp" #include "libslic3r/BoundingBox.hpp" #include "libslic3r/ClipperUtils.hpp" diff --git a/tests/libslic3r/test_belt_brim.cpp b/tests/libslic3r/test_belt_brim.cpp index d5e99dafaa..05c650a809 100644 --- a/tests/libslic3r/test_belt_brim.cpp +++ b/tests/libslic3r/test_belt_brim.cpp @@ -1,9 +1,21 @@ #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include "libslic3r/BeltBrim.hpp" #include "libslic3r/BoundingBox.hpp" #include "libslic3r/ClipperUtils.hpp" #include "libslic3r/ExPolygon.hpp" +#include "libslic3r/Polygon.hpp" +#include "libslic3r/libslic3r.h" using namespace Slic3r; From 61a0db4a87c93f0ad8d6b4772331b51eca3f5da0 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Mon, 5 Oct 2026 23:06:01 -0500 Subject: [PATCH 189/224] Belt printer: address the review on #14394 Code review items (raistlin7447): 1. PrintObject::slice() zeroes m_belt_min_z, m_belt_global_z_offset and m_belt_global_xy_correction before slicing. They were only written in belt mode, so a project switched to a normal printer, or whose tilt axis was set to None, kept the old offsets and shifted the adaptive infill octree and the organic support layers by them. 2. TreeModelVolumes shifts the support blockers into the raft-offset index space; a test now pins the index the blocker lands on. 3. The final-alignment clamp in libnest2d is opt-in (NfpPConfig::clamp_to_bin) and arrange sets it for belt printers only. Printers with an off-centre best_object_pos keep their alignment; a flat-bed test pins that. 4. The preview's belt view follows the loaded G-code, not the selected printer: GCodeProcessor carries the file's belt keys (and, for a belt file, its bed) into export_config_for_render(), and GCodeViewer enables the belt view from the header tilt. 5. belt_shift_layer_grid() also shifts the cached belt floor and the global Z offset, so a support-only or brim-only change after the purge-prism snap matches a fresh slice. 6. update_print_fff_config() resets raft_layers and draft_shield on a belt printer instead of only greying out the fields Print::validate() rejects. 7. GCodeWriter takes a first-layer point test instead of the FirstLayerPlane; GCode installs one that measures from the belt surface, like its extrusions, so the first-layer travel speed and the second-layer temperature change no longer depend on the gcode_remap_* convention. 8. belt_brim_clip_leading_edge() is exported and called by both the generator and the test. 9. Both phong.vs shaders use slope.up_direction for the overhang highlight. The pre-slice and G-code axis remaps are gated on belt_printer through BeltTransformPipeline::axis_remap_enabled(), so belt keys left in a profile cannot change a non-belt print. Tests requested in the review: belt-only keys at non-default values leave non-belt G-code unchanged; switching a sliced project from belt to non-belt (and tilt axis None) matches a fresh slice; a support-only change on a belt purge print matches a fresh slice; non-belt start G-code moves keep the first-layer Z in the processor; the belt brim's segment count catches a band emitted twice. The belt-to-non-belt test exposed an unrelated gap: invalidate_step(posSlice) re-invalidated posSupportMaterial but not posSimplifySupportPath, so after any re-slice the regenerated support paths were exported unsimplified. posSimplifySupportPath is now in that list. Co-Authored-By: Claude Fable 5.1 --- .../include/libnest2d/placers/nfpplacer.hpp | 21 +- resources/shaders/110/phong.vs | 5 +- resources/shaders/140/phong.vs | 5 +- src/libslic3r/Arrange.cpp | 4 +- src/libslic3r/BeltBrim.cpp | 32 +- src/libslic3r/BeltBrim.hpp | 8 + src/libslic3r/BeltPurge.cpp | 9 + src/libslic3r/BeltTransform.cpp | 8 +- src/libslic3r/BeltTransform.hpp | 19 +- src/libslic3r/GCode.cpp | 76 ++++- src/libslic3r/GCode.hpp | 3 + src/libslic3r/GCode/GCodeProcessor.cpp | 28 ++ src/libslic3r/GCode/GCodeProcessor.hpp | 4 + src/libslic3r/GCodeWriter.cpp | 5 +- src/libslic3r/GCodeWriter.hpp | 38 ++- src/libslic3r/PrintObject.cpp | 5 +- src/libslic3r/PrintObjectSlice.cpp | 9 + src/slic3r/GUI/ConfigManipulation.cpp | 25 ++ src/slic3r/GUI/GCodeViewer.cpp | 13 +- tests/fff_print/test_gcode_processor.cpp | 46 +++ tests/fff_print/test_print.cpp | 286 ++++++++++++++++++ tests/fff_print/test_skirt_brim.cpp | 54 ++++ tests/libslic3r/test_arrange.cpp | 28 +- tests/libslic3r/test_belt_brim.cpp | 44 +-- 24 files changed, 680 insertions(+), 95 deletions(-) diff --git a/deps_src/libnest2d/include/libnest2d/placers/nfpplacer.hpp b/deps_src/libnest2d/include/libnest2d/placers/nfpplacer.hpp index cf66c4c97e..62d231e375 100644 --- a/deps_src/libnest2d/include/libnest2d/placers/nfpplacer.hpp +++ b/deps_src/libnest2d/include/libnest2d/placers/nfpplacer.hpp @@ -88,6 +88,18 @@ struct NfpPConfig { */ bool explore_holes = false; + /** + * @brief Keep the final pile on the bin. + * + * The final alignment centres the pile on the alignment target. A target + * near an edge (a belt printer starts its parts at the leading end of the + * belt) would push part of a pile that is larger than the room around that + * point off the bed; with this set the pile stops at the edge instead, and a + * pile that does not fit along an axis is centred on it. Off by default, so + * the alignment of every other printer is unchanged. + */ + bool clamp_to_bin = false; + /** * @brief If true, use all CPUs available. Run on a single core otherwise. */ @@ -1113,12 +1125,9 @@ private: auto d = cb - ci; - // Keep the pile on the bin. A target near an edge (a belt printer starts its parts - // at the leading end of the belt) would otherwise centre a pile that is larger than - // the room around that point on it and push part of the pile off the bed. The pile - // stops at the edge instead; the items' boxes carry their inflation, which is the - // margin left there. A pile that does not fit along an axis is centred on it. - { + // Keep the pile on the bin (see Config::clamp_to_bin). The items' boxes carry + // their inflation, which is the margin left at the edge. + if (config_.clamp_to_bin) { auto on_bin = [](Coord lo, Coord hi, Coord bin_lo, Coord bin_hi, Coord shift) { if (hi - lo >= bin_hi - bin_lo) return (bin_lo + bin_hi) / 2 - (lo + hi) / 2; diff --git a/resources/shaders/110/phong.vs b/resources/shaders/110/phong.vs index 10d36e233f..d857147c28 100644 --- a/resources/shaders/110/phong.vs +++ b/resources/shaders/110/phong.vs @@ -7,6 +7,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform mat4 view_model_matrix; @@ -46,8 +47,8 @@ void main() // Point in homogenous coordinates. world_pos = volume_world_matrix * vec4(v_position, 1.0); - // z component of normal vector in world coordinate used for slope shading - world_normal_z = slope.actived ? (normalize(slope.volume_world_normal_matrix * v_normal)).z : 0.0; + // dot product of world normal with up direction, used for slope shading + world_normal_z = slope.actived ? dot(normalize(slope.volume_world_normal_matrix * v_normal), slope.up_direction) : 0.0; gl_Position = projection_matrix * position; if (is_outline) { diff --git a/resources/shaders/140/phong.vs b/resources/shaders/140/phong.vs index c7570edb95..7120b62c66 100644 --- a/resources/shaders/140/phong.vs +++ b/resources/shaders/140/phong.vs @@ -7,6 +7,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform mat4 view_model_matrix; @@ -46,8 +47,8 @@ void main() // Point in homogenous coordinates. world_pos = volume_world_matrix * vec4(v_position, 1.0); - // z component of normal vector in world coordinate used for slope shading - world_normal_z = slope.actived ? (normalize(slope.volume_world_normal_matrix * v_normal)).z : 0.0; + // dot product of world normal with up direction, used for slope shading + world_normal_z = slope.actived ? dot(normalize(slope.volume_world_normal_matrix * v_normal), slope.up_direction) : 0.0; gl_Position = projection_matrix * position; if (is_outline) { diff --git a/src/libslic3r/Arrange.cpp b/src/libslic3r/Arrange.cpp index 8c224af515..79d93f7909 100644 --- a/src/libslic3r/Arrange.cpp +++ b/src/libslic3r/Arrange.cpp @@ -301,10 +301,12 @@ template void fill_config(PConf& pcfg, const ArrangeParams ¶ms) { if (params.is_belt) { - // Pack from the end of the belt that prints first. + // Pack from the end of the belt that prints first, and keep the pile on the + // bed when it is larger than the room around that end. pcfg.starting_point = !params.belt_reversed ? PConf::Alignment::BOTTOM_LEFT : params.belt_axis == 1 ? PConf::Alignment::TOP_LEFT : PConf::Alignment::BOTTOM_RIGHT; + pcfg.clamp_to_bin = true; } else if (params.is_seq_print) { // Start placing the items from the center of the print bed diff --git a/src/libslic3r/BeltBrim.cpp b/src/libslic3r/BeltBrim.cpp index d77f163467..3f2507f24e 100644 --- a/src/libslic3r/BeltBrim.cpp +++ b/src/libslic3r/BeltBrim.cpp @@ -230,6 +230,19 @@ static Polygon band_box(const BoundingBox &bounds, int from_axis, coordf_t u_lo, return poly; } +ExPolygons belt_brim_clip_leading_edge(const ExPolygons ®ion, const BeltBrimFrame &frame, coordf_t u_cut) +{ + if (region.empty()) + return region; + BoundingBox keep_bb = get_extents(region); + keep_bb.offset(scale_(1.)); + const bool low_side = frame.downhill_sign() < 0; // downhill is -u + const Polygon keep = band_box(keep_bb, frame.from_axis, + low_side ? unscale(frame.from_axis == 0 ? keep_bb.min.x() : keep_bb.min.y()) : u_cut, + low_side ? u_cut : unscale(frame.from_axis == 0 ? keep_bb.max.x() : keep_bb.max.y())); + return keep.empty() ? ExPolygons{} : intersection_ex(region, Polygons{ keep }); +} + // Everything the per-band line generator needs, gathered once per object. struct BeltBrimContext { @@ -476,20 +489,11 @@ void make_belt_brim(PrintObject &object) width, gap, leading, lateral, bc.frame), bc.frame); - if (bt == btLeadingEdgeOnly && ! bc.region.empty()) { - // Keep only what lies at or downhill of the object's FIRST contact with the - // belt, so the part is supported as it lands and nothing is printed alongside - // it afterwards. The cut is the uphill edge of the first layer's contact band: - // everything past it belongs to later contacts. - const coordf_t u_cut = bc.ctx.cutoff_u(object.layers().front()->print_z); - BoundingBox keep_bb = get_extents(bc.region); - keep_bb.offset(scale_(1.)); - const bool low_side = bc.frame.shear > 0.; // downhill is -u - const Polygon keep = band_box(keep_bb, bc.frame.from_axis, - low_side ? unscale(bc.frame.from_axis == 0 ? keep_bb.min.x() : keep_bb.min.y()) : u_cut, - low_side ? u_cut : unscale(bc.frame.from_axis == 0 ? keep_bb.max.x() : keep_bb.max.y())); - bc.region = keep.empty() ? ExPolygons{} : intersection_ex(bc.region, Polygons{ keep }); - } + if (bt == btLeadingEdgeOnly && ! bc.region.empty()) + // The cut is the uphill edge of the first layer's contact band: everything + // past it belongs to later contacts. + bc.region = belt_brim_clip_leading_edge(bc.region, bc.frame, + bc.ctx.cutoff_u(object.layers().front()->print_z)); if (bc.region.empty()) return; diff --git a/src/libslic3r/BeltBrim.hpp b/src/libslic3r/BeltBrim.hpp index ac4986c311..3f4fcc97de 100644 --- a/src/libslic3r/BeltBrim.hpp +++ b/src/libslic3r/BeltBrim.hpp @@ -91,6 +91,14 @@ inline ExPolygons belt_unflatten(const ExPolygons &src, const BeltBrimFrame &fra // t - not when it is wider in its narrowest Euclidean direction. ExPolygons sweep_ex(const ExPolygons &src, const Point &t); +// "Leading edge only": keep the part of a brim region (unflattened, slicing XY) +// at or downhill of the object's first contact with the belt, so the part is +// supported as it lands and nothing is printed alongside it afterwards. `u_cut` +// is the uphill edge of the first layer's contact band along `frame.from_axis`, +// in mm (BeltFloorContext::cutoff_u of the first layer); downhill is the side +// `frame.downhill_sign()` points to. +ExPolygons belt_brim_clip_leading_edge(const ExPolygons ®ion, const BeltBrimFrame &frame, coordf_t u_cut); + // Brim region for one already-flattened belt footprint. All lengths are scaled // and measured in the flattened (true on-belt) metric. // diff --git a/src/libslic3r/BeltPurge.cpp b/src/libslic3r/BeltPurge.cpp index 3b5c5571a6..5fb925f9aa 100644 --- a/src/libslic3r/BeltPurge.cpp +++ b/src/libslic3r/BeltPurge.cpp @@ -333,6 +333,15 @@ void PrintObject::belt_shift_layer_grid(double delta) for (BeltBrimBand &band : m_belt_brim_prologue) band.print_z += delta; m_slicing_params.belt_floor_z_shift += delta; + // The grid stays shifted across a support-only or brim-only change (posSlice does + // not rerun), so everything slice() derived from it has to follow: the cached floor + // that update_slicing_parameters() restores, and the global offset the organic + // support layers and the adaptive infill octree are placed with. Left alone, the + // next alignment finds a delta of 0 and the floor and the supports sit up to half + // a layer off the grid, unlike a fresh slice. + if (m_belt_floor_z_shift_cache_valid) + m_belt_floor_z_shift_cached += delta; + m_belt_global_z_offset += delta; } // Belt mode: drop layers strictly above z (used to cancel the purge prism early diff --git a/src/libslic3r/BeltTransform.cpp b/src/libslic3r/BeltTransform.cpp index 0a59507ff2..d1ea31d33c 100644 --- a/src/libslic3r/BeltTransform.cpp +++ b/src/libslic3r/BeltTransform.cpp @@ -96,15 +96,13 @@ Transform3d BeltTransformPipeline::build_forward_transform(const PrintConfig &co 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); - if (pre_rx == int(RemapAxis::PosX) && - pre_ry == int(RemapAxis::PosY) && - pre_rz == int(RemapAxis::PosZ)) - return bb; // Identity remap. - auto remap_coord = [](int r, const Vec3d &v) -> double { int axis = r % 3; if (r < 3) return v[axis]; diff --git a/src/libslic3r/BeltTransform.hpp b/src/libslic3r/BeltTransform.hpp index 2fe725e1df..59669fe36a 100644 --- a/src/libslic3r/BeltTransform.hpp +++ b/src/libslic3r/BeltTransform.hpp @@ -47,16 +47,31 @@ 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. + 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("belt_printer"); + return opt != nullptr && opt->value; + } + static bool has_preslice_remap(const PrintConfig &config) { - return int(config.preslice_remap_x.value) != int(RemapAxis::PosX) || + 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); + 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>(key); return opt ? int(opt->value) : 0; diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 7fefed0f87..690c4e99ac 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -107,6 +107,7 @@ #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) @@ -3133,15 +3134,18 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato this->init_belt_writer(print); m_writer.set_is_bbl_machine(is_bbl_printers); - // Standalone axis remap (works with or without belt mode). - // Sync the writer's remap state to the current export UNCONDITIONALLY — even at - // the identity mapping (0,1,2) — so a reused writer never retains a stale - // non-identity mapping from a prior export. has_axis_remap() returns false at - // identity, so identity/default output stays unchanged. + // G-code axis remap. Only belt printers get one (see + // BeltTransformPipeline::axis_remap_enabled): a remap left in a profile must + // not change a non-belt print. Sync the writer's remap state to the current + // export UNCONDITIONALLY — even at the identity mapping (0,1,2) — so a reused + // writer never retains a stale non-identity mapping from a prior export. + // has_axis_remap() returns false at identity, so identity/default output stays + // unchanged. { - int rx = int(print.config().gcode_remap_x.value); - int ry = int(print.config().gcode_remap_y.value); - int rz = int(print.config().gcode_remap_z.value); + const bool remap = BeltTransformPipeline::axis_remap_enabled(print.config()); + int rx = remap ? int(print.config().gcode_remap_x.value) : int(RemapAxis::PosX); + int ry = remap ? int(print.config().gcode_remap_y.value) : int(RemapAxis::PosY); + int rz = remap ? int(print.config().gcode_remap_z.value) : int(RemapAxis::PosZ); m_writer.set_axis_remap(rx, ry, rz); BoundingBoxf bbox_bed(print.config().printable_area.values); m_writer.set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(), @@ -3153,13 +3157,21 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // 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(print.config()); - // Belt writers only: the plane also switches travel-speed selection to be - // per-point (see GCodeWriter::uses_pointwise_travel_speed()), which must not - // change for non-belt printers. + // 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. if (print.config().belt_printer.value) { - m_writer.set_first_layer_plane( - m_first_layer_plane.get(), - print.config().initial_layer_print_height.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(); + return this->on_first_layer(Vec3d(point_logical.x() - m_origin.x() + extruder_offset.x(), + point_logical.y() - m_origin.y() + extruder_offset.y(), + point_logical.z())); + }); } // How many times will be change_layer() called? @@ -6295,7 +6307,11 @@ LayerResult GCode::process_layer( // fall back to the legacy `!first_layer` predicate so behavior is // bit-identical to the pre-feature path. bool past_first_layer_band = !first_layer; - if (m_first_layer_plane && m_first_layer_plane->is_active()) { + 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. + 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 @@ -10617,6 +10633,36 @@ std::string GCode::set_object_info(Print *print) { return gcode.str(); } +// 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. +int GCode::belt_layer_past_first_layer_band(const Layer *object_layer) const +{ + if (object_layer == nullptr) + return -1; + // The belt surface is linear in the sliced XY, so a bbox's lowest point above + // it is at one of its corners. + double min_height = std::numeric_limits::max(); + bool known = false; + for (const BoundingBox &bb : object_layer->lslices_bboxes) { + const double xs[2] = { unscale(bb.min.x()), unscale(bb.max.x()) }; + const double ys[2] = { unscale(bb.min.y()), unscale(bb.max.y()) }; + for (double x : xs) + for (double y : ys) { + double h; + if (! this->belt_height_above_floor(Vec3d(x, y, object_layer->print_z), h)) + return -1; + known = true; + min_height = std::min(min_height, h); + } + } + if (! known) + return -1; + return min_height >= this->first_layer_band_mm() - EPSILON ? 1 : 0; +} + bool GCode::belt_height_above_floor(const Vec3d &point_slicing_mm, double &height_mm) const { // The owning object, which is what carries the belt description. During diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 3a08ffccec..c5268705c3 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -993,6 +993,9 @@ protected: // first-layer. Measuring against the belt itself is 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. + int belt_layer_past_first_layer_band(const Layer *object_layer) const; int layer_id() const { if (m_layer == nullptr) return -1; diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 1832e24a84..fa418e1b62 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -3214,6 +3214,33 @@ void GCodeProcessor::apply_config(const DynamicPrintConfig& config) { m_parser.apply_config(config); + // Belt printer: remember the file's belt keys for export_config_for_render(). The + // config block lists belt_printer for every printer, so a non-belt file loaded while + // a belt printer is selected switches the preview's belt view off, and a belt file + // loaded on another printer brings its own tilt, remaps and bed along. + m_belt_render_config.clear(); + { + const auto *belt = config.option("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_frame_tilt_decouple", "belt_frame_tilt_angle", + }; + for (const char *key : belt_keys) + if (const ConfigOption *opt = config.option(key); opt != nullptr) + m_belt_render_config.set_key_value(key, opt->clone()); + // The Rev remaps mirror inside the build volume, so the designed view needs + // the bed the file was sliced for. Only a belt file may override it. + static const char *bed_keys[] = { "printable_area", "printable_height" }; + if (belt->value) + for (const char *key : bed_keys) + if (const ConfigOption *opt = config.option(key); opt != nullptr) + m_belt_render_config.set_key_value(key, opt->clone()); + } + } + //BBS const ConfigOptionFloatsNullable* nozzle_volume = config.option("nozzle_volume"); if (nozzle_volume != nullptr) { @@ -3738,6 +3765,7 @@ DynamicConfig GCodeProcessor::export_config_for_render() const config.set_key_value("filament_is_support", new ConfigOptionBools(m_parser.get_config().filament_is_support.values)); config.set_key_value("filament_type", new ConfigOptionStrings(m_parser.get_config().filament_type.values)); config.set_key_value("filament_map", new ConfigOptionInts(m_parser.get_config().filament_map.values)); + config.apply(m_belt_render_config); return config; } diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index 09dd99816e..a5c5ce7914 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -1105,6 +1105,10 @@ class Print; private: CommandProcessor m_command_processor; GCodeReader m_parser; + // Belt printer: the belt keys of the loaded file's config block (plus the bed they + // are relative to), handed to the preview through export_config_for_render() so the + // belt view and its back-transform follow the file, not the selected printer. + DynamicConfig m_belt_render_config; EUnits m_units; EPositioningType m_global_positioning_type; EPositioningType m_e_local_positioning_type; diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index a6e81dfe1e..6176b6693c 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -1,5 +1,4 @@ #include "GCodeWriter.hpp" -#include "FirstLayerPlane.hpp" #include "Config.hpp" #include "Extruder.hpp" #include "Geometry.hpp" @@ -57,8 +56,8 @@ bool GCodeWriter::must_skip_lift_now() const bool GCodeWriter::point_on_first_layer(const Vec3d &point_logical) const { - if (m_first_layer_plane && m_first_layer_plane->is_active()) - return m_first_layer_plane->is_first_layer(point_logical, m_first_layer_thickness_mm); + if (m_first_layer_point_test) + return m_first_layer_point_test(point_logical); return m_is_first_layer; } diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index 224ad4c1e4..88be72a6f1 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -10,6 +10,8 @@ #include #include #include +#include +#include #include "Extruder.hpp" #include "Point.hpp" #include "Polygon.hpp" @@ -20,7 +22,6 @@ namespace Slic3r { -class FirstLayerPlane; class GCodeWriter { public: @@ -168,13 +169,13 @@ public: void set_kinematics(std::unique_ptr kinematics); const MachineKinematics& kinematics() const { return *m_kinematics; } - // First-layer plane evaluator. When set to an active plane, travel speed - // selection consults the plane per destination point instead of the - // layer-coarse m_is_first_layer flag. Borrowed pointer; lifetime is owned - // by GCode, which constructs the plane after the writer exists -- so this is - // deliberately a setter and not a constructor argument. - void set_first_layer_plane(const FirstLayerPlane *plane, double first_layer_height_mm) - { m_first_layer_plane = plane; m_first_layer_thickness_mm = first_layer_height_mm; } + // Per-point first-layer test. When set, travel speed selection asks it per + // destination point (in the writer's logical placed frame) instead of using + // the layer-coarse m_is_first_layer flag. GCode installs it on belt printers + // with the same test its extrusions use (GCode::on_first_layer(point)), so a + // travel is judged against the belt surface exactly as the path it leads to. + using FirstLayerPointTest = std::function; + void set_first_layer_point_test(FirstLayerPointTest test) { m_first_layer_point_test = std::move(test); } // Force every lift to a plain vertical lift. Spiral and slope lifts compute // their slope in the logical frame and do not account for a machine mapping @@ -197,9 +198,8 @@ protected: std::string _travel_to_z(double z, const std::string &comment); - // Whether a destination gets first-layer treatment. With an active plane - // evaluator, distance from the plane decides; otherwise the layer-coarse - // m_is_first_layer flag does. + // Whether a destination gets first-layer treatment. With a point test + // installed it decides; otherwise the layer-coarse m_is_first_layer flag does. bool point_on_first_layer(const Vec3d &point_logical) const; // True when a lift must be skipped because this mapping would emit the @@ -207,17 +207,15 @@ protected: bool must_skip_lift_now() const; // True when travel speed is selected per destination point rather than per - // layer. Set for writers that install a first-layer plane. The historical - // path emits the raw configured travel speed in the final branch of - // travel_to_xyz(), ignoring the first-layer selection computed at the top of - // that function; a plane-driven writer uses the first-layer-aware value - // throughout. Both are preserved exactly -- unifying them would change + // layer. Set for writers that install a first-layer point test. The + // historical path emits the raw configured travel speed in the final branch + // of travel_to_xyz(), ignoring the first-layer selection computed at the top + // of that function; a point-test-driven writer uses the first-layer-aware + // value throughout. Both are preserved exactly -- unifying them would change // emitted feedrates and belongs in its own commit. - bool uses_pointwise_travel_speed() const { return m_first_layer_plane != nullptr; } + bool uses_pointwise_travel_speed() const { return bool(m_first_layer_point_test); } - // Borrowed; null = inactive. - const FirstLayerPlane *m_first_layer_plane = nullptr; - double m_first_layer_thickness_mm = 0.; + FirstLayerPointTest m_first_layer_point_test; bool m_force_normal_lift = false; // The machine frame mapping. Owns the axis-remap state that used to live diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 417b472bd0..748bed4a1f 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1777,7 +1777,10 @@ bool PrintObject::invalidate_step(PrintObjectStep step) invalidated |= this->invalidate_steps({ posIroning, posContouring, posSimplifyInfill }); invalidated |= m_print->invalidate_steps({ psSkirtBrim }); } else if (step == posSlice) { - invalidated |= this->invalidate_steps({ posPerimeters, posPrepareInfill, posInfill, posIroning, posContouring, posSupportMaterial, posSimplifyPath, posSimplifyInfill }); + // posSimplifySupportPath is listed with posSupportMaterial: invalidate_steps() does not + // propagate, so without it a re-slice regenerated the supports but kept the step done, + // and the new support paths were exported unsimplified, unlike a fresh slice. + invalidated |= this->invalidate_steps({ posPerimeters, posPrepareInfill, posInfill, posIroning, posContouring, posSupportMaterial, posSimplifyPath, posSimplifyInfill, posSimplifySupportPath }); invalidated |= m_print->invalidate_steps({ psSkirtBrim }); m_slicing_params.valid = false; // The exact belt_floor_z_shift is recomputed when slice() runs again. diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 8795cdc444..b384a28ef3 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -895,6 +895,15 @@ void PrintObject::slice() this->update_layer_height_profile(*this->model_object(), m_slicing_params, layer_height_profile); m_print->throw_if_canceled(); m_typed_slices = false; + // The belt state below is only written while belt mode is on (and the min-Z + // lift only when there is a rotation or remap). Start every slice from zero, + // or a project switched from a belt printer to a normal one, or whose tilt + // axis was set to None, keeps the previous offsets: the adaptive infill octree + // and the organic support layers (PrintObject.cpp) would still be shifted by + // them. + m_belt_min_z = 0.; + m_belt_global_z_offset = 0.; + m_belt_global_xy_correction = Vec2d::Zero(); this->clear_layers(); m_layers = new_layers(this, generate_object_layers(m_slicing_params, layer_height_profile, m_config.precise_z_height.value)); this->slice_volumes(); diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 0d1c141fa8..9c4a03ca47 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -353,6 +353,31 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con bool is_object_config = (!is_global_config && !is_plate_config); + // Belt printer: a raft and a draft shield are refused by Print::validate(), and + // the fields that would clear them are greyed out in belt mode, so a preset that + // carries either could not be sliced at all. Reset them instead of only disabling + // the fields. + if (GUI::wxGetApp().preset_bundle != nullptr) { + const auto *belt_opt = GUI::wxGetApp().preset_bundle->printers.get_edited_preset().config.option("belt_printer"); + const auto *raft_opt = config->option("raft_layers"); + const auto *shield_opt = config->option>("draft_shield"); + const bool has_raft = raft_opt != nullptr && raft_opt->value > 0; + const bool has_shield = shield_opt != nullptr && shield_opt->value != dsDisabled; + if (belt_opt != nullptr && belt_opt->value && (has_raft || has_shield)) { + const wxString msg_text = _(L("Raft and draft shield are not available on belt printers.\nThey have been disabled.")); + MessageDialog dialog(m_msg_dlg_parent, msg_text, "", wxICON_WARNING | wxOK); + DynamicPrintConfig new_conf = *config; + is_msg_dlg_already_exist = true; + dialog.ShowModal(); + if (has_raft) + new_conf.set_key_value("raft_layers", new ConfigOptionInt(0)); + if (has_shield) + new_conf.set_key_value("draft_shield", new ConfigOptionEnum(dsDisabled)); + apply(config, &new_conf); + is_msg_dlg_already_exist = false; + } + } + // layer_height shouldn't be equal to zero auto layer_height = config->opt_float("layer_height"); if (layer_height < EPSILON) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index cec7f9507e..b03e6b5f03 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1376,9 +1376,12 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const m_loaded_as_preview = false; // Belt printers: drive the designed/raw view UI (legend checkbox, hotkey B, canvas-toolbar - // menu item) from the loaded print. The tilt magnitude comes from the G-code header - // (gcode_result.belt_tilt_angle, abs of the slicing rotation). - m_belt_view_enabled = print.config().belt_printer.value; + // menu item) from the G-code itself. Only BeltGCode writes the belt header, so its tilt + // (gcode_result.belt_tilt_angle, abs of the slicing rotation) says whether this is belt + // G-code; the selected printer does not, for a file opened from disk. The back-transform + // below still reads print.config(), which Plater::load_gcode() fills from the file's own + // config block (GCodeProcessor::export_config_for_render). + m_belt_view_enabled = gcode_result.belt_tilt_angle > 0.f; m_belt_angle_deg = gcode_result.belt_tilt_angle; const bool current_top_layer_only = m_viewer.is_top_layer_only_view_range(); @@ -1394,7 +1397,7 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const // 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 = !print.config().belt_printer.value || m_last_belt_show_designed == m_belt_show_designed; + 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) { //BBS: add logs BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": the same id %1%, return directly, result %2% ") % m_last_result_id % (&gcode_result); @@ -1441,7 +1444,7 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const // 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). - const bool is_belt = print.config().belt_printer.value; + 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(); // Belt: move positions are stored as gcode_Z + belt_z_origin (the start G-code's diff --git a/tests/fff_print/test_gcode_processor.cpp b/tests/fff_print/test_gcode_processor.cpp index 2d3aab2909..d45d6534f7 100644 --- a/tests/fff_print/test_gcode_processor.cpp +++ b/tests/fff_print/test_gcode_processor.cpp @@ -6,6 +6,7 @@ #include "test_utils.hpp" +#include #include #include #include @@ -47,3 +48,48 @@ TEST_CASE("The belt header's angle marks G-code as belt G-code", "[GCodeProcesso "; CONFIG_BLOCK_START\n; belt_printer = 1\n; belt_slice_rotation_angle = -45\n; CONFIG_BLOCK_END\n"; CHECK_THAT(processed_belt_tilt(gcode), WithinAbs(45., 1e-6)); } + +TEST_CASE("Non-belt start G-code moves keep the first-layer Z in the processor", "[GCodeProcessor]") +{ + // The belt path (GCodeWriter tests: "start-gcode prepare-stage moves keep their real Z") + // stores the real Z of a move inside the start G-code. Every other printer must keep + // the historical behaviour: a prepare-stage move is pinned to the first-layer height + // so the preview does not draw the start sequence's travel. The gate is the belt + // header, so a file without one, whatever its config block says, takes this path. + struct BBLPrinterGuard { + bool prev = GCodeProcessor::s_IsBBLPrinter; + BBLPrinterGuard() { GCodeProcessor::s_IsBBLPrinter = false; } + ~BBLPrinterGuard() { GCodeProcessor::s_IsBBLPrinter = prev; } + } bbl_guard; + + const std::string gcode = + "G90\n" + "G21\n" + "M83\n" + ";TYPE:Custom\n" + "G1 E-1.5 F2100\n" + "G1 X45 Y0.3 Z50 F12000\n" // prepare-stage travel to a high Z + "G1 E1.5 F1800\n" + ";TYPE:Outer wall\n" + "G1 X46 Y0.3 Z50 E0.05\n" + "; CONFIG_BLOCK_START\n; belt_printer = 0\n; belt_slice_rotation_angle = 45\n; CONFIG_BLOCK_END\n"; + + GCodeProcessor processor; + FullPrintConfig config; + config.initial_layer_print_height.value = 0.3; + processor.apply_config(config); + processor.process_buffer(gcode); + const GCodeProcessorResult &result = processor.get_result(); + REQUIRE_THAT(result.belt_tilt_angle, WithinAbs(0., 1e-6)); + + size_t first_extrude = result.moves.size(); + for (size_t i = 0; i < result.moves.size(); ++i) + if (result.moves[i].type == EMoveType::Extrude) { first_extrude = i; break; } + REQUIRE(first_extrude < result.moves.size()); + REQUIRE(first_extrude > 0); + + // The extrusion keeps its real Z; the prepare-stage move before it is pinned to the + // first-layer height. + CHECK_THAT(result.moves[first_extrude].position.z(), WithinAbs(50., 1e-3)); + CHECK_THAT(result.moves[first_extrude - 1].position.z(), WithinAbs(0.3, 1e-3)); +} diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index a098a4a653..bdebf052b6 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -14,6 +14,7 @@ #include #include #include "libslic3r/PrintConfig.hpp" +#include "libslic3r/libslic3r.h" #include #include "libslic3r/Surface.hpp" #include "libslic3r/Config.hpp" @@ -26,6 +27,10 @@ #include "libslic3r/Print.hpp" #include "libslic3r/Layer.hpp" +#include "libslic3r/BuildVolume.hpp" +#include "libslic3r/Support/TreeModelVolumes.hpp" +#include "libslic3r/Support/TreeSupportCommon.hpp" +#include "libslic3r/Polygon.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/GCodeReader.hpp" #include "libslic3r/GCode/GCodeProcessor.hpp" @@ -969,3 +974,284 @@ TEST_CASE("Slicing errors are reported per object with the object's name", "[Pri CHECK(message.rfind("floating cube: ", 0) == 0); CHECK(message.find("empty first layer") != std::string::npos); } + +// --------------------------------------------------------------------------- +// Belt mode must be invisible when it is off, and must not leave traces behind. +// --------------------------------------------------------------------------- + +// Everything the slicer decided, without the lines that legitimately differ between +// two exports of the same print: comments (the config block lists every key, the +// header carries the export time) and the thumbnail blocks. +static std::string gcode_body(const std::string &gcode) +{ + std::string body; + std::istringstream in(gcode); + for (std::string line; std::getline(in, line); ) { + line.erase(std::min(line.size(), line.find(';'))); + while (! line.empty() && line.back() == ' ') + line.pop_back(); + if (! line.empty()) + body += line + '\n'; + } + return body; +} + +static DynamicPrintConfig belt_test_config() +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "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" }, + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "skirt_loops", 0 }, + { "z_hop", 0 }, + { "machine_start_gcode", "T[initial_tool]\n" }, + { "layer_change_gcode", "G92 E0\n" }, + }); + return config; +} + +TEST_CASE("Belt-only keys at non-default values leave non-belt G-code unchanged", "[Print][belt][Regression]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "z_hop", 0 }, + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "enable_support", 1 }, + { "support_type", "tree(auto)" }, + { "support_style", "organic" }, + { "sparse_infill_pattern", "adaptivecubic" }, + { "machine_start_gcode", "T[initial_tool]\n" }, + { "layer_change_gcode", "G92 E0\n" }, + }); + const std::string reference = gcode_body(slice({ TestMesh::overhang }, config)); + REQUIRE(! reference.empty()); + + // 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. + 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 }, + { "extra_brim_width", 5 }, + }); + CHECK(gcode_body(slice({ TestMesh::overhang }, config)) == reference); +} + +TEST_CASE("Switching a sliced project from belt to non-belt matches a fresh slice", "[Print][belt][Regression]") +{ + // The organic support layers and the adaptive infill octree are placed with the + // belt global Z offset, and the mesh with the belt min-Z lift. Both are only + // written while belt mode slices, so they used to survive a switch away from it. + DynamicPrintConfig flat = DynamicPrintConfig::full_print_config(); + flat.set_deserialize_strict({ + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "skirt_loops", 0 }, + { "z_hop", 0 }, + { "enable_support", 1 }, + { "support_type", "tree(auto)" }, + { "support_style", "organic" }, + { "sparse_infill_pattern", "adaptivecubic" }, + { "machine_start_gcode", "T[initial_tool]\n" }, + { "layer_change_gcode", "G92 E0\n" }, + }); + DynamicPrintConfig belt = belt_test_config(); + belt.set_deserialize_strict({ + { "enable_support", 1 }, + { "support_type", "tree(auto)" }, + { "support_style", "organic" }, + { "sparse_infill_pattern", "adaptivecubic" }, + }); + + // Both prints are placed with the belt config, so only the slicing history differs. + auto fresh_slice = [&](const DynamicPrintConfig &target) { + Print print; + Model model; + init_print({ TestMesh::overhang }, print, model, belt); + print.apply(model, target); + const std::string out = gcode(print); + return gcode_body(out); + }; + auto resliced = [&](const DynamicPrintConfig &target) { + Print print; + Model model; + init_print({ TestMesh::overhang }, print, model, belt); + REQUIRE(! gcode(print).empty()); + print.apply(model, target); + const std::string out = gcode(print); + return gcode_body(out); + }; + SECTION("belt printer to a flat-bed printer") { + CHECK(resliced(flat) == fresh_slice(flat)); + } + SECTION("belt tilt axis set to None") { + DynamicPrintConfig untilted = belt; + untilted.set_deserialize_strict({ { "belt_slice_rotation", "none" } }); + CHECK(resliced(untilted) == fresh_slice(untilted)); + } +} + +TEST_CASE("A support-only change on a belt purge print matches a fresh slice", "[Print][belt][PurgeTower][Regression]") +{ + // Snapping the purge prism onto the parts' layer grid shifts every object's layers by + // up to half a layer. A support-only change reruns support generation without + // reslicing, so the cached belt floor and the global Z offset have to carry the + // snap too, or the supports land on the pre-snap grid. + auto make_config = [](bool support) { + DynamicPrintConfig config = multifilament_config(2, { + { "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" }, + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "skirt_loops", 0 }, + { "z_hop", 0 }, + { "enable_belt_purge_tower", 1 }, + { "machine_start_gcode", "T[initial_tool]\n" }, + { "layer_change_gcode", "G92 E0\n" }, + }); + config.set_deserialize_strict({ + { "enable_support", support ? 1 : 0 }, + { "support_type", "tree(auto)" }, + { "support_style", "organic" }, + }); + return config; + }; + const std::vector> overrides { + { { "extruder", 1 } }, { { "extruder", 2 } }, + }; + auto build = [&](Print &print, Model &model, const DynamicPrintConfig &config) { + init_print(std::vector{ mesh(TestMesh::overhang), cube(20) }, print, model, config, &overrides); + model.objects.back()->config.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true)); + print.apply(model, config); + REQUIRE(print.has_belt_purge_tower()); + }; + + std::string fresh; + { + Print print; + Model model; + build(print, model, make_config(true)); + fresh = gcode_body(gcode(print)); + } + REQUIRE(! fresh.empty()); + + Print print; + Model model; + build(print, model, make_config(false)); + REQUIRE(! gcode(print).empty()); + // Support only: posSlice stays valid, posSupportMaterial reruns. + print.apply(model, make_config(true)); + CHECK(gcode_body(gcode(print)) == fresh); +} + +TEST_CASE("Organic tree supports place a support blocker at its own height above a raft", "[Print][Support][Regression]") +{ + // TreeModelVolumes consumes the support blockers in the same index space as the + // layer outlines, where object layer i sits at num_raft_layers + i, but + // slice_support_blockers() returns them in object-layer space. Without the shift + // every blocker lands num_raft_layers too low, so branches are kept out of the + // wrong layers and may pass through the blocked ones. + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "skirt_loops", 0 }, + { "enable_support", 1 }, + { "support_type", "tree(auto)" }, + { "support_style", "organic" }, + { "raft_layers", 3 }, + }); + Print print; + Model model; + init_print({ cube(20) }, print, model, config); + // A blocker floating beside the cube, 8 mm to 12 mm above the bed, so a collision at + // its centre can only come from the blocker itself (the part keeps its mesh + // coordinates in object space, hence the offset relative to the part). + ModelObject *object = model.objects.front(); + ModelVolume *blocker = object->add_volume(TriangleMesh(its_make_cube(6., 6., 4.))); + blocker->set_type(ModelVolumeType::SUPPORT_BLOCKER); + const Vec3d part_offset = object->volumes.front()->get_offset(); + blocker->set_offset(Vec3d(part_offset.x() + 20., part_offset.y(), 10.)); + print.apply(model, config); + print.set_status_silent(); + print.process(); + + const PrintObject &print_object = *print.objects().front(); + const std::vector bed = { { 0., 0. }, { 200., 0. }, { 200., 200. }, { 0., 200. } }; + const BuildVolume build_volume{ bed, print.config().printable_height.value, {}, {} }; + TreeSupport3D::TreeModelVolumes volumes{ print_object, build_volume, scaled(1.), scaled(0.5), 0, {} }; + + // The generator's raft layer count: the raft itself plus the gap layers up to the object. + const size_t num_raft = TreeSupport3D::TreeSupportSettings(TreeSupport3D::TreeSupportMeshGroupSettings(print_object), + print_object.slicing_parameters()).raft_layers.size(); + REQUIRE(num_raft >= 3); + // Object layers the blocker was sliced into (object-layer space, as the generator + // receives them). + const std::vector blockers = print_object.slice_support_blockers(); + size_t first = 0, last = 0; + bool found = false; + for (size_t i = 0; i < blockers.size(); ++ i) + if (! blockers[i].empty()) { + if (! found) { first = i; found = true; } + last = i; + } + REQUIRE(found); + REQUIRE(last - first > num_raft); + // The blocker's centre in the slicing frame (add_volume centred its mesh on its offset). + const Vec3d centre3 = print_object.trafo_sliced() * blocker->get_offset(); + const Point centre = Point::new_scale(centre3.x(), centre3.y()); + auto collides = [&](size_t tree_layer) { + for (const Polygon &poly : volumes.getCollision(0, TreeSupport3D::LayerIndex(tree_layer), false)) + if (poly.contains(centre)) + return true; + return false; + }; + // In TreeModelVolumes' index space the blocker lives at num_raft + object layer. + CHECK(collides(num_raft + first)); + CHECK(collides(num_raft + last)); + // The layers just below it, where an unshifted blocker would land, are free; the + // layers just above the unshifted range, which the blocker does occupy, are not. + CHECK_FALSE(collides(first)); + CHECK_FALSE(collides(first + num_raft - 1)); + CHECK(collides(last + 1)); + CHECK(collides(last + num_raft)); +} diff --git a/tests/fff_print/test_skirt_brim.cpp b/tests/fff_print/test_skirt_brim.cpp index 537aac8528..45086b9d2a 100644 --- a/tests/fff_print/test_skirt_brim.cpp +++ b/tests/fff_print/test_skirt_brim.cpp @@ -834,6 +834,60 @@ TEST_CASE("Belt brim on a single extruder emits every band once", "[SkirtBrim][b CHECK(belt_tools_for_role(gc, "brim") == std::set{ 0 }); // filament 1 -> tool 0 } +// Number of brim segments the belt brim generator produced for `object`: the lattice +// lines of every per-layer band plus the apron prologue. Each segment is written as one +// extruding move, so this is what a G-code count has to match. A pass count cannot see a +// band emitted twice back to back (two copies of the same band merge into one pass). +static long belt_brim_segments(const PrintObject &object) +{ + auto segments = [](const ExtrusionEntityCollection &fills) { + long n = 0; + for (const ExtrusionEntity *entity : fills.flatten().entities) + for (const Polyline &pl : entity->as_polylines()) + n += long(pl.size()) - 1; + return n; + }; + long n = 0; + for (const ExtrusionEntityCollection &band : object.belt_brim_by_layer()) + n += segments(band); + for (const BeltBrimBand &band : object.belt_brim_prologue()) + n += segments(band.fills); + return n; +} + +// Number of extruding moves in the G-code whose role is `role`. +static long role_segments(const std::string &gcode, const std::string &role) +{ + long n = 0; + GCodeReader reader; + reader.parse_buffer(gcode, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) { + if (line.extruding(self) && line.dist_XY(self) > EPSILON && line.comment().find(role) != std::string_view::npos) + ++ n; + }); + return n; +} + +TEST_CASE("Belt brim writes every generated segment exactly once", "[SkirtBrim][belt]") +{ + // The pass count above cannot tell one band from the same band twice in a row; the + // segment count can, so a brim band emitted twice back to back fails here. + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "brim_object_gap", 0 }, + { "leading_brim_length", 6 }, + }); + Print print; + Model model; + init_print({ cube(20) }, print, model, config); + const std::string gc = gcode(print); + + const long expected = belt_brim_segments(*print.objects().front()); + REQUIRE(expected > 100); + CHECK(role_segments(gc, "brim") == expected); +} + // B - multi extruder (wall filament id 2). Every belt-brim line must print on the object's // wall filament (index 2 -> tool 1), and the total number of passes must equal the // single-extruder baseline: no per-filament doubling. diff --git a/tests/libslic3r/test_arrange.cpp b/tests/libslic3r/test_arrange.cpp index d8c675954a..f4dad02808 100644 --- a/tests/libslic3r/test_arrange.cpp +++ b/tests/libslic3r/test_arrange.cpp @@ -279,12 +279,15 @@ TEST_CASE("Arrange aligns the pile to a custom center", "[Arrange]") // Centring a pile on a point that close to the edge pushed everything longer than the room // around it off the bed: four 90 mm parts on a 95 x 500 mm belt ended with one across the // edge and one outside, with 290 mm of belt free behind them. The pile stops at the edge. -TEST_CASE("Arrange keeps a pile aligned near an edge on the bed", "[Arrange]") +TEST_CASE("Arrange keeps a pile aligned near an edge on the bed", "[Arrange][belt]") { const BoundingBox belt = bed(95, 500); ArrangePolygons items = squares(4, 90.); ArrangeParams params = quiet_params(scaled(2.)); params.align_center = Vec2d(0.5, 0.05); + params.is_belt = true; + params.belt_axis = 1; + params.belt_tilt_slope = 1.f; arrange(items, belt, params); @@ -300,6 +303,29 @@ TEST_CASE("Arrange keeps a pile aligned near an edge on the bed", "[Arrange]") require_no_overlap(items); } +// The clamp is a belt feature. Printers whose best_object_pos is off-centre (the A1 mini +// and the H2 family) keep their final alignment: the pile is centred on that point, even +// when that puts part of it outside the bed. +TEST_CASE("Arrange leaves the final alignment of a flat bed unclamped", "[Arrange]") +{ + const BoundingBox bed_ = bed(95, 500); + ArrangePolygons items = squares(4, 90.); + ArrangeParams params = quiet_params(scaled(2.)); + params.align_center = Vec2d(0.5, 0.05); + + arrange(items, bed_, params); + + BoundingBox pile; + for (const ArrangePolygon &ap : items) { + REQUIRE(ap.bed_idx == 0); + pile.merge(ap.transformed_poly().contour.bounding_box()); + } + // Centred on the 5% mark of the bed's length, not pushed inside it. + CHECK_THAT(unscaled(pile.center().y()), Catch::Matchers::WithinAbs(0.05 * 500., 15.)); + CHECK(pile.min.y() < 0); + require_no_overlap(items); +} + // On a belt the parts print in belt order, so two colours that alternate along the // belt, or sit side by side, cost a filament change on every shared layer. Arrange // keeps each colour together: no part shares belt length with a part of another diff --git a/tests/libslic3r/test_belt_brim.cpp b/tests/libslic3r/test_belt_brim.cpp index 05c650a809..f6d794f939 100644 --- a/tests/libslic3r/test_belt_brim.cpp +++ b/tests/libslic3r/test_belt_brim.cpp @@ -324,11 +324,9 @@ SCENARIO("belt_brim_region builds an inner ring inside a hole", "[BeltBrim]") { } SCENARIO("Leading-edge-only retains the downhill half of the brim region", "[BeltBrim]") { - // BeltBrim.cpp ~445-458 clips the region to the object's first-contact band and keeps - // only what lies at or downhill of it. That clip is built with band_box(), which is - // file-static, so the rectangular half-band is reconstructed here with the SAME sign - // rule the code uses (low_side = shear > 0, i.e. downhill is -u) to pin the convention - // for both tilt signs. downhill_sign() is the exported accessor the flag mirrors. + // The production clip, belt_brim_clip_leading_edge(), keeps what lies at or downhill + // of the first-contact cut. downhill_sign() pins the convention for both tilt signs: + // low_side = shear > 0, i.e. downhill is -u. const coord_t mm = scale_(1.); const double shear = GENERATE(1.0, -1.0); DYNAMIC_SECTION("shear " << shear) { @@ -336,27 +334,37 @@ SCENARIO("Leading-edge-only retains the downhill half of the brim region", "[Bel CHECK((frame.downhill_sign() < 0) == (frame.shear > 0.)); const ExPolygons region { make_box(0, 0, 20 * mm, 20 * mm) }; // straddles the cut - const coord_t u_cut = 8 * mm; - const BoundingBox bb = get_extents(region); + const coordf_t u_cut = 8.; // mm - const bool low_side = frame.shear > 0.; - const coord_t lo = low_side ? bb.min.y() : u_cut; - const coord_t hi = low_side ? u_cut : bb.max.y(); - Polygon keep; - keep.points = { Point(bb.min.x(), lo), Point(bb.max.x(), lo), - Point(bb.max.x(), hi), Point(bb.min.x(), hi) }; - const ExPolygons kept = intersection_ex(region, Polygons{ keep }); + const ExPolygons kept = belt_brim_clip_leading_edge(region, frame, u_cut); REQUIRE(! kept.empty()); const BoundingBox kb = get_extents(kept); if (frame.shear > 0.) { // downhill is -u: nothing above the cut survives. - CHECK(kb.max.y() <= u_cut + 2); - CHECK(kb.min.y() < u_cut); + CHECK(kb.max.y() <= 8 * mm + 2); + CHECK(kb.min.y() < 8 * mm); } else { // downhill is +u: nothing below the cut survives. - CHECK(kb.min.y() >= u_cut - 2); - CHECK(kb.max.y() > u_cut); + CHECK(kb.min.y() >= 8 * mm - 2); + CHECK(kb.max.y() > 8 * mm); + } + // Half of the box is kept either way, and the full width across the belt. + CHECK_THAT(area(kept), Catch::Matchers::WithinRel(area(region) * (frame.shear > 0. ? 8. / 20. : 12. / 20.), 0.01)); + CHECK(kb.min.x() == 0); + CHECK(kb.max.x() == 20 * mm); + } + WHEN("the cut lies beyond the region") { + const BeltBrimFrame frame { 1.0, 1 }; + const ExPolygons region { make_box(0, 0, 20 * mm, 20 * mm) }; + THEN("a cut past the uphill end keeps everything") { + CHECK_THAT(area(belt_brim_clip_leading_edge(region, frame, 30.)), Catch::Matchers::WithinRel(area(region), 0.001)); + } + THEN("a cut before the downhill end keeps nothing") { + CHECK(belt_brim_clip_leading_edge(region, frame, -5.).empty()); + } + THEN("an empty region stays empty") { + CHECK(belt_brim_clip_leading_edge(ExPolygons{}, frame, 8.).empty()); } } } From f97aa6a5af07067cda0771ceddf8423e77975c7a Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Tue, 6 Oct 2026 00:12:59 -0500 Subject: [PATCH 190/224] Qualify Slic3r::Polygon in the blocker-index test tests/fff_print/test_print.cpp includes , so an unqualified Polygon in the new TreeModelVolumes test is ambiguous with GDI's Polygon() and fails the Windows x64 and arm64 builds on belt-printer. Co-Authored-By: Claude Fable 5.1 --- tests/fff_print/test_print.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index bdebf052b6..c0e5572b97 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -1240,7 +1240,7 @@ TEST_CASE("Organic tree supports place a support blocker at its own height above const Vec3d centre3 = print_object.trafo_sliced() * blocker->get_offset(); const Point centre = Point::new_scale(centre3.x(), centre3.y()); auto collides = [&](size_t tree_layer) { - for (const Polygon &poly : volumes.getCollision(0, TreeSupport3D::LayerIndex(tree_layer), false)) + for (const Slic3r::Polygon &poly : volumes.getCollision(0, TreeSupport3D::LayerIndex(tree_layer), false)) if (poly.contains(centre)) return true; return false; From 97f0b49a1eb930f82ef7c8665fa03fc44ed82d1a Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Tue, 6 Oct 2026 00:56:52 -0500 Subject: [PATCH 191/224] Belt printer: link every belt option to its wiki page Every option in the Belt printer and Machine frame transforms groups, the build plate tilt, the belt purge tower enable and the belt purge tower width get a wiki link, matching the pages added in OrcaSlicer/OrcaSlicer_WIKI#374. The two purge tower links that pointed at a page now point at their section. Co-Authored-By: Claude Fable 5.1 --- src/slic3r/GUI/Tab.cpp | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index fb2086c457..c64b4c8807 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -3113,7 +3113,7 @@ void TabPrint::build() optgroup->append_single_option_line("enable_tower_interface_cooldown_during_tower", "multimaterial_settings_prime_tower"); optgroup->append_single_option_line("prime_tower_enable_framework", "multimaterial_settings_prime_tower"); optgroup->append_single_option_line("prime_tower_width", "multimaterial_settings_prime_tower#width"); - optgroup->append_single_option_line("belt_purge_tower_width", "multimaterial_settings_prime_tower"); + optgroup->append_single_option_line("belt_purge_tower_width", "multimaterial_settings_prime_tower#belt-purge-tower-width"); optgroup->append_single_option_line("prime_volume", "multimaterial_settings_prime_tower"); optgroup->append_single_option_line("prime_tower_brim_width", "multimaterial_settings_prime_tower#brim-width"); optgroup->append_single_option_line("prime_tower_infill_gap", "multimaterial_settings_prime_tower"); @@ -5245,8 +5245,8 @@ void TabPrinter::build_fff() // Belt printer: dedicated section. Everything except the "Enable belt printing" // checkbox is hidden when belt_printer is off (see TabPrinter::toggle_options). auto belt_og = page->new_optgroup(L("Belt printer"), L"param_advanced"); - belt_og->append_single_option_line("belt_printer"); - belt_og->append_single_option_line("belt_printer_infinite_y"); + belt_og->append_single_option_line("belt_printer", "printer_basic_information_belt_printer#enable-belt-printing"); + belt_og->append_single_option_line("belt_printer_infinite_y", "printer_basic_information_belt_printer#infinite-y-axis"); // 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 @@ -5257,6 +5257,7 @@ void TabPrinter::build_fff() "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")); @@ -5267,14 +5268,15 @@ void TabPrinter::build_fff() 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"); - belt_og->append_single_option_line("gcode_back_transform"); + 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 " @@ -5282,6 +5284,7 @@ void TabPrinter::build_fff() "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")); @@ -5289,9 +5292,9 @@ void TabPrinter::build_fff() } // Support floor: split across lines so each setting's own mode controls // its visibility (floor_mode = Develop, floor_offset = Advanced, z_offset_mode = Expert). - belt_og->append_single_option_line("belt_support_floor_offset"); - belt_og->append_single_option_line("belt_support_z_offset_mode"); - belt_og->append_single_option_line("belt_support_floor_mode"); + 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 // Cartesian G-code into the printer's physical machine frame are derived @@ -5301,6 +5304,7 @@ void TabPrinter::build_fff() 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")); @@ -5312,6 +5316,7 @@ void TabPrinter::build_fff() "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); @@ -5362,8 +5367,8 @@ void TabPrinter::build_fff() optgroup->append_single_option_line("use_firmware_retraction", "printer_basic_information_advanced#use-firmware-retraction"); // optgroup->append_single_option_line("spaghetti_detector"); optgroup->append_single_option_line("time_cost", "printer_basic_information_advanced#time-cost"); - optgroup->append_single_option_line("build_plate_tilt_x"); - optgroup->append_single_option_line("build_plate_tilt_y"); + optgroup->append_single_option_line("build_plate_tilt_x", "printer_basic_information_advanced#build-plate-tilt"); + optgroup->append_single_option_line("build_plate_tilt_y", "printer_basic_information_advanced#build-plate-tilt"); optgroup = page->new_optgroup(L("Plugin Configuration"), L"param_gcode"); optgroup->append_single_option_line("printer_plugin_config_overrides"); @@ -5883,7 +5888,7 @@ if (is_marlin_flavor) // auto-generated purge prism; this is its enable (gated to belt printers // in toggle_options()). optgroup = page->new_optgroup(L("Belt purge tower"), "param_tower"); - optgroup->append_single_option_line("enable_belt_purge_tower", "printer_multimaterial_wipe_tower"); + optgroup->append_single_option_line("enable_belt_purge_tower", "printer_multimaterial_wipe_tower#belt-purge-tower"); optgroup = page->new_optgroup(L("Single extruder multi-material parameters"), "param_settings"); From 2867a701afd678a0ad032628493dd10b93b5ca0c Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Tue, 6 Oct 2026 13:59:28 -0500 Subject: [PATCH 192/224] Belt printer: declare up_direction in both phong fragment shaders #16195 added slope.up_direction to the SlopeDetection uniform struct of phong.vs (110 and 140) but not to phong.fs, so the two stages declared the uniform with different types and the program failed to link: "unable to load shaders: phong" at startup, and studio lighting / realistic phong rendering fell back. gouraud.fs already carries the member. Co-Authored-By: Claude Fable 5.1 --- resources/shaders/110/phong.fs | 1 + resources/shaders/140/phong.fs | 1 + 2 files changed, 2 insertions(+) diff --git a/resources/shaders/110/phong.fs b/resources/shaders/110/phong.fs index a47a24fdea..7d1e7bfe8c 100644 --- a/resources/shaders/110/phong.fs +++ b/resources/shaders/110/phong.fs @@ -41,6 +41,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform vec4 uniform_color; diff --git a/resources/shaders/140/phong.fs b/resources/shaders/140/phong.fs index 809621b76d..4cfae1e03e 100644 --- a/resources/shaders/140/phong.fs +++ b/resources/shaders/140/phong.fs @@ -44,6 +44,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform vec4 uniform_color; From 8039d4d2acae4ca22894db268da066a8d7548657 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Tue, 6 Oct 2026 18:14:34 -0500 Subject: [PATCH 193/224] Belt printer: retire the redundant and unused options Removed, with the keys added to handle_legacy()'s ignore list so saved profiles and 3MFs keep loading: - belt_slice_rotation_global and preslice_remap_global. Both were only consulted when belt_preslice_global ("Global mesh transforms") was off, which no profile does; belt_preslice_global is now the single global mode and is presumed on everywhere the old flags were ORed in (PrintObjectSlice, BeltBackTransform, BeltGCode, Print::process, PrintApply). The Belt tilt row is axis + angle only. - 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 (matrix columns vs remap_bbox rows). BeltTransformPipeline::build_preslice_remap, remap_bbox and has_preslice_remap are gone, the forward transform is the rotation, and the G-code header no longer carries the remap. - belt_support_z_offset_mode. Saved and invalidated steps, but no support generator read it. - first_layer_plane and first_layer_plane_offset, with FirstLayerPlane.cpp. On every shipped configuration the band is measured from the belt surface (GCode::belt_height_above_floor) and the evaluator was only reached for an explicit XY/YZ/XZ choice or a non-zero offset, which nobody set. first_layer_plane_thickness stays as the band unit, relabelled "First layer band thickness". UI: the Machine frame transforms group is five single-option rows (G-code remap X / Y / Z, Decouple machine-frame tilt, Machine-frame tilt angle; the angle row is shown only when decoupled) instead of two multi-column lines, and the remap fields carry full labels. Also carries the phong.fs struct fix from #16226 so the worktree build links its shaders. libslic3r_tests and fff_print_tests pass; clang-tidy diff check clean; orca_profile_tool.py check clean. Co-Authored-By: Claude Fable 5.1 --- .../Custom/machine/fdm_belt_common.json | 1 - .../IdeaFormer/machine/fdm_belt_common.json | 1 - .../Printcepts/machine/fdm_belt_common.json | 1 - resources/shaders/110/phong.fs | 1 + resources/shaders/140/phong.fs | 1 + src/libslic3r/BeltGCode.cpp | 31 +-- src/libslic3r/BeltSliceStrategy.cpp | 11 +- src/libslic3r/BeltTransform.cpp | 97 +------- src/libslic3r/BeltTransform.hpp | 58 +---- src/libslic3r/CMakeLists.txt | 2 - src/libslic3r/FirstLayerPlane.cpp | 229 ------------------ src/libslic3r/FirstLayerPlane.hpp | 76 ------ src/libslic3r/GCode.cpp | 72 +----- src/libslic3r/GCode.hpp | 43 +--- src/libslic3r/GCode/BeltBackTransform.cpp | 12 +- src/libslic3r/GCode/BeltBackTransform.hpp | 16 +- src/libslic3r/GCode/GCodeProcessor.cpp | 36 +-- src/libslic3r/GCode/GCodeProcessor.hpp | 6 - src/libslic3r/Preset.cpp | 7 +- src/libslic3r/Print.cpp | 22 +- src/libslic3r/Print.hpp | 7 +- src/libslic3r/PrintApply.cpp | 13 +- src/libslic3r/PrintConfig.cpp | 136 ++--------- src/libslic3r/PrintConfig.hpp | 38 --- src/libslic3r/PrintObject.cpp | 7 +- src/libslic3r/PrintObjectSlice.cpp | 47 +--- src/slic3r/GUI/GCodeViewer.cpp | 7 +- src/slic3r/GUI/Tab.cpp | 87 ++----- tests/fff_print/test_gcodewriter.cpp | 2 - tests/fff_print/test_precise_seam.cpp | 1 - tests/fff_print/test_print.cpp | 17 +- tests/fff_print/test_skirt_brim.cpp | 4 - 32 files changed, 125 insertions(+), 964 deletions(-) delete mode 100644 src/libslic3r/FirstLayerPlane.cpp delete mode 100644 src/libslic3r/FirstLayerPlane.hpp diff --git a/resources/profiles/Custom/machine/fdm_belt_common.json b/resources/profiles/Custom/machine/fdm_belt_common.json index 19765b8d4e..96b3138832 100644 --- a/resources/profiles/Custom/machine/fdm_belt_common.json +++ b/resources/profiles/Custom/machine/fdm_belt_common.json @@ -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", diff --git a/resources/profiles/IdeaFormer/machine/fdm_belt_common.json b/resources/profiles/IdeaFormer/machine/fdm_belt_common.json index 60ef096de0..aacd36e118 100644 --- a/resources/profiles/IdeaFormer/machine/fdm_belt_common.json +++ b/resources/profiles/IdeaFormer/machine/fdm_belt_common.json @@ -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", diff --git a/resources/profiles/Printcepts/machine/fdm_belt_common.json b/resources/profiles/Printcepts/machine/fdm_belt_common.json index 2a392b4f60..96d38ac31a 100644 --- a/resources/profiles/Printcepts/machine/fdm_belt_common.json +++ b/resources/profiles/Printcepts/machine/fdm_belt_common.json @@ -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", diff --git a/resources/shaders/110/phong.fs b/resources/shaders/110/phong.fs index a47a24fdea..7d1e7bfe8c 100644 --- a/resources/shaders/110/phong.fs +++ b/resources/shaders/110/phong.fs @@ -41,6 +41,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform vec4 uniform_color; diff --git a/resources/shaders/140/phong.fs b/resources/shaders/140/phong.fs index 809621b76d..4cfae1e03e 100644 --- a/resources/shaders/140/phong.fs +++ b/resources/shaders/140/phong.fs @@ -44,6 +44,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform vec4 uniform_color; diff --git a/src/libslic3r/BeltGCode.cpp b/src/libslic3r/BeltGCode.cpp index a78c0cbfa6..45b0728810 100644 --- a/src/libslic3r/BeltGCode.cpp +++ b/src/libslic3r/BeltGCode.cpp @@ -25,12 +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). @@ -40,25 +34,12 @@ 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) + // Global pre-slice mode (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. + if (!m_config.belt_preslice_global.value) return; // Adjust origin: transform through belt forward pipeline so that diff --git a/src/libslic3r/BeltSliceStrategy.cpp b/src/libslic3r/BeltSliceStrategy.cpp index 8892b4e5de..aa01c3ca9e 100644 --- a/src/libslic3r/BeltSliceStrategy.cpp +++ b/src/libslic3r/BeltSliceStrategy.cpp @@ -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 diff --git a/src/libslic3r/BeltTransform.cpp b/src/libslic3r/BeltTransform.cpp index d1ea31d33c..168d9cde1c 100644 --- a/src/libslic3r/BeltTransform.cpp +++ b/src/libslic3r/BeltTransform.cpp @@ -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,52 +37,13 @@ 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(), mx = bb.max.cast(); - 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. @@ -216,15 +133,15 @@ 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); } } // namespace Slic3r diff --git a/src/libslic3r/BeltTransform.hpp b/src/libslic3r/BeltTransform.hpp index 59669fe36a..01346464ae 100644 --- a/src/libslic3r/BeltTransform.hpp +++ b/src/libslic3r/BeltTransform.hpp @@ -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("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>(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 { @@ -153,15 +115,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); }; diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 68a1434699..a108588f84 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -94,8 +94,6 @@ set(lisbslic3r_sources BeltSliceStrategy.hpp BeltTransform.cpp BeltTransform.hpp - FirstLayerPlane.cpp - FirstLayerPlane.hpp Brim.cpp BrimEarsPoint.hpp Brim.hpp diff --git a/src/libslic3r/FirstLayerPlane.cpp b/src/libslic3r/FirstLayerPlane.cpp deleted file mode 100644 index 7642d42267..0000000000 --- a/src/libslic3r/FirstLayerPlane.cpp +++ /dev/null @@ -1,229 +0,0 @@ -#include "FirstLayerPlane.hpp" -#include "BeltTransform.hpp" -#include "BoundingBox.hpp" -#include "Point.hpp" -#include "PrintConfig.hpp" -#include "libslic3r.h" - -#include -#include -#include - -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 diff --git a/src/libslic3r/FirstLayerPlane.hpp b/src/libslic3r/FirstLayerPlane.hpp deleted file mode 100644 index 35b8346c82..0000000000 --- a/src/libslic3r/FirstLayerPlane.hpp +++ /dev/null @@ -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_ diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 46c1458c8b..8ad30a611a 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -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(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 &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(bb.min.x()), unscale(bb.min.y())), - Vec2d(unscale(bb.max.x()), unscale(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 diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index c5268705c3..ccfd14dd39 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -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 m_first_layer_plane; // Plate origin, kept so a writer replaced during export can be given it again. std::unique_ptr m_pressure_equalizer; @@ -938,42 +935,33 @@ 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* only. Two separate + // thresholds: on_first_layer(point) tests against initial_layer_print_height, + // while effective_layer_index_for_point() 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. 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; @@ -985,13 +973,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. diff --git a/src/libslic3r/GCode/BeltBackTransform.cpp b/src/libslic3r/GCode/BeltBackTransform.cpp index 4b247323cd..e7257cf90e 100644 --- a/src/libslic3r/GCode/BeltBackTransform.cpp +++ b/src/libslic3r/GCode/BeltBackTransform.cpp @@ -13,16 +13,12 @@ bool BeltBackTransform::init_from_config(const PrintConfig &config) if (!config.belt_printer.value || !config.gcode_back_transform.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)) + // The back-transform undoes the global pre-slice rotation; without global + // mode the slicing frame is not a common frame to undo. + if (!config.belt_preslice_global.value) 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; diff --git a/src/libslic3r/GCode/BeltBackTransform.hpp b/src/libslic3r/GCode/BeltBackTransform.hpp index 878ae12b5a..69e32a4b2c 100644 --- a/src/libslic3r/GCode/BeltBackTransform.hpp +++ b/src/libslic3r/GCode/BeltBackTransform.hpp @@ -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 when gcode_back_transform and belt_preslice_global are both set. 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 diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index fa418e1b62..1899e2358b 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -2641,9 +2641,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; @@ -3223,8 +3220,7 @@ void GCodeProcessor::apply_config(const DynamicPrintConfig& config) const auto *belt = config.option("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", + "belt_printer", "belt_slice_rotation", "belt_slice_rotation_angle", "belt_preslice_global", "gcode_remap_x", "gcode_remap_y", "gcode_remap_z", "gcode_back_transform", "belt_frame_tilt_decouple", "belt_frame_tilt_angle", }; @@ -4350,36 +4346,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; diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index a5c5ce7914..cb8178d113 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -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).belt_tilt_angle; belt_z_origin = std::forward(other).belt_z_origin; machine_frame_transform_active = std::forward(other).machine_frame_transform_active; - preslice_remap_x = std::forward(other).preslice_remap_x; - preslice_remap_y = std::forward(other).preslice_remap_y; - preslice_remap_z = std::forward(other).preslice_remap_z; #if ENABLE_GCODE_VIEWER_STATISTICS time = std::forward(other).time; #endif diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index bc25c448dd..e1c1308076 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1560,13 +1560,12 @@ static std::vector s_Preset_machine_limits_options { static std::vector 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", + "belt_slice_rotation", "belt_slice_rotation_angle", "gcode_remap_x", "gcode_remap_y", "gcode_remap_z", "gcode_back_transform", "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", + "first_layer_plane_thickness", + "belt_support_floor_offset", "belt_support_floor_mode", "enable_belt_purge_tower", "gcode_flavor", "gcode_skip_config_block", "fan_kickstart", "part_cooling_fan_min_pwm", "fan_speedup_time", "fan_speedup_overhangs", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 9dc9ff3cd3..9aa9626e5e 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -177,7 +177,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n // 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", + "first_layer_plane_thickness", // Only inflates the GUI bed volume, like printable_area. "belt_printer_infinite_y", //BBS @@ -389,17 +389,11 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || 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_preslice_global") { 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_mode") { osteps.emplace_back(posSupportMaterial); } else if ( opt_key == "print_sequence" @@ -2107,9 +2101,7 @@ StringObjectException Print::validate(std::vector *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 +3061,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) std::set 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 && m_config.belt_preslice_global.value; if (!use_cache) { for (int index = 0; index < object_count; index++) { diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index 425912b9c2..5ee4815489 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -231,11 +231,10 @@ class ConstSupportLayerPtrsAdaptor : public ConstVectorOfPtrsAdaptor(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. diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index 94725118c7..108e2cc796 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1844,12 +1844,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ // 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))); + bool belt_force_separate = m_config.belt_printer.value && m_config.belt_preslice_global.value; model_object_status.print_instances = print_objects_from_model_object(*model_object, this->shrinkage_compensation(), belt_force_separate); std::vector old; old.reserve(print_object_status_db.count(*model_object)); @@ -1940,11 +1935,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ // 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)))) { + && m_config.belt_preslice_global.value) { for (PrintObject *object : m_objects) update_apply_status(object->invalidate_step(posSlice)); } diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index ee23a88940..708e942359 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -404,25 +404,6 @@ static t_config_enum_values s_keys_map_BeltSupportFloorMode { }; 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 }, @@ -7425,16 +7406,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 +7429,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,39 +7443,9 @@ void PrintConfigDef::init_fff_params() def->set_default_value(new ConfigOptionEnum(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 @@ -7528,53 +7469,14 @@ void PrintConfigDef::init_fff_params() 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::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::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->label = L("First layer 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->tooltip = L("Belt printers only. Every tilted layer touches the belt, so the first-layer " + "settings apply to a band above the belt surface rather than to the first " + "slicing layer. This is the thickness of one band, in mm: the unit by which " + "'No cooling for the first N layers' and similar layer-count thresholds are " + "multiplied. -1 means use the first layer height."); def->sidetext = L("mm"); def->min = -1; def->max = 100; @@ -7605,19 +7507,6 @@ void PrintConfigDef::init_fff_params() def->set_default_value(new ConfigOptionEnum(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::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::Unconditional)); - } - def = this->add("enable_belt_purge_tower", coBool); def->label = L("Enable belt purge tower"); def->category = L("Multimaterial"); @@ -9793,6 +9682,11 @@ 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 switches collapsed + // into belt_preslice_global, the pre-slice axis remap and the support Z offset mode + // 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", }; if (ignore.find(opt_key) != ignore.end()) { diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 1a9e3ea075..01ee8b4b0c 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -300,34 +300,6 @@ enum class BeltSupportFloorMode 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, @@ -775,8 +747,6 @@ 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 +1868,18 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( // g-code back-transform inverts the rotation before the machine-frame stage. ((ConfigOptionEnum, 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, preslice_remap_x)) - ((ConfigOptionEnum, preslice_remap_y)) - ((ConfigOptionEnum, preslice_remap_z)) - ((ConfigOptionBool, preslice_remap_global)) ((ConfigOptionEnum, gcode_remap_x)) ((ConfigOptionEnum, gcode_remap_y)) ((ConfigOptionEnum, gcode_remap_z)) ((ConfigOptionBool, gcode_back_transform)) ((ConfigOptionBool, belt_preslice_global)) - ((ConfigOptionEnum, first_layer_plane)) - ((ConfigOptionFloat, first_layer_plane_offset)) ((ConfigOptionFloat, first_layer_plane_thickness)) ((ConfigOptionFloat, belt_support_floor_offset)) ((ConfigOptionEnum, belt_support_floor_mode)) - ((ConfigOptionEnum, 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 diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 7a46f354fc..d11f0ec1a3 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -4154,9 +4154,7 @@ 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(); + const BoundingBoxf3 bb = this->model_object()->raw_bounding_box(); auto hr = BeltTransformPipeline::compute_belt_height_and_floor(pcfg, bb, object_height); object_height = hr.object_height; belt_floor = hr.floor_params; @@ -4217,9 +4215,6 @@ 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(); 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; diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index b384a28ef3..46408fe886 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -343,8 +343,7 @@ static std::vector> 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,7 +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 +996,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 @@ -1036,7 +1028,9 @@ void PrintObject::slice() 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(inst_shift.x()), unscale(inst_shift.y()), 0.); Vec3d c = T.linear() * d - d; @@ -1046,31 +1040,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(inst_shift.x()), unscale(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(inst_shift.x()), unscale(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 diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index b03e6b5f03..49940229dc 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1339,10 +1339,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) { diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index c64b4c8807..a2d0f3ea3e 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -5260,67 +5260,27 @@ void TabPrinter::build_fff() 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); - } + belt_og->append_single_option_line("first_layer_plane_thickness", "printer_basic_information_belt_printer#first-layer-band-thickness"); // Support floor: split across lines so each setting's own mode controls - // its visibility (floor_mode = Develop, floor_offset = Advanced, z_offset_mode = Expert). + // its visibility (floor_mode = Develop, floor_offset = Advanced). 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"); @@ -6370,36 +6330,27 @@ void TabPrinter::toggle_options() // 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", "gcode_back_transform"}) 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>("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); + toggle_line("first_layer_plane_thickness", is_belt); - for (auto el : {"belt_support_floor_mode", "belt_support_floor_offset", "belt_support_z_offset_mode"}) + for (auto el : {"belt_support_floor_mode", "belt_support_floor_offset"}) toggle_line(el, is_belt); const bool support_parallel_printheads = printer_cfg.opt_bool("support_parallel_printheads"); toggle_line("parallel_printheads_count", support_parallel_printheads); diff --git a/tests/fff_print/test_gcodewriter.cpp b/tests/fff_print/test_gcodewriter.cpp index b0f36c33d9..9ef4a8f39d 100644 --- a/tests/fff_print/test_gcodewriter.cpp +++ b/tests/fff_print/test_gcodewriter.cpp @@ -56,7 +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; @@ -1030,7 +1029,6 @@ SCENARIO("Belt: the first travel does not lift through the uninitialised origin" 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; diff --git a/tests/fff_print/test_precise_seam.cpp b/tests/fff_print/test_precise_seam.cpp index d5e2709cdd..b5cb913904 100644 --- a/tests/fff_print/test_precise_seam.cpp +++ b/tests/fff_print/test_precise_seam.cpp @@ -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 }, diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index c0e5572b97..f16a0ce662 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -765,7 +765,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 +814,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 +849,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 +927,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 +999,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 +1034,23 @@ 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" }, diff --git a/tests/fff_print/test_skirt_brim.cpp b/tests/fff_print/test_skirt_brim.cpp index 45086b9d2a..99e7860c3e 100644 --- a/tests/fff_print/test_skirt_brim.cpp +++ b/tests/fff_print/test_skirt_brim.cpp @@ -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" }, @@ -1246,9 +1244,7 @@ TEST_CASE("Belt brim allows instances placed across the belt", "[SkirtBrim][belt 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 }, From dda58b07cdf8fa573141ad01443b8c35089fb805 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Tue, 6 Oct 2026 22:14:25 -0500 Subject: [PATCH 194/224] Belt printer: supports reach the belt under a leading overhang 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, by the overhang's length times the tilt's shear. Every support generator works in layers at z >= 0, so none of them could reach it: normal supports stopped at the object's own lowest layer, and the two tree generators each carried a stack of hacks to extend themselves below it (a post-hoc copy of the lowest base area in TreeSupport, "virtual belt raft layers" in TreeSupport3D/TreeModelVolumes), sized from the pre-rotation bbox and capped at global z = 0, which is only right for the trailing half of the belt. Start the frame at the lowest belt-floor point under the footprint instead, less a 10 mm margin along the belt for the base of a support column (BeltSliceStrategy::apply_preslice_transforms and BeltTransformPipeline::compute_belt_height_and_floor agree on it). The layers between it and the first vertex come out empty, which belt slicing already tolerates, and the generators need no extension at all: - normal supports: the generator anchors its layer grid at the frame origin, so run it in the object frame and shift the global belt Z offset onto the result afterwards, as organic supports 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). Drop the first-layer flange expansion on a belt: the first support layer is the leading tip of the support, not a flange, and inflating it put lines in the air ahead of the belt. - classic tree: a node now keeps dropping until its whole circle is in the belt, so the branch tapers to a tip on the belt instead of stopping, a radius above it, when its centre crosses. - 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 tilted belt and ahead of the part; the belt is where branches end, which the per-layer m_belt_floor clipping already does. The belt brim is generated after the parallel support step instead of inside it: 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. This is the race behind the Windows arm64 segfault in "Belt brim of each object precedes its perimeters on its own filament". Also: the belt tilt axis moves to Developer mode as its own row (a shared line is shown by its first option's mode), first_layer_plane band thickness, belt_support_floor_mode, belt_preslice_global and gcode_back_transform are retired and presumed on, the gravity arrow is a plain line along the up direction, and the "Show raw G-code (belt only)" preview toggle is gone. Regression test: "Belt supports reach the belt under a leading overhang" slices 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, and checks that the lowest support layer sits on the belt beneath its own lines. Co-Authored-By: Claude Fable 5.1 --- src/libslic3r/BeltGCode.cpp | 12 +- src/libslic3r/BeltSliceStrategy.cpp | 18 ++ src/libslic3r/BeltTransform.cpp | 74 +++++--- src/libslic3r/BeltTransform.hpp | 13 ++ src/libslic3r/GCode.hpp | 10 +- src/libslic3r/GCode/BeltBackTransform.cpp | 7 +- src/libslic3r/GCode/BeltBackTransform.hpp | 2 +- src/libslic3r/GCode/GCodeProcessor.cpp | 4 +- src/libslic3r/Preset.cpp | 6 +- src/libslic3r/Print.cpp | 25 +-- src/libslic3r/Print.hpp | 11 +- src/libslic3r/PrintApply.cpp | 10 +- src/libslic3r/PrintConfig.cpp | 64 +------ src/libslic3r/PrintConfig.hpp | 11 -- src/libslic3r/PrintObject.cpp | 81 +++++---- src/libslic3r/PrintObjectSlice.cpp | 3 +- src/libslic3r/Support/SupportCommon.cpp | 8 +- src/libslic3r/Support/SupportMaterial.cpp | 5 +- src/libslic3r/Support/TreeModelVolumes.cpp | 59 ++----- src/libslic3r/Support/TreeSupport.cpp | 186 ++++----------------- src/libslic3r/Support/TreeSupport.hpp | 7 + src/libslic3r/Support/TreeSupport3D.cpp | 41 +---- src/slic3r/GUI/3DBed.cpp | 27 +-- src/slic3r/GUI/3DBed.hpp | 1 + src/slic3r/GUI/GCodeViewer.cpp | 66 ++------ src/slic3r/GUI/GCodeViewer.hpp | 6 - src/slic3r/GUI/GLCanvas3D.cpp | 24 --- src/slic3r/GUI/GUI_Preview.cpp | 11 -- src/slic3r/GUI/GUI_Preview.hpp | 1 - src/slic3r/GUI/Plater.cpp | 5 - src/slic3r/GUI/Plater.hpp | 1 - src/slic3r/GUI/Shortcuts.cpp | 1 - src/slic3r/GUI/Shortcuts.hpp | 2 +- src/slic3r/GUI/Tab.cpp | 31 +--- tests/fff_print/test_gcodewriter.cpp | 3 - tests/fff_print/test_print.cpp | 102 ++++++++++- tests/fff_print/test_skirt_brim.cpp | 92 +++++----- 37 files changed, 412 insertions(+), 618 deletions(-) diff --git a/src/libslic3r/BeltGCode.cpp b/src/libslic3r/BeltGCode.cpp index 45b0728810..1726fc6eac 100644 --- a/src/libslic3r/BeltGCode.cpp +++ b/src/libslic3r/BeltGCode.cpp @@ -25,7 +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_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); @@ -34,13 +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 (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. - if (!m_config.belt_preslice_global.value) - 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. diff --git a/src/libslic3r/BeltSliceStrategy.cpp b/src/libslic3r/BeltSliceStrategy.cpp index aa01c3ca9e..8aac729f1b 100644 --- a/src/libslic3r/BeltSliceStrategy.cpp +++ b/src/libslic3r/BeltSliceStrategy.cpp @@ -38,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::max(); for (const ModelVolume *mv : model_volumes) { if (!mv->is_model_part()) continue; @@ -47,8 +61,12 @@ void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo, Vec3d vm = v.cast(); 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::max()) + min_z -= BeltTransformPipeline::frame_margin(floor); const double z_shift_val = (min_z < 0. && min_z != std::numeric_limits::max()) ? -min_z : 0.; if (z_shift_val > 0.) { Transform3d z_shift = Transform3d::Identity(); diff --git a/src/libslic3r/BeltTransform.cpp b/src/libslic3r/BeltTransform.cpp index 168d9cde1c..d11e2fccdc 100644 --- a/src/libslic3r/BeltTransform.cpp +++ b/src/libslic3r/BeltTransform.cpp @@ -50,6 +50,37 @@ Transform3d BeltTransformPipeline::build_forward_transform(const PrintConfig &co // 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 BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl( const Config &config, const BoundingBoxf3 &bb, double original_height) @@ -93,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::max(); double max_rz = std::numeric_limits::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; @@ -144,4 +165,15 @@ BeltTransformPipeline::BeltHeightResult BeltTransformPipeline::compute_belt_heig 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 diff --git a/src/libslic3r/BeltTransform.hpp b/src/libslic3r/BeltTransform.hpp index 01346464ae..7409fb2c7f 100644 --- a/src/libslic3r/BeltTransform.hpp +++ b/src/libslic3r/BeltTransform.hpp @@ -108,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 diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index ccfd14dd39..4aa96f9a4c 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -957,14 +957,10 @@ protected: return on_first_layer() ? 0 : layer_id(); } - // Band thickness for the *effective layer index* only. Two separate - // thresholds: on_first_layer(point) tests against initial_layer_print_height, - // while effective_layer_index_for_point() 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; } diff --git a/src/libslic3r/GCode/BeltBackTransform.cpp b/src/libslic3r/GCode/BeltBackTransform.cpp index e7257cf90e..3d2cabc718 100644 --- a/src/libslic3r/GCode/BeltBackTransform.cpp +++ b/src/libslic3r/GCode/BeltBackTransform.cpp @@ -10,12 +10,7 @@ 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) - return false; - - // The back-transform undoes the global pre-slice rotation; without global - // mode the slicing frame is not a common frame to undo. - if (!config.belt_preslice_global.value) + if (!config.belt_printer.value) return false; // Build the forward pipeline (the rotation) and store its inverse. diff --git a/src/libslic3r/GCode/BeltBackTransform.hpp b/src/libslic3r/GCode/BeltBackTransform.hpp index 69e32a4b2c..3956450a9f 100644 --- a/src/libslic3r/GCode/BeltBackTransform.hpp +++ b/src/libslic3r/GCode/BeltBackTransform.hpp @@ -14,7 +14,7 @@ namespace Slic3r { // Initialized once from PrintConfig, then applied per-point in // BeltKinematics::to_machine() before axis remapping. // -// Active when gcode_back_transform and belt_preslice_global are both set. +// Active on belt printers with a non-identity pre-slice rotation. class BeltBackTransform { public: diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 1899e2358b..1ff76dd9fa 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -3220,8 +3220,8 @@ void GCodeProcessor::apply_config(const DynamicPrintConfig& config) const auto *belt = config.option("belt_printer"); if (belt != nullptr) { static const char *belt_keys[] = { - "belt_printer", "belt_slice_rotation", "belt_slice_rotation_angle", "belt_preslice_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) diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index e1c1308076..4b2b28d4a8 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1561,11 +1561,9 @@ static std::vector 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", - "gcode_remap_x", "gcode_remap_y", "gcode_remap_z", "gcode_back_transform", + "gcode_remap_x", "gcode_remap_y", "gcode_remap_z", "belt_frame_tilt_decouple", "belt_frame_tilt_angle", - "belt_preslice_global", - "first_layer_plane_thickness", - "belt_support_floor_offset", "belt_support_floor_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", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 9aa9626e5e..9f7adaf86d 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -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_thickness", // Only inflates the GUI bed volume, like printable_area. "belt_printer_infinite_y", //BBS @@ -388,12 +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_preslice_global") { + || 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_floor_offset") { osteps.emplace_back(posSupportMaterial); } else if ( opt_key == "print_sequence" @@ -1908,14 +1904,6 @@ StringObjectException Print::validate(std::vector *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 " @@ -3061,7 +3049,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) std::set 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_preslice_global.value; + bool belt_no_share = m_config.belt_printer.value; if (!use_cache) { for (int index = 0; index < object_count; index++) { @@ -3221,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)) @@ -3230,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) diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index 5ee4815489..f60f6b4e19 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -439,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& belt_brim_by_layer() const { return m_belt_brim_by_layer; } const std::vector& belt_brim_areas_by_layer() const { return m_belt_brim_areas_by_layer; } const std::vector& belt_brim_prologue() const { return m_belt_brim_prologue; } @@ -583,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(); @@ -712,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. diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index 108e2cc796..39d5716b08 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1842,9 +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(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_preslice_global.value; + // 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 old; old.reserve(print_object_status_db.count(*model_object)); @@ -1933,9 +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_preslice_global.value) { + if (belt_instances_shifted && m_config.belt_printer.value) { for (PrintObject *object : m_objects) update_apply_status(object->invalidate_step(posSlice)); } diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 708e942359..8e5b2e2b0f 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -398,12 +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_SupportMaterialPattern { { "rectilinear", smpRectilinear }, { "rectilinear-grid", smpRectilinearGrid }, @@ -7390,7 +7384,7 @@ void PrintConfigDef::init_fff_params() def->enum_keys_map = &ConfigOptionEnum::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::X)); def = this->add("belt_slice_rotation_angle", coFloat); @@ -7451,38 +7445,6 @@ void PrintConfigDef::init_fff_params() // 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)); - - def = this->add("first_layer_plane_thickness", coFloat); - def->label = L("First layer band thickness"); - def->category = L("Printable space"); - def->tooltip = L("Belt printers only. Every tilted layer touches the belt, so the first-layer " - "settings apply to a band above the belt surface rather than to the first " - "slicing layer. This is the thickness of one band, in mm: the unit by which " - "'No cooling for the first N layers' and similar layer-count thresholds are " - "multiplied. -1 means use the first layer 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"); @@ -7494,19 +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::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::GeneratorOnly)); - } - def = this->add("enable_belt_purge_tower", coBool); def->label = L("Enable belt purge tower"); def->category = L("Multimaterial"); @@ -9440,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") { @@ -9682,11 +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 switches collapsed - // into belt_preslice_global, the pre-slice axis remap and the support Z offset mode - // were removed. + // 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()) { diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 01ee8b4b0c..05ee97a806 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -294,12 +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 SupportMaterialPattern { smpDefault, smpRectilinear, smpRectilinearGrid, smpHoneycomb, @@ -746,7 +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(SupportMaterialPattern) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialStyle) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialInterfacePattern) @@ -1875,11 +1868,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionEnum, gcode_remap_x)) ((ConfigOptionEnum, gcode_remap_y)) ((ConfigOptionEnum, gcode_remap_z)) - ((ConfigOptionBool, gcode_back_transform)) - ((ConfigOptionBool, belt_preslice_global)) - ((ConfigOptionFloat, first_layer_plane_thickness)) ((ConfigOptionFloat, belt_support_floor_offset)) - ((ConfigOptionEnum, belt_support_floor_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 diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index d11f0ec1a3..583a72766c 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -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(); @@ -4741,11 +4728,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 diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 46408fe886..5f70887c9c 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -976,7 +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_preslice_global=" << pcfg.belt_preslice_global.value << " object=" << this->model_object()->name; if (pcfg.belt_printer.value) { @@ -1026,7 +1025,7 @@ 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, so // objects at different bed positions print at different machine Z values diff --git a/src/libslic3r/Support/SupportCommon.cpp b/src/libslic3r/Support/SupportCommon.cpp index f7d173bb60..481213fe48 100644 --- a/src/libslic3r/Support/SupportCommon.cpp +++ b/src/libslic3r/Support/SupportCommon.cpp @@ -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(); diff --git a/src/libslic3r/Support/SupportMaterial.cpp b/src/libslic3r/Support/SupportMaterial.cpp index d8b712b55e..17dc858262 100644 --- a/src/libslic3r/Support/SupportMaterial.cpp +++ b/src/libslic3r/Support/SupportMaterial.cpp @@ -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(0, support_layers.size()), [&](const tbb::blocked_range &range) { diff --git a/src/libslic3r/Support/TreeModelVolumes.cpp b/src/libslic3r/Support/TreeModelVolumes.cpp index f3e322154a..33d379c2b5 100644 --- a/src/libslic3r/Support/TreeModelVolumes.cpp +++ b/src/libslic3r/Support/TreeModelVolumes.cpp @@ -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 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) diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 31b93292a7..f8b1ad1f26 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -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::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)); diff --git a/src/libslic3r/Support/TreeSupport.hpp b/src/libslic3r/Support/TreeSupport.hpp index d0855e6cb6..c6acd806e0 100644 --- a/src/libslic3r/Support/TreeSupport.hpp +++ b/src/libslic3r/Support/TreeSupport.hpp @@ -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; diff --git a/src/libslic3r/Support/TreeSupport3D.cpp b/src/libslic3r/Support/TreeSupport3D.cpp index 189e855ed9..dc284ba94f 100644 --- a/src/libslic3r/Support/TreeSupport3D.cpp +++ b/src/libslic3r/Support/TreeSupport3D.cpp @@ -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 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); } diff --git a/src/slic3r/GUI/3DBed.cpp b/src/slic3r/GUI/3DBed.cpp index b28f9c5d1c..d7fd871f27 100644 --- a/src/slic3r/GUI/3DBed.cpp +++ b/src/slic3r/GUI/3DBed.cpp @@ -36,6 +36,7 @@ #include #include "libslic3r/Preset.hpp" #include "libslic3r/Config.hpp" +#include #if BOOST_VERSION >= 107800 #include @@ -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()); diff --git a/src/slic3r/GUI/3DBed.hpp b/src/slic3r/GUI/3DBed.hpp index 497617a2e3..eeabe7da82 100644 --- a/src/slic3r/GUI/3DBed.hpp +++ b/src/slic3r/GUI/3DBed.hpp @@ -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 }; diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 49940229dc..0af44aebf2 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1318,13 +1318,11 @@ std::vector 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) @@ -1360,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 @@ -1393,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); @@ -1439,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 @@ -1691,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) @@ -5146,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(); diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 06d275a187..9cd669372f 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -199,8 +199,6 @@ private: std::vector 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(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(); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 6a7cc39079..668d2cc2b4 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -3778,16 +3778,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; @@ -10190,20 +10180,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 diff --git a/src/slic3r/GUI/GUI_Preview.cpp b/src/slic3r/GUI/GUI_Preview.cpp index f52bd20cc2..1968df996f 100644 --- a/src/slic3r/GUI/GUI_Preview.cpp +++ b/src/slic3r/GUI/GUI_Preview.cpp @@ -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) { diff --git a/src/slic3r/GUI/GUI_Preview.hpp b/src/slic3r/GUI/GUI_Preview.hpp index acb3211fd9..2ad1f8788c 100644 --- a/src/slic3r/GUI/GUI_Preview.hpp +++ b/src/slic3r/GUI/GUI_Preview.hpp @@ -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(); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 3684020f8a..09f3cd2b8f 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -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() { diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 83d028549b..e7d94bc606 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -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); diff --git a/src/slic3r/GUI/Shortcuts.cpp b/src/slic3r/GUI/Shortcuts.cpp index 2395d31778..8773c6f364 100644 --- a/src/slic3r/GUI/Shortcuts.cpp +++ b/src/slic3r/GUI/Shortcuts.cpp @@ -149,7 +149,6 @@ constexpr std::array 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), diff --git a/src/slic3r/GUI/Shortcuts.hpp b/src/slic3r/GUI/Shortcuts.hpp index 6a58d64971..0e61554e2f 100644 --- a/src/slic3r/GUI/Shortcuts.hpp +++ b/src/slic3r/GUI/Shortcuts.hpp @@ -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 diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index a2d0f3ea3e..96152efb3d 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -5250,25 +5250,12 @@ 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")); - 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"); - belt_og->append_single_option_line("first_layer_plane_thickness", "printer_basic_information_belt_printer#first-layer-band-thickness"); - // Support floor: split across lines so each setting's own mode controls - // its visibility (floor_mode = Develop, floor_offset = Advanced). + // 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_floor_mode", "printer_basic_information_belt_printer#floor-mode"); // Machine-frame transform: the shear (cot) + scale (1/sin) that map // Cartesian G-code into the printer's physical machine frame are derived @@ -6326,6 +6313,7 @@ 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 @@ -6333,9 +6321,8 @@ void TabPrinter::toggle_options() // 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 : {"gcode_remap_x", "gcode_remap_y", "gcode_remap_z", "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); // Rotation is the only mesh-side belt transform. Gray out its angle when no // rotation axis is selected. @@ -6348,10 +6335,8 @@ void TabPrinter::toggle_options() toggle_line("belt_frame_tilt_angle", is_belt && expert_or_above && m_config->opt_bool("belt_frame_tilt_decouple")); - toggle_line("first_layer_plane_thickness", is_belt); - for (auto el : {"belt_support_floor_mode", "belt_support_floor_offset"}) - 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); diff --git a/tests/fff_print/test_gcodewriter.cpp b/tests/fff_print/test_gcodewriter.cpp index 9ef4a8f39d..3dbb3a986b 100644 --- a/tests/fff_print/test_gcodewriter.cpp +++ b/tests/fff_print/test_gcodewriter.cpp @@ -56,7 +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.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; @@ -1026,10 +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_preslice_global.value = true; belt_config.belt_frame_tilt_decouple.value = false; belt_config.belt_frame_tilt_angle.value = 45.0; diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index f16a0ce662..57232d5a11 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -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 #include "libslic3r/Polygon.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/GCodeReader.hpp" @@ -1041,16 +1045,12 @@ TEST_CASE("Belt-only keys at non-default values leave non-belt G-code unchanged" { "belt_printer_infinite_y", 0 }, { "belt_slice_rotation", "y" }, { "belt_slice_rotation_angle", 30 }, - { "belt_preslice_global", 0 }, { "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_thickness", 1 }, { "belt_support_floor_offset", -5 }, - { "belt_support_floor_mode", "none" }, { "enable_belt_purge_tower", 1 }, { "belt_purge_tower_width", 10 }, { "leading_brim_length", 10 }, @@ -1242,3 +1242,97 @@ 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()); + 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::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::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::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); + } +} diff --git a/tests/fff_print/test_skirt_brim.cpp b/tests/fff_print/test_skirt_brim.cpp index 99e7860c3e..34f71a71a7 100644 --- a/tests/fff_print/test_skirt_brim.cpp +++ b/tests/fff_print/test_skirt_brim.cpp @@ -715,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; @@ -727,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 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. @@ -801,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 @@ -826,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{ 0 }); // filament 1 -> tool 0 } @@ -1231,49 +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_preslice_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 From a2f5a8ce2bb4bfd9e3089cc51b1fac632c9aad51 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 00:10:24 -0500 Subject: [PATCH 195/224] Belt printer: size the layer range from the on-bed box The belt object height is estimated from a bounding box swept through the tilt rotation. raw_bounding_box() has the instance's Z offset removed, which did not matter while the estimate was the box's rotated Z extent (a Z shift moves every corner alike), but the frame now starts at the lowest belt-floor point under the footprint, and a point's rotated z and the floor under it move in opposite directions under a Z shift: the offset box under-estimated the height by twice the object's height above the bed, so the layers stopped at the part's diagonal and every part came out as a wedge (GUI and CLI alike; the unit tests never checked the top). Use the box of the mesh in the frame it is sliced in (trafo_centered(), Z as placed on the bed), and have the leading overhang test check that the whole part is sliced. Co-Authored-By: Claude Fable 5.1 --- src/libslic3r/PrintObject.cpp | 19 ++++++++++++++++++- tests/fff_print/test_print.cpp | 16 ++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 583a72766c..4f46921c3f 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -4141,7 +4141,17 @@ void PrintObject::update_slicing_parameters() BeltTransformPipeline::BeltFloorParams belt_floor; const auto &pcfg = this->print()->config(); if (pcfg.belt_printer.value) { - const BoundingBoxf3 bb = this->model_object()->raw_bounding_box(); + // 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; @@ -4202,6 +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) { + // 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; diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 57232d5a11..be5311fd7b 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -1305,6 +1305,22 @@ TEST_CASE("Belt supports reach the belt under a leading overhang", "[Print][belt 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::max(), hi = std::numeric_limits::lowest(); + for (const stl_vertex &v : mesh.its.vertices) { + lo = std::min(lo, v.y() + v.z()); + hi = std::max(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())); From 314e227cee6c82321984e33155227ed8a0bd8edd Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 00:53:05 -0500 Subject: [PATCH 196/224] Profile tool: list the retired belt keys as obsolete The loader's ignore set in PrintConfigDef::handle_legacy() gained the belt options retired in #16236, and the profile tool's unit test checks that its OBSOLETE_KEYS matches that set, so the Check profiles job failed on belt-printer. Add the twelve keys. Co-Authored-By: Claude Fable 5.1 --- scripts/orca_profile_tool.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/orca_profile_tool.py b/scripts/orca_profile_tool.py index f7f622c8c0..ec94eb7653 100755 --- a/scripts/orca_profile_tool.py +++ b/scripts/orca_profile_tool.py @@ -167,6 +167,11 @@ OBSOLETE_KEYS = { "filament_load_time", "filament_unload_time", "smooth_coefficient", "overhang_totally_speed", "silent_mode", "overhang_speed_classic", "anisotropic_surfaces", + # Belt printer options retired before the feature shipped (#16236). + "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", } # Keys renamed at some point, whose old and new spellings must never co-exist: From cd155f07ae2b6adb6c9d9f327859889bad8d3afa Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 01:29:53 -0500 Subject: [PATCH 197/224] Tests: include for std::sqrt in the belt overhang test clang-tidy (misc-include-cleaner) flagged it on belt-printer. Co-Authored-By: Claude Fable 5.1 --- tests/fff_print/test_print.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index be5311fd7b..7185b1eae5 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -34,6 +34,7 @@ #include "libslic3r/ExtrusionEntity.hpp" #include "libslic3r/Polyline.hpp" #include +#include #include "libslic3r/Polygon.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/GCodeReader.hpp" From e34f4aa0e518b0880dba0368f1d30f80ba60d117 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 01:33:02 -0500 Subject: [PATCH 198/224] Belt printer: bring back the raw G-code view in the canvas view menu The "Show raw G-code (belt only)" toggle, retired in #16236, returns as an item of the Preview canvas view menu (with its B shortcut), and only there: no legend checkbox. Unlit, the preview shows the designed, upright view; lit, the raw machine-frame G-code, which is what to look at when checking the machine frame transforms. The toggle is view only; exported G-code is the same either way. Co-Authored-By: Claude Fable 5.1 --- src/slic3r/GUI/GCodeViewer.cpp | 21 ++++++++++++++------- src/slic3r/GUI/GCodeViewer.hpp | 6 ++++++ src/slic3r/GUI/GLCanvas3D.cpp | 26 ++++++++++++++++++++++++++ src/slic3r/GUI/GUI_Preview.cpp | 19 +++++++++++++++++++ src/slic3r/GUI/GUI_Preview.hpp | 1 + src/slic3r/GUI/Plater.cpp | 5 +++++ src/slic3r/GUI/Plater.hpp | 1 + src/slic3r/GUI/Shortcuts.cpp | 1 + src/slic3r/GUI/Shortcuts.hpp | 2 +- 9 files changed, 74 insertions(+), 8 deletions(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 0af44aebf2..949177d4ec 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1387,7 +1387,11 @@ 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. - if (m_last_result_id == gcode_result.id && wxGetApp().is_editor()) { + // 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) { //BBS: add logs BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": the same id %1%, return directly, result %2% ") % m_last_result_id % (&gcode_result); @@ -1429,20 +1433,22 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const } // convert data from PrusaSlicer format to libvgcode format. - // 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. + // Belt printers: when the designed (upright) view is active, back-transform + // the toolpath geometry into model/Cartesian space using the general belt + // inverse (handles the mesh rotation, shear and axis remap). When off, the + // raw machine-frame G-code is shown (useful for checking the transform itself). const bool is_belt = m_belt_view_enabled && print.config().belt_printer.value; - Transform3d belt_inv = is_belt ? compute_belt_back_transform(print.config()) : Transform3d::Identity(); + Transform3d belt_inv = (is_belt && m_belt_show_designed) + ? 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 && gcode_result.belt_z_origin != 0.0f) + if (is_belt && m_belt_show_designed && 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 + const bool apply_belt = is_belt && m_belt_show_designed && !belt_inv.matrix().isApprox(Transform3d::Identity().matrix()); if (apply_belt) { // The linear belt back-transform recovers the print's shape and orientation but not @@ -1679,6 +1685,7 @@ 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) diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 9cd669372f..e5703939b2 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -199,6 +199,8 @@ private: std::vector 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(EMoveType::Count)> m_move_type_counts{}; @@ -262,6 +264,8 @@ mutable bool m_no_render_path { false }; bool m_is_dark = false; bool m_belt_view_enabled = false; + bool m_belt_show_designed = true; // Designed (upright, back-transformed) view by default; off shows + // the raw machine-frame G-code (canvas view menu, hotkey B). float m_belt_angle_deg = 0.f; libvgcode::Viewer m_viewer; @@ -402,6 +406,8 @@ 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(); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 1b4deced82..808c1831d2 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -3804,6 +3804,16 @@ bool GLCanvas3D::handle_shortcut(const KeyChord& chord) get_gcode_viewer().get_layers_slider()->switch_one_layer_mode(); m_dirty = true; break; + case Shortcut::ToggleBeltRawGcode: + // Same state as the canvas view 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::GoToLayer: if (!m_gizmos.is_enabled()) { get_gcode_viewer().get_layers_slider()->show_go_to_layer(true); @@ -10334,6 +10344,22 @@ void GLCanvas3D::_render_canvas_toolbar() [p]{p->show_view3D_labels(!p->are_view3D_labels_shown());} ); + // Belt printers, G-code preview only: show the raw machine-frame G-code instead of + // the designed (upright) view. This menu is the only place the toggle lives (plus + // its shortcut); 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()) { + ImGui::Separator(); + 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::PopItemFlag(); ImGui::EndPopup(); } diff --git a/src/slic3r/GUI/GUI_Preview.cpp b/src/slic3r/GUI/GUI_Preview.cpp index 1968df996f..1c17a02956 100644 --- a/src/slic3r/GUI/GUI_Preview.cpp +++ b/src/slic3r/GUI/GUI_Preview.cpp @@ -369,6 +369,25 @@ 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 range and only-gcode mode so the view doesn't jump on toggle. + // The layer Z values differ between the designed and the raw view (the raw view's are + // machine-frame heights), so keep_z_range alone cannot find the old span: carry the + // slider over by layer index instead. + IMSlider *layers_slider = m_canvas->get_gcode_viewer().get_layers_slider(); + const int lower = layers_slider->GetLowerValue(); + const int higher = layers_slider->GetHigherValue(); + m_loaded_print = nullptr; + load_print(true /*keep_z_range*/, m_only_gcode); + if (higher <= layers_slider->GetMaxValue()) + layers_slider->SetSelectionSpan(lower, higher); +} + //BBS: always load shell at preview void Preview::load_shells(const Print& print, bool force_previewing) { diff --git a/src/slic3r/GUI/GUI_Preview.hpp b/src/slic3r/GUI/GUI_Preview.hpp index 2ad1f8788c..acb3211fd9 100644 --- a/src/slic3r/GUI/GUI_Preview.hpp +++ b/src/slic3r/GUI/GUI_Preview.hpp @@ -149,6 +149,7 @@ 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(); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 09f3cd2b8f..3684020f8a 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -17885,6 +17885,11 @@ void Plater::reload_print() p->preview->reload_print(); } +void Plater::refresh_belt_view() +{ + p->preview->refresh_belt_view(); +} + // BBS wxString Plater::get_project_name() { diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index e7d94bc606..cf9a1536bb 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -387,6 +387,7 @@ public: void reload_print(); // 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 + void refresh_belt_view(); // toolpath geometry at load time. Keeps the current layer range and only-gcode mode. // SoftFever diff --git a/src/slic3r/GUI/Shortcuts.cpp b/src/slic3r/GUI/Shortcuts.cpp index 8773c6f364..2395d31778 100644 --- a/src/slic3r/GUI/Shortcuts.cpp +++ b/src/slic3r/GUI/Shortcuts.cpp @@ -149,6 +149,7 @@ constexpr std::array 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), diff --git a/src/slic3r/GUI/Shortcuts.hpp b/src/slic3r/GUI/Shortcuts.hpp index 0e61554e2f..6a58d64971 100644 --- a/src/slic3r/GUI/Shortcuts.hpp +++ b/src/slic3r/GUI/Shortcuts.hpp @@ -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, + ShowLabels, ShowWireframe, ToggleGcodeWindow, ToggleOneLayerMode, ToggleBeltRawGcode, // Application Preferences, Search, SwitchView, CollapseSidebar, ReloadDevicePage, KeyboardShortcuts, // Speed Dial From f64b49ab52f494258c30ca28ab46cdc71e53de95 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 02:23:54 -0500 Subject: [PATCH 199/224] Belt printer: no layer changes that print nothing, and a preview that survives them Since the slicing frame of a belt object starts at the belt below its leading end, its first layers are empty. On a single part they carry the brim bands; with several parts along the belt the later parts' empty layers fall between the earlier parts' printing layers and were written to the G-code as layer changes with no moves at all. The preview numbers its layers (libvgcode::Layers) from the vertices it is given and expects consecutive ids, so at the first such gap it stopped creating layers and folded everything after it into the last one: the top slider layer held nearly the whole print, the slider jumped every other layer through the single-colour stretch before a second part on another filament, and with the belt purge tower the whole print greyed out while dragging. Drop the belt layers that print nothing (no object, support or brim content) in GCode::collect_layers_to_print, and renumber the layers consecutively over the moves that exist when converting a result for libvgcode, so a file with empty layers from any source still previews correctly. The layer slider labels a belt layer with its print Z (the slicer's layer Z, which increases along the belt) instead of libvgcode's toolpath height, which on a tilted layer is wherever its last extrusion ended; the slider assumes that list increases and showed "0 / max" on alternate layers. The processor reads that print Z from the ";Z:" tag non-BBL printers write (it only knew "; Z_HEIGHT:"), on belt printers only, so nothing changes elsewhere. Regression test: two cubes 60 mm apart along the belt produce no layer without an extrusion and the header's layer count matches. Co-Authored-By: Claude Fable 5.1 --- src/libslic3r/GCode.cpp | 30 +++++++++ src/libslic3r/GCode/GCodeProcessor.cpp | 9 +++ src/slic3r/GUI/GCodeViewer.cpp | 22 +++++++ src/slic3r/GUI/GCodeViewer.hpp | 6 ++ src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp | 22 +++++-- tests/fff_print/test_print.cpp | 62 +++++++++++++++++++ 6 files changed, 147 insertions(+), 4 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 8ad30a611a..c96d299211 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2440,6 +2440,36 @@ std::vector>> GCode::collec layers_to_print.emplace_back(std::move(merged)); } + // Belt printers: drop the layers that print nothing at all. An object's + // slicing frame starts at the belt below its leading end, so its first layers + // are empty, and with several objects along the belt those empty layers fall + // between other objects' printing layers. A layer change with no moves is + // noise in the file, and the preview (libvgcode) numbers its layers from the + // moves it sees, so a gap folds every later layer into the one before it. + if (print.config().belt_printer.value) { + auto prints_something = [](const LayerToPrint <p) { + if (ltp.object_layer != nullptr && ltp.object_layer->has_extrusions()) + return true; + if (ltp.support_layer != nullptr && ltp.support_layer->has_extrusions()) + return true; + if (ltp.belt_brim_band != nullptr && ! ltp.belt_brim_band->fills.empty()) + return true; + if (ltp.object_layer != nullptr && ltp.original_object != nullptr && ltp.original_object->has_belt_brim()) { + const auto &by_layer = ltp.original_object->belt_brim_by_layer(); + const size_t id = ltp.object_layer->id(); + if (id < by_layer.size() && ! by_layer[id].empty()) + return true; + } + return false; + }; + layers_to_print.erase( + std::remove_if(layers_to_print.begin(), layers_to_print.end(), + [&prints_something](const std::pair> &group) { + return std::none_of(group.second.begin(), group.second.end(), prints_something); + }), + layers_to_print.end()); + } + return layers_to_print; } diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index deb776bf32..ac5a435bcd 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -4305,6 +4305,15 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers return; } + // ;Z: -- the layer Z tag non-BBL printers write. Only read on a belt printer, + // where the preview labels its layers with it (GCodeViewer::load_as_gcode); + // elsewhere print_z stays unset, as it always was, so nothing downstream of + // it changes for other printers. + if (m_belt_printer && boost::starts_with(comment, "Z:")) { + m_print_z = get_z_height(comment); + return; + } + if (boost::starts_with(comment, " CONFIG_BLOCK_START")) { m_in_config_block = true; return; diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 949177d4ec..dba2546f0c 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -87,6 +87,7 @@ #include #include +#include #include #include #include @@ -1635,6 +1636,26 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const m_viewer.reset_default_extrusion_roles_colors(); m_viewer.load(std::move(data)); + // Belt printers: libvgcode labels a layer with the height of its toolpaths, which + // on a tilted layer is wherever its last extrusion happened to end, and the layer + // slider looks its labels and the colour-change ticks up in that list assuming it + // increases. Give it the layers' print Z instead (the slicer's layer Z, which + // increases along the belt), numbered the way libvgcode::convert() numbers the + // layers: consecutively over the moves that exist. + m_belt_layer_zs.clear(); + if (is_belt) { + unsigned int src_layer_id = std::numeric_limits::max(); + for (size_t i = 1; i < gcode_result.moves.size(); ++ i) { + const GCodeProcessorResult::MoveVertex &mv = gcode_result.moves[i]; + if (mv.layer_id != src_layer_id) { + src_layer_id = mv.layer_id; + m_belt_layer_zs.emplace_back(double(mv.print_z)); + } + } + if (m_belt_layer_zs.size() != m_viewer.get_layers_count()) + m_belt_layer_zs.clear(); + } + // #if !VGCODE_ENABLE_COG_AND_TOOL_MARKERS // const size_t vertices_count = m_viewer.get_vertices_count(); // m_cog.reset(); @@ -1905,6 +1926,7 @@ void GCodeViewer::reset() //BBS: should also reset the result id BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": current result id %1% ")%m_last_result_id; m_last_result_id = -1; + m_belt_layer_zs.clear(); //BBS: add only gcode mode m_only_gcode_in_preview = false; diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index e5703939b2..c038aec671 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -201,6 +201,8 @@ private: 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 }; + // Belt printers: the print Z of each viewer layer, in the viewer's layer numbering. + std::vector m_belt_layer_zs; //BBS: save m_gcode_result as well const GCodeProcessorResult* m_gcode_result; std::array(EMoveType::Count)> m_move_type_counts{}; @@ -329,6 +331,10 @@ public: const BoundingBoxf3& get_max_bounding_box() const { return m_max_bounding_box; } const BoundingBoxf3& get_shell_bounding_box() const { return m_shell_bounding_box; } std::vector get_layers_zs() const { + // Belt printers: the layer Z the slider labels and the colour-change ticks + // use is the layer's print Z (see load_as_gcode), not a toolpath height. + if (! m_belt_layer_zs.empty()) + return m_belt_layer_zs; const std::vector zs = m_viewer.get_layers_zs(); std::vector ret; std::transform(zs.begin(), zs.end(), std::back_inserter(ret), [](float z) { return static_cast(z); }); diff --git a/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp b/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp index d6f9650972..ea010a41b5 100644 --- a/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp +++ b/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp @@ -17,6 +17,8 @@ #include "libslic3r/libslic3r.h" #include "LibVGCodeWrapper.hpp" + +#include #include "libslic3r/Print.hpp" #include "libslic3r/Color.hpp" #include "libslic3r/CustomGCode.hpp" @@ -230,10 +232,22 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve const std::vector& moves = result.moves; ret.vertices.reserve(2 * moves.size()); + // libvgcode numbers its layers from the vertices it is given and expects them to + // arrive one layer after the other with consecutive ids: a layer without any move + // (a belt file can carry layer changes that print nothing) would leave a gap, and + // every later vertex would then be folded into the last layer created. Renumber + // the ids consecutively over the moves that exist. + uint32_t src_layer_id = std::numeric_limits::max(); + uint32_t layer_id = 0; for (size_t i = 1; i < moves.size(); ++i) { const Slic3r::GCodeProcessorResult::MoveVertex& curr = moves[i]; const Slic3r::GCodeProcessorResult::MoveVertex& prev = moves[i - 1]; const EMoveType curr_type = convert(curr.type); + if (curr.layer_id != src_layer_id) { + if (src_layer_id != std::numeric_limits::max()) + ++ layer_id; + src_layer_id = curr.layer_id; + } const EOptionType option_type = move_type_to_option(curr_type); if (option_type == EOptionType::COUNT || option_type == EOptionType::Travels || option_type == EOptionType::Wipes) { if (ret.vertices.empty() || prev.type != curr.type || prev.extrusion_role != curr.extrusion_role @@ -245,7 +259,7 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve #if VGCODE_ENABLE_COG_AND_TOOL_MARKERS const libvgcode::PathVertex vertex = { xform_pos(prev.position), curr.height, curr.width, curr.feedrate, prev.actual_feedrate, curr.mm3_per_mm, curr.fan_speed, curr.temperature, 0.0f, convert(curr.extrusion_role), curr_type, - static_cast(curr.gcode_id), static_cast(curr.layer_id), + static_cast(curr.gcode_id), layer_id, static_cast(curr.extruder_id), static_cast(curr.cp_color_id), { 0.0f, 0.0f }, /* ORCA: Add Pressure Advance visualization support */ 0.0f, curr.pressure_advance, /* ORCA: Add Acceleration visualization support */ curr.acceleration, @@ -253,7 +267,7 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve #else const libvgcode::PathVertex vertex = { xform_pos(prev.position), curr.height, curr.width, curr.feedrate, prev.actual_feedrate, curr.mm3_per_mm, curr.fan_speed, curr.temperature, convert(curr.extrusion_role), curr_type, - static_cast(curr.gcode_id), static_cast(curr.layer_id), + static_cast(curr.gcode_id), layer_id, static_cast(curr.extruder_id), static_cast(curr.cp_color_id), { 0.0f, 0.0f }, /* ORCA: Add Pressure Advance visualization support */ 0.0f, curr.pressure_advance, /* ORCA: Add Acceleration visualization support */ curr.acceleration, @@ -267,7 +281,7 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve const libvgcode::PathVertex vertex = { xform_pos(curr.position), curr.height, curr.width, curr.feedrate, curr.actual_feedrate, curr.mm3_per_mm, curr.fan_speed, curr.temperature, result.filament_densities[curr.extruder_id] * curr.mm3_per_mm * (curr.position - prev.position).norm(), - convert(curr.extrusion_role), curr_type, static_cast(curr.gcode_id), static_cast(curr.layer_id), + convert(curr.extrusion_role), curr_type, static_cast(curr.gcode_id), layer_id, static_cast(curr.extruder_id), static_cast(curr.cp_color_id), curr.time, /* ORCA: Add Pressure Advance visualization support */ 0.0f, curr.pressure_advance, /* ORCA: Add Acceleration visualization support */ curr.acceleration, @@ -275,7 +289,7 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve #else const libvgcode::PathVertex vertex = { xform_pos(curr.position), curr.height, curr.width, curr.feedrate, curr.actual_feedrate, curr.mm3_per_mm, curr.fan_speed, curr.temperature, convert(curr.extrusion_role), curr_type, - static_cast(curr.gcode_id), static_cast(curr.layer_id), + static_cast(curr.gcode_id), layer_id, static_cast(curr.extruder_id), static_cast(curr.cp_color_id), curr.time, /* ORCA: Add Pressure Advance visualization support */ 0.0f, curr.pressure_advance, /* ORCA: Add Acceleration visualization support */ curr.acceleration, diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 7185b1eae5..0f1d33c67f 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -1244,6 +1244,68 @@ TEST_CASE("Organic tree supports place a support blocker at its own height above CHECK(collides(last + num_raft)); } +// Two parts along the belt: the second part's slicing frame starts at the belt +// below its leading end, so its first layers are empty and interleave with the +// first part's printing layers. Those must not reach the G-code as layer changes +// that print nothing: the preview numbers its layers from the moves it sees, and +// a gap folded every later layer into the one before it. +TEST_CASE("Belt G-code has no layer that prints nothing", "[Print][belt][GCode][Regression]") +{ + 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 }, + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "machine_start_gcode", "T[initial_tool]\n" }, + { "layer_change_gcode", "G92 E0\n" }, + }); + Print print; + Model model; + TriangleMesh cube_a(its_make_cube(20., 20., 20.)); + TriangleMesh cube_b(its_make_cube(20., 20., 20.)); + init_print({ cube_a, cube_b }, print, model, config); + // 60 mm apart along the belt: the second cube's lead-in layers fall among the + // first cube's layers. + model.objects[0]->instances.front()->set_offset(Vec3d(50., 40., 0.)); + model.objects[1]->instances.front()->set_offset(Vec3d(50., 100., 0.)); + print.apply(model, config); + print.set_status_silent(); + const std::string gc = gcode(print); + REQUIRE(! gc.empty()); + + size_t layers = 0, empty = 0, total_header = 0; + bool extruded = true; // before the first layer change + std::istringstream in(gc); + std::string line; + auto close_layer = [&]() { if (! extruded) ++ empty; }; + while (std::getline(in, line)) { + if (line.rfind(";LAYER_CHANGE", 0) == 0) { + close_layer(); + ++ layers; + extruded = false; + } else if (line.rfind("; total layer number: ", 0) == 0) { + total_header = size_t(std::atoi(line.c_str() + 22)); + } else if (! extruded && line.rfind("G1 ", 0) == 0 && line.find('E') != std::string::npos + && (line.find('X') != std::string::npos || line.find('Y') != std::string::npos)) { + extruded = true; + } + } + close_layer(); + INFO("layers " << layers << ", header " << total_header << ", layers without extrusion " << empty); + CHECK(layers > 150); // both cubes, 141 layers each, overlapping along the belt + CHECK(empty == 0); + CHECK(total_header == layers); +} + // 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 From ef6d4a71def3c194574c4dfbc7daca932e99c49d Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 05:24:58 -0500 Subject: [PATCH 200/224] Tests: qualify Polyline in the belt overhang test for Windows Windows headers declare a global Polyline, so the unqualified name in test_print.cpp is ambiguous there (both Windows builds of belt-printer fail at tests/fff_print/test_print.cpp:1336), as Polygon was in #16196. Co-Authored-By: Claude Fable 5.1 --- tests/fff_print/test_print.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 0f1d33c67f..32c37882f5 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -1395,7 +1395,7 @@ TEST_CASE("Belt supports reach the belt under a leading overhang", "[Print][belt REQUIRE(lowest != nullptr); double floor_under_lowest = std::numeric_limits::max(); for (const ExtrusionEntity *entity : lowest->support_fills.flatten().entities) - for (const Polyline &pl : entity->as_polylines()) + for (const Slic3r::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 From 006a9377c32764cb15e1a8b17bd4c4a27df55907 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 05:30:08 -0500 Subject: [PATCH 201/224] Profile validator: slice belt printers with two cubes along the belt The slice check (-s) prints one 10 mm cube per printer with a height range on filament 2 and expects the filament change to fire. Since #16236 a belt object's slicing Z starts at the belt below its leading end, well below the part's first printed layer, so the range 4-10 falls into the empty lead-in and filament 2 is never used: every belt printer reported "the filament change never fired" and the Slice check job on belt-printer went red. A height range in slicing Z does not map onto a part on a belt in any case. Slice belt printers with two cubes one behind the other along the belt, the second on filament 2, which gives the one plain T1 the check looks for. Other printers are unchanged. Co-Authored-By: Claude Fable 5.1 --- .../OrcaSlicer_profile_validator.cpp | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/dev-utils/OrcaSlicer_profile_validator.cpp b/src/dev-utils/OrcaSlicer_profile_validator.cpp index 7b80a3bbb1..208b950cfb 100644 --- a/src/dev-utils/OrcaSlicer_profile_validator.cpp +++ b/src/dev-utils/OrcaSlicer_profile_validator.cpp @@ -202,6 +202,7 @@ Vec2d place_wipe_tower(DynamicPrintConfig &cfg, const Vec2d ¢er) std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl, bool by_object) { const Vec2d center = printable_area_center(cfg); + const bool belt = cfg.opt_bool("belt_printer"); std::vector cube_mins; if (by_object) { // By-object printing fires the hook only without a wipe tower, and rules out clumping detection and @@ -212,6 +213,12 @@ std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl, cfg.set_key_value("timelapse_type", new ConfigOptionEnum(tlTraditional)); cfg.set_key_value("skirt_loops", new ConfigOptionInt(0)); cube_mins = {center + Vec2d(-20., -5.), center + Vec2d(10., -5.)}; + } else if (belt) { + // A belt object's slicing Z starts at the belt below its leading end, well below its first + // printed layer, so a height range in slicing Z does not map onto the part. Two cubes one + // behind the other along the belt, the second on filament 2, give the one filament change + // instead (the purge prism is an object the GUI adds, so there is no tower to place). + cube_mins = {center - Vec2d(5., 15.), center + Vec2d(-5., 5.)}; } else { // Clumping detection changes the tower footprint, so turn it on before placing the tower. if (!cfg.opt_string("wrapping_detection_gcode").empty()) @@ -229,14 +236,20 @@ std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl, obj->name = "cube"; // populates [input_filename_base] the way a loaded model does obj->add_volume(m); obj->add_instance(); - // Filament 2 is used only above z=4, so the upper layers carry a single filament change. - DynamicPrintConfig range_config; - range_config.set_key_value("extruder", new ConfigOptionInt(2)); - // Every range must carry a layer_height; use the process's own so a fine nozzle (e.g. 0.15 mm - // printing ~0.1 mm layers) isn't forced to a height its extrusion width can't support - that - // trips Flow::with_spacing. - range_config.set_key_value("layer_height", new ConfigOptionFloat(cfg.opt_float("layer_height"))); - obj->layer_config_ranges[{4.0, 10.0}].assign_config(std::move(range_config)); + if (belt && !by_object) { + // The second cube along the belt is on filament 2 (see cube_mins above). + if (&cube_min == &cube_mins.back()) + obj->config.set_key_value("extruder", new ConfigOptionInt(2)); + } else { + // Filament 2 is used only above z=4, so the upper layers carry a single filament change. + DynamicPrintConfig range_config; + range_config.set_key_value("extruder", new ConfigOptionInt(2)); + // Every range must carry a layer_height; use the process's own so a fine nozzle (e.g. 0.15 mm + // printing ~0.1 mm layers) isn't forced to a height its extrusion width can't support - that + // trips Flow::with_spacing. + range_config.set_key_value("layer_height", new ConfigOptionFloat(cfg.opt_float("layer_height"))); + obj->layer_config_ranges[{4.0, 10.0}].assign_config(std::move(range_config)); + } obj->ensure_on_bed(); print.auto_assign_extruders(obj); } From d527db6bdff0f593435b0028fc75ca75f2bd9166 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 04:27:32 -0500 Subject: [PATCH 202/224] Belt printer: classic tree branches land on the belt instead of sliding down it TreeSupportData added the belt surface to every layer's outlines, so the belt fed the classic tree's collision and avoidance maps. A node that descends onto an obstacle is pushed out of it, and on a belt that walked the branch down the tilted surface, ahead of the part, before it could end: tree hybrid/slim/strong supports swept far along the belt where organic supports dropped straight down. Take the belt out of the outlines. The belt is where a branch ends, and that is already handled: drop_nodes() stops a node once its whole circle is in the belt (belt_node_landed()) and draw_circles() clips every layer's circles to the belt plane, so the branch tapers to a tip on it. Co-Authored-By: Claude Fable 5.1 --- src/libslic3r/Support/TreeSupport.cpp | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index f8b1ad1f26..89a3a4987d 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -3927,18 +3927,13 @@ TreeSupportData::TreeSupportData(const PrintObject &object, coordf_t xy_distance poly.simplify(scale_(m_radius_sample_resolution), &outline); } - // Belt floor: add belt surface polygon to layer outlines so the - // collision system treats the belt as a physical surface. - { - 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())) { - Polygons belt_surface = ctx.surface_polygon(local_print_z); - for (auto &p : belt_surface) - outline.emplace_back(ExPolygon(p)); - } - } + // The belt surface is deliberately NOT part of the outlines. The outlines + // feed the collision and avoidance maps, and a node descending onto an + // obstacle is pushed out of it: with the belt as an obstacle the nodes slid + // down the tilted surface, ahead of the part, instead of landing on it. The + // belt is where a branch ENDS: drop_nodes() stops a node once its whole + // circle is in the belt (belt_node_landed()) and draw_circles() clips every + // layer's circles to the belt plane, so the branch tapers to a tip on it. if (layer_nr == 0) m_layer_outlines_below.push_back(outline); From 54e079ae25bd2949bb9dc77e91cbd4b42463e59a Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 16:08:28 -0500 Subject: [PATCH 203/224] Organic supports: restore the per-slice collision, bed and belt trim dda58b07cd stripped debug instrumentation from TreeSupport3D.cpp with a script, and that script also deleted the loop in organic_draw_branches() that trims every branch slice against the collision volume, the bed and, on a belt, the belt plane. This is the generator every printer uses, not a belt code path, and it is the one place where raistlin7447's export fixtures differed from main with belt printing off. Restore the loop as it was on main, with the belt-floor clip. The new test prints a cube carrying a 60 mm plate with organic supports on a flat-bed printer and checks on every support layer that no support extrusion comes within 0.2 mm of the part's slice. It guards that invariant; on this fixture the loop's own effect is a sub-millimetre reshaping of one branch (verified by slicing the fixture with and without the loop), below the asserted gap, so the test does not by itself fail without the loop. Co-Authored-By: Claude Fable 5.1 --- src/libslic3r/Support/TreeSupport3D.cpp | 7 ++ tests/fff_print/test_print.cpp | 94 +++++++++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/src/libslic3r/Support/TreeSupport3D.cpp b/src/libslic3r/Support/TreeSupport3D.cpp index dc284ba94f..ef267fba68 100644 --- a/src/libslic3r/Support/TreeSupport3D.cpp +++ b/src/libslic3r/Support/TreeSupport3D.cpp @@ -3952,6 +3952,13 @@ 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); } diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 32c37882f5..546f3667d5 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -31,10 +31,12 @@ #include "libslic3r/Support/TreeModelVolumes.hpp" #include "libslic3r/Support/TreeSupportCommon.hpp" #include "libslic3r/Support/BeltFloorContext.hpp" +#include "libslic3r/ClipperUtils.hpp" #include "libslic3r/ExtrusionEntity.hpp" #include "libslic3r/Polyline.hpp" #include #include +#include #include "libslic3r/Polygon.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/GCodeReader.hpp" @@ -1244,6 +1246,98 @@ TEST_CASE("Organic tree supports place a support blocker at its own height above CHECK(collides(last + num_raft)); } +// organic_draw_branches() trims every branch slice against the collision volume (the +// part grown by the support XY distance), the bed and, on a belt, the belt plane before +// it becomes support, so a branch never runs into the part it supports. Not a belt +// feature: this is the generator every printer uses. +TEST_CASE("Organic tree supports keep their distance from the part", "[Print][Support]") +{ + // A 20 mm cube carrying a 60 x 60 mm plate: a 20 mm wide ceiling all around the + // cube, 16 mm above the bed, with the cube's four corners in the way of the branches + // that drop from it. The plate reaches into the cube so the two shells overlap + // instead of sharing a face. + indexed_triangle_set its = its_make_cube(20., 20., 20.); + indexed_triangle_set plate = its_make_cube(60., 60., 4.); + its_translate(its, Vec3f(20.f, 20.f, 0.f)); + its_translate(plate, Vec3f(0.f, 0.f, 16.f)); + its_merge(its, plate); + TriangleMesh mesh(std::move(its)); + + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "skirt_loops", 0 }, + { "enable_support", 1 }, + { "support_type", "tree(auto)" }, + { "support_style", "organic" }, + { "support_threshold_angle", 30 }, + }); + Print print; + Model model; + init_print({ mesh }, print, model, config); + // On the bed, not at its corner (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(); + INFO("object layers " << object.layers().size() << ", support layers " << object.support_layers().size()); + REQUIRE(! object.support_layers().empty()); + // Support exists under the plate at all. + size_t support_layers_with_fills = 0; + for (const SupportLayer *layer : object.support_layers()) + if (! layer->support_fills.empty()) + ++ support_layers_with_fills; + INFO("support layers with extrusions " << support_layers_with_fills); + CHECK(support_layers_with_fills > 20); + + // Object layers by print_z, to look up the part's slice at a support layer's height. + std::map object_layers; + for (const Layer *layer : object.layers()) + object_layers[scaled(layer->print_z)] = layer; + auto contains = [](const ExPolygons &expolys, const Point &pt) { + for (const ExPolygon &ex : expolys) + if (ex.contains(pt)) + return true; + return false; + }; + // No support extrusion may run closer to the part's slice than half a line width: + // the generator keeps the support XY distance (0.35 mm by default) plus the line's + // own half width away from it. + const float min_gap = scaled(0.2); + size_t too_close = 0, points = 0, layers_checked = 0, layers_unmatched = 0; + for (const SupportLayer *layer : object.support_layers()) { + if (layer->support_fills.empty()) + continue; + // The object layer whose slab spans this support layer's height. + auto it = object_layers.lower_bound(scaled(layer->print_z - EPSILON)); + if (it == object_layers.end()) { + ++ layers_unmatched; + continue; + } + ++ layers_checked; + const ExPolygons grown = offset_ex(it->second->lslices, min_gap); + for (const ExtrusionEntity *entity : layer->support_fills.flatten().entities) + for (const Slic3r::Polyline &pl : entity->as_polylines()) + for (size_t i = 0; i < pl.points.size(); ++ i) { + // The vertices and the midpoints of the segments between them. + ++ points; + if (contains(grown, pl.points[i])) + ++ too_close; + if (i + 1 < pl.points.size() && contains(grown, (pl.points[i] + pl.points[i + 1]) / 2)) + ++ too_close; + } + } + INFO("support layers checked " << layers_checked << " (unmatched " << layers_unmatched << "), support points " << points + << ", within 0.2 mm of the part " << too_close); + CHECK(layers_checked > 20); + CHECK(layers_unmatched == 0); + REQUIRE(points > 0); + CHECK(too_close == 0); +} + // Two parts along the belt: the second part's slicing frame starts at the belt // below its leading end, so its first layers are empty and interleave with the // first part's printing layers. Those must not reach the G-code as layer changes From 4eaaf9a992974108b37471591344e4ee9ae69291 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 16:09:25 -0500 Subject: [PATCH 204/224] Belt G-code: first-layer speed test subtracts the instance offset only The writer hands set_first_layer_point_test() a point with the plate origin (its own XY offset) already removed, but the test subtracted the whole of m_origin, which carries the plate origin as well as the instance shift. On a plate other than the first the point was moved by the plate origin a second time and the band test looked at the wrong spot. Subtract only the part of m_origin that is not the writer's offset. Co-Authored-By: Claude Fable 5.1 --- src/libslic3r/GCode.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index c96d299211..6f4c15561f 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -3192,8 +3192,11 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato 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(); - return this->on_first_layer(Vec3d(point_logical.x() - m_origin.x() + extruder_offset.x(), - point_logical.y() - m_origin.y() + extruder_offset.y(), + // The writer hands over the point with the plate origin (its XY offset) already + // taken off, while m_origin still carries it: take off the instance part only. + const Vec2d plate_offset = m_writer.get_xy_offset().cast(); + return this->on_first_layer(Vec3d(point_logical.x() - (m_origin.x() - plate_offset.x()) + extruder_offset.x(), + point_logical.y() - (m_origin.y() - plate_offset.y()) + extruder_offset.y(), point_logical.z())); }); } From 05e37d838c8a23d9fa18a47f2b86a9969e15e3b1 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 16:09:25 -0500 Subject: [PATCH 205/224] Belt G-code: the mixed-filament sub-layer pass rotates its origin too process_layer()'s sub-layer pass (several filaments in one layer without a purge tower) calls set_origin() per instance like the main instance loop, but not on_set_origin(), which on a belt printer runs the origin through the belt transform. Add the call so both passes place the instance the same way. Co-Authored-By: Claude Fable 5.1 --- src/libslic3r/GCode.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 6f4c15561f..a2d2ea80af 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -7321,6 +7321,9 @@ LayerResult GCode::process_layer( m_avoid_crossing_perimeters.use_external_mp_once(); m_last_obj_copy = this_object_copy; this->set_origin(unscale(offset)); + // Same as the main instance loop: a belt printer rotates the origin through + // the belt transform (BeltGCode::on_set_origin). + this->on_set_origin(&instance_to_print.print_object, offset); // --- Build emission plan --- // Each entry represents one travel_to_z + extrude pass. Per-object mode produces From fba8b8b029275035707198a8f9a9eb6ecb57ebec Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 16:09:25 -0500 Subject: [PATCH 206/224] Belt G-code: reset the first-layer band state per object In by-object printing the cooling buffer is reset for every object, but m_belt_in_band, which tracks whether the extrusion is inside the band along the belt where the part fan stays off, kept the previous object's value. If the previous object ended inside the band the next one never emitted its band start marker. Reset it with the cooling buffer. Co-Authored-By: Claude Fable 5.1 --- src/libslic3r/GCode.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index a2d2ea80af..2b38fa0a51 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -4176,6 +4176,9 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Reset the cooling buffer internal state (the current position, feed rate, accelerations). m_cooling_buffer->set_current_extruder(initial_extruder_id, get_extruder_id(initial_extruder_id)); m_cooling_buffer->reset(this->writer().get_position()); + // The belt first-layer band is tracked per object as well: if the previous + // object ended inside the band, this one has to open its own. + m_belt_in_band = false; // Process all layers of a single object instance (sequential mode) with a parallel pipeline: // Generate G-code, run the filters (vase mode, cooling buffer), run the G-code analyser // and export G-code into file. From febd82f6dfd35b3269348dd202df1f70f08123e5 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 16:09:25 -0500 Subject: [PATCH 207/224] Belt G-code: count only the layers that are written collect_layers_to_print() drops the belt layers that print nothing (an object's empty lead-in), but m_layer_count still counted every object and support layer, so "total layers count", the total_layer_count placeholder and the M73 progress disagreed with the layer changes in the file. Count with the same predicate, shared through belt_object_layer_prints_something(). The by-object overload of collect_layers_to_print() now drops those layers as well, so both print sequences write the same layer changes. Dropping the empty entries per object has one more effect on multi- filament belt prints: a layer no longer selects a filament that it then prints nothing with. On belt_project.3mf (two filaments, belt purge tower) the T commands go from 472 to 106 while the extruded length per filament is unchanged; each of the removed tool changes was followed by no extrusion. The empty-layers test now runs for both print sequences and also checks "total layers count" against the layer changes. Co-Authored-By: Claude Fable 5.1 --- src/libslic3r/GCode.cpp | 52 +++++++++++++++++++++++++++------- tests/fff_print/test_print.cpp | 14 +++++++-- 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 2b38fa0a51..6797f34821 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2222,6 +2222,22 @@ void GCode::PlaceholderParserIntegration::validate_output_vector_variables() // Collect pairs of object_layer + support_layer sorted by print_z. // object_layer & support_layer are considered to be on the same print_z, if they are not further than EPSILON. +// Belt printers: whether an object layer writes anything, its own extrusions or a belt +// brim band riding on it. Shared by collect_layers_to_print() (which drops the layers +// that do not) and the layer count. +static bool belt_object_layer_prints_something(const PrintObject &object, const Layer &layer) +{ + if (layer.has_extrusions()) + return true; + if (object.has_belt_brim()) { + const auto &by_layer = object.belt_brim_by_layer(); + const size_t id = layer.id(); + if (id < by_layer.size() && ! by_layer[id].empty()) + return true; + } + return false; +} + std::vector GCode::collect_layers_to_print(const PrintObject& object, bool skip_empty_first_layer) { std::vector layers_to_print; @@ -2378,6 +2394,17 @@ std::vector GCode::collect_layers_to_print(const PrintObjec PrintStateBase::WarningLevel::CRITICAL, warning, PrintStateBase::SlicingEmptyGcodeLayers); } + // Belt printers: drop the layers that print nothing (see the by-layer overload), so + // the by-object export writes the same layer changes as the by-layer one. + if (object.print()->config().belt_printer.value) + layers_to_print.erase( + std::remove_if(layers_to_print.begin(), layers_to_print.end(), [&object](const LayerToPrint <p) { + return ! ((ltp.object_layer != nullptr && belt_object_layer_prints_something(object, *ltp.object_layer)) || + (ltp.support_layer != nullptr && ltp.support_layer->has_extrusions()) || + (ltp.belt_brim_band != nullptr && ! ltp.belt_brim_band->fills.empty())); + }), + layers_to_print.end()); + return layers_to_print; } @@ -2448,18 +2475,13 @@ std::vector>> GCode::collec // moves it sees, so a gap folds every later layer into the one before it. if (print.config().belt_printer.value) { auto prints_something = [](const LayerToPrint <p) { - if (ltp.object_layer != nullptr && ltp.object_layer->has_extrusions()) + if (ltp.object_layer != nullptr && ltp.original_object != nullptr && + belt_object_layer_prints_something(*ltp.original_object, *ltp.object_layer)) return true; if (ltp.support_layer != nullptr && ltp.support_layer->has_extrusions()) return true; if (ltp.belt_brim_band != nullptr && ! ltp.belt_brim_band->fills.empty()) return true; - if (ltp.object_layer != nullptr && ltp.original_object != nullptr && ltp.original_object->has_belt_brim()) { - const auto &by_layer = ltp.original_object->belt_brim_by_layer(); - const size_t id = ltp.object_layer->id(); - if (id < by_layer.size() && ! by_layer[id].empty()) - return true; - } return false; }; layers_to_print.erase( @@ -3204,15 +3226,21 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // How many times will be change_layer() called? // change_layer() in turn increments the progress bar status. m_layer_count = 0; + // On a belt, collect_layers_to_print() drops the layers that print nothing (an + // object's empty lead-in), so they must not be counted here either or the layer + // count in the file disagrees with its layer changes. + const bool belt = print.config().belt_printer.value; if (print.config().print_sequence == PrintSequence::ByObject) { // Add each of the object's layers separately. for (auto object : print.objects()) { std::vector zs; zs.reserve(object->layers().size() + object->support_layers().size()); for (auto layer : object->layers()) - zs.push_back(layer->print_z); + if (! belt || belt_object_layer_prints_something(*object, *layer)) + zs.push_back(layer->print_z); for (auto layer : object->support_layers()) - zs.push_back(layer->print_z); + if (! belt || layer->has_extrusions()) + zs.push_back(layer->print_z); // Belt brim apron bands each get their own change_layer() call. for (const BeltBrimBand &band : object->belt_brim_prologue()) zs.push_back(band.print_z); @@ -3232,9 +3260,11 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato for (auto object : print.objects()) { zs.reserve(zs.size() + object->layers().size() + object->support_layers().size()); for (auto layer : object->layers()) - zs.push_back(layer->print_z); + if (! belt || belt_object_layer_prints_something(*object, *layer)) + zs.push_back(layer->print_z); for (auto layer : object->support_layers()) - zs.push_back(layer->print_z); + if (! belt || layer->has_extrusions()) + zs.push_back(layer->print_z); // See the ByObject branch: apron bands are real printed layers. for (const BeltBrimBand &band : object->belt_brim_prologue()) zs.push_back(band.print_z); diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 546f3667d5..a84bc3b451 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -1362,6 +1362,9 @@ TEST_CASE("Belt G-code has no layer that prints nothing", "[Print][belt][GCode][ { "machine_start_gcode", "T[initial_tool]\n" }, { "layer_change_gcode", "G92 E0\n" }, }); + // Both export paths drop the empty layers and count the layers the same way. + SECTION("by layer") { config.set_deserialize_strict({{ "print_sequence", "by layer" }}); } + SECTION("by object") { config.set_deserialize_strict({{ "print_sequence", "by object" }}); } Print print; Model model; TriangleMesh cube_a(its_make_cube(20., 20., 20.)); @@ -1376,7 +1379,7 @@ TEST_CASE("Belt G-code has no layer that prints nothing", "[Print][belt][GCode][ const std::string gc = gcode(print); REQUIRE(! gc.empty()); - size_t layers = 0, empty = 0, total_header = 0; + size_t layers = 0, empty = 0, total_header = 0, total_count = 0; bool extruded = true; // before the first layer change std::istringstream in(gc); std::string line; @@ -1387,17 +1390,24 @@ TEST_CASE("Belt G-code has no layer that prints nothing", "[Print][belt][GCode][ ++ layers; extruded = false; } else if (line.rfind("; total layer number: ", 0) == 0) { + // Counted by the G-code processor from the layer changes it saw. total_header = size_t(std::atoi(line.c_str() + 22)); + } else if (line.rfind("; total layers count = ", 0) == 0) { + // GCode::m_layer_count, counted up front from the objects' layers; it also + // drives the M73 progress and the total_layer_count placeholder. + total_count = size_t(std::atoi(line.c_str() + 23)); } else if (! extruded && line.rfind("G1 ", 0) == 0 && line.find('E') != std::string::npos && (line.find('X') != std::string::npos || line.find('Y') != std::string::npos)) { extruded = true; } } close_layer(); - INFO("layers " << layers << ", header " << total_header << ", layers without extrusion " << empty); + INFO("layers " << layers << ", header " << total_header << ", count " << total_count + << ", layers without extrusion " << empty); CHECK(layers > 150); // both cubes, 141 layers each, overlapping along the belt CHECK(empty == 0); CHECK(total_header == layers); + CHECK(total_count == layers); } // A part with an overhang on its LEADING side (the end that prints first) needs From b2fb552e325694ab6c27bef383fc2ce6157edc05 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 16:09:25 -0500 Subject: [PATCH 208/224] PrintObject: a re-slice invalidates posDetectOverhangsForLift posSlice's invalidation list did not include posDetectOverhangsForLift. A re-slice starts the layers over with empty overhang regions while the step stayed done, so GCode::needs_retraction() had no overhangs to test against until something else invalidated it. Co-Authored-By: Claude Fable 5.1 --- src/libslic3r/PrintObject.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 4f46921c3f..8b5e115be3 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1767,7 +1767,11 @@ bool PrintObject::invalidate_step(PrintObjectStep step) // posSimplifySupportPath is listed with posSupportMaterial: invalidate_steps() does not // propagate, so without it a re-slice regenerated the supports but kept the step done, // and the new support paths were exported unsimplified, unlike a fresh slice. - invalidated |= this->invalidate_steps({ posPerimeters, posPrepareInfill, posInfill, posIroning, posContouring, posSupportMaterial, posSimplifyPath, posSimplifyInfill, posSimplifySupportPath }); + // posDetectOverhangsForLift reads the layers' overhang regions, which a re-slice + // starts over empty: without it here the step stayed done and the lift logic in + // GCode::needs_retraction() had no overhangs to test against until something else + // invalidated it. + invalidated |= this->invalidate_steps({ posPerimeters, posPrepareInfill, posInfill, posIroning, posContouring, posSupportMaterial, posSimplifyPath, posSimplifyInfill, posSimplifySupportPath, posDetectOverhangsForLift }); invalidated |= m_print->invalidate_steps({ psSkirtBrim }); m_slicing_params.valid = false; // The exact belt_floor_z_shift is recomputed when slice() runs again. From 9289fc4dd33572b6ca4cf16c5ff7dad15c7ba261 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 16:09:26 -0500 Subject: [PATCH 209/224] Leading-edge brim takes part in the outer-wall-first rule The first-layer rule that prints the outer wall first when a brim is attached to it, and the brim_type change rule that regenerates the perimeters for it, only knew btOuterOnly. btLeadingEdgeOnly, the belt brim at the part's first contact, is an outer brim too. Co-Authored-By: Claude Fable 5.1 --- src/libslic3r/PerimeterGenerator.cpp | 4 +++- src/libslic3r/PrintObject.cpp | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/PerimeterGenerator.cpp b/src/libslic3r/PerimeterGenerator.cpp index 41db0ed9db..b1c6ed21fa 100644 --- a/src/libslic3r/PerimeterGenerator.cpp +++ b/src/libslic3r/PerimeterGenerator.cpp @@ -1839,8 +1839,10 @@ void PerimeterGenerator::process_classic() bool is_outer_wall_first = this->config->wall_sequence == WallSequence::OuterInner; if (is_outer_wall_first || //BBS: always print outer wall first when there indeed has brim. + // btLeadingEdgeOnly is an outer brim too (a belt brim at the part's first contact). (this->layer_id == 0 && - this->object_config->brim_type == BrimType::btOuterOnly && + (this->object_config->brim_type == BrimType::btOuterOnly || + this->object_config->brim_type == BrimType::btLeadingEdgeOnly) && this->object_config->brim_width.value > 0)) entities.reverse(); // Orca: sandwich mode. Apply after 1st layer. diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 8b5e115be3..c2abb5466a 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1373,7 +1373,10 @@ bool PrintObject::invalidate_state_by_config_options( const auto* new_brim_type = new_config.option>(opt_key); //BBS: When switch to manual brim, the object must have brim, then re-generate perimeter //to make the wall order of first layer to be outer-first - if (old_brim_type->value == btOuterOnly || new_brim_type->value == btOuterOnly) + // btLeadingEdgeOnly is printed as an outer brim (Brim.cpp, BeltBrim.cpp), so it + // takes part in the same first-layer wall order rule. + if (old_brim_type->value == btOuterOnly || new_brim_type->value == btOuterOnly || + old_brim_type->value == btLeadingEdgeOnly || new_brim_type->value == btLeadingEdgeOnly) steps.emplace_back(posPerimeters); } } else if ( From d5cfaae5dd33ba0f708d55b43b5d6be383a2bdee Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 16:09:26 -0500 Subject: [PATCH 210/224] Belt brim: adding or removing an object rebuilds the other objects' brims A belt brim is clipped against the other objects on the plate and is built with its object's support step. When an object was added or deleted only the print-level skirt/brim and export steps were invalidated, so the remaining objects kept brims clipped against objects that were no longer there, or overlapping ones that had arrived. Invalidate posSupportMaterial on every object in that case on a belt printer. Also reword the comments that still described the global Z offset as a minimum across all objects. Co-Authored-By: Claude Fable 5.1 --- src/libslic3r/PrintApply.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index 39d5716b08..53f93f5b69 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1916,6 +1916,12 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ } if (new_objects || deleted_objects) update_apply_status(this->invalidate_steps({ psSkirtBrim, psWipeTower, psGCodeExport })); + // A belt brim is clipped against the other objects on the plate (BeltBrim.cpp, + // belt_brim_obstacles), and it is rebuilt with its object's support step: an + // object that arrived or left changes every other object's brim. + if ((new_objects || deleted_objects) && m_config.belt_printer.value) + for (PrintObject *object : m_objects) + update_apply_status(object->invalidate_step(posSupportMaterial)); if (new_objects) update_apply_status(false); print_regions_reshuffled = true; @@ -1930,9 +1936,9 @@ 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). + // Belt printer: when any object's instances shifted, re-slice every object. + // The global Z offset follows each object's position along the belt, and the + // belt brims are clipped against the other objects. if (belt_instances_shifted && m_config.belt_printer.value) { for (PrintObject *object : m_objects) update_apply_status(object->invalidate_step(posSlice)); From 798ca8272e318afa1c063526413e8b1106b2cb1d Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 16:09:26 -0500 Subject: [PATCH 211/224] Printer settings: build plate tilt is read-only on a belt printer update_fff() derives build_plate_tilt_x/y from the belt tilt on a belt printer, so a value typed into those fields was silently overwritten. Disable the two fields while belt_printer is on. Co-Authored-By: Claude Fable 5.1 --- src/slic3r/GUI/Tab.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index bd19a62024..f5a0e62458 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -6349,6 +6349,10 @@ void TabPrinter::toggle_options() // Belt printer: show belt-specific settings only when belt_printer is enabled. bool is_belt = m_config->opt_bool("belt_printer"); + // update_fff() derives build_plate_tilt_{x,y} from the belt tilt on a belt + // printer, so an edit here would be overwritten; keep them read-only there. + toggle_option("build_plate_tilt_x", !is_belt); + toggle_option("build_plate_tilt_y", !is_belt); 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). From e78f437ce4ec97742ea5d49ab3252e7dec5a1027 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 16:09:26 -0500 Subject: [PATCH 212/224] Belt temperature tower: refuse a range without a model The calibration fell back to the 230-190 tower when no embossed model existed for the requested range, so the printed numbers did not match the temperatures. Show an error naming the range and stop instead. Co-Authored-By: Claude Fable 5.1 --- src/slic3r/GUI/Plater.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 86ed871ed6..6542abb8f6 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -17080,9 +17080,15 @@ void Plater::calib_temp(const Calib_Params& params) { const std::string calib_dir = Slic3r::resources_dir() + "/calib/temperature_tower/"; std::string asset = calib_dir + "belt_temp_tower_" + std::to_string(t_start) + "_" + std::to_string(t_end) + ".stl"; if (!boost::filesystem::exists(asset)) { - BOOST_LOG_TRIVIAL(warning) << "[belt_temp] no embossed provini for " << t_start << "->" << t_end - << ", falling back to 230_190 (embossed numbers will not match)"; - asset = calib_dir + "belt_temp_tower_230_190.stl"; + // The embossed numbers are part of the model, so another model's tower would + // print numbers that do not match its temperatures. + MessageDialog dlg(static_cast(wxGetApp().mainframe), + format_wxstr(_L("No belt temperature tower is available for the range %1% to %2% °C. " + "Use a range the tower models cover, for example 230 to 190."), + t_start, t_end), + _L("Temperature tower"), wxICON_ERROR | wxOK); + dlg.ShowModal(); + return; } if (!add_model(false, asset) || model().objects.empty()) return; From 1fb585dc5c268fa0a4b1386490b8980dc5a41a56 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 16:09:26 -0500 Subject: [PATCH 213/224] Belt config: fix the tilt tooltip math and extract the remap strings The machine-frame transform is a shear of cot(tilt) and a scale of 1/sin(tilt), not tan and 1/cos; fix the tooltip and the matching comment in BeltGCode.cpp. The gcode_remap_* labels and tooltips were passed through L() as variables inside a lambda, which the string extraction does not see; pass L("literal") at the call sites. Co-Authored-By: Claude Fable 5.1 --- src/libslic3r/BeltGCode.cpp | 2 +- src/libslic3r/PrintConfig.cpp | 28 +++++++++++++++++----------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/libslic3r/BeltGCode.cpp b/src/libslic3r/BeltGCode.cpp index 1726fc6eac..54d9abe30f 100644 --- a/src/libslic3r/BeltGCode.cpp +++ b/src/libslic3r/BeltGCode.cpp @@ -25,7 +25,7 @@ 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); - // Machine-frame transform: shear (tan) + scale (1/cos) derived from the belt + // Machine-frame transform: shear (cot) + scale (1/sin) 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); file.write_format("; belt_frame_tilt_angle = %.1f\n", print.config().belt_frame_tilt_angle.value); diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 503e7c464b..6723b903fd 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -7414,8 +7414,8 @@ void PrintConfigDef::init_fff_params() def = this->add("belt_frame_tilt_angle", coFloat); def->label = L("Machine-frame tilt angle"); def->category = L("Printable space"); - def->tooltip = L("Tilt angle (degrees) used to derive the machine-frame shear (tan) and " - "scale (1/cos) applied to G-code. Only used when 'Decouple machine-frame " + def->tooltip = L("Tilt angle (degrees) used to derive the machine-frame shear (cot) and " + "scale (1/sin) applied to G-code. Only used when 'Decouple machine-frame " "tilt' is enabled; otherwise the belt tilt angle is used."); def->sidetext = L("°"); def->min = -89.9; @@ -7423,23 +7423,29 @@ void PrintConfigDef::init_fff_params() def->mode = comExpert; def->set_default_value(new ConfigOptionFloat(45.)); - // 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) { + // G-code axis remap with sign. Each field is its own row in the settings tab. The + // labels and tooltips are literals in L() so they are extracted for translation. + auto add_belt_remap = [this](const char *key, const std::string &label, const std::string &tooltip, + RemapAxis default_axis, ConfigOptionMode mode) { auto def = this->add(key, coEnum); - def->label = L(label); + def->label = label; def->category = L("Printable space"); - def->tooltip = L(tooltip); + def->tooltip = tooltip; def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); def->enum_values = {"pos_x", "pos_y", "pos_z", "neg_x", "neg_y", "neg_z", "rev_x", "rev_y", "rev_z"}; def->enum_labels = {L("+X"), L("+Y"), L("+Z"), L("-X"), L("-Y"), L("-Z"), L("Rev X"), L("Rev Y"), L("Rev Z")}; def->mode = mode; // Visibility may also be gated by toggle_line in Tab.cpp def->set_default_value(new ConfigOptionEnum(default_axis)); }; - - 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); + add_belt_remap("gcode_remap_x", L("G-code remap X"), + L("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", L("G-code remap Y"), + L("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", L("G-code remap Z"), + L("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 From 441113cf3b99f829c814ff910762ce84a3ab105b Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 16:09:26 -0500 Subject: [PATCH 214/224] Belt: remove dead code belt_remapped_bbox() had no callers; belt_min_z() and m_belt_global_xy_correction were written but never read; LayerTools::has_belt_brim was set but never read; belt_surface_z was a named zero. Co-Authored-By: Claude Fable 5.1 --- src/libslic3r/GCode/ToolOrdering.cpp | 2 -- src/libslic3r/GCode/ToolOrdering.hpp | 4 ---- src/libslic3r/Print.hpp | 10 ---------- src/libslic3r/PrintObjectSlice.cpp | 7 ++----- 4 files changed, 2 insertions(+), 21 deletions(-) diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index 979371e304..f61587d562 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -1023,7 +1023,6 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto continue; LayerTools &layer_tools = this->tools_for_layer(band.print_z); layer_tools.extruders.push_back(brim_filament); - layer_tools.has_belt_brim = true; } } @@ -1042,7 +1041,6 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto continue; LayerTools &layer_tools = this->tools_for_layer(object.layers()[i]->print_z); layer_tools.extruders.push_back(brim_filament); - layer_tools.has_belt_brim = true; } } diff --git a/src/libslic3r/GCode/ToolOrdering.hpp b/src/libslic3r/GCode/ToolOrdering.hpp index 3d42457e1a..5c11f482cd 100644 --- a/src/libslic3r/GCode/ToolOrdering.hpp +++ b/src/libslic3r/GCode/ToolOrdering.hpp @@ -179,10 +179,6 @@ public: // Should a skirt be printed at this layer? // Layers are marked for infinite skirt aka draft shield. Not all the layers have to be printed. bool has_skirt = false; - // Belt printers: is this one of the brim-only apron layers below the object's - // first layer? Kept separate from has_object so skirt marking and wiping - // overrides are unaffected. - bool has_belt_brim = false; // Will there be anything extruded on this layer for the wipe tower? // Due to the support layers possibly interleaving the object layers, // wipe tower will be disabled for some support only layers. diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index f60f6b4e19..c726f7b79f 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -231,12 +231,6 @@ class ConstSupportLayerPtrsAdaptor : public ConstVectorOfPtrsAdaptor(data) {} }; -// 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 model_object.raw_bounding_box(); -} - // Single instance of a PrintObject. // As multiple PrintObjects may be generated for a single ModelObject (their instances differ in rotation around Z), // ModelObject's instancess will be distributed among these multiple PrintObjects. @@ -721,8 +715,6 @@ private: 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. - Vec2d m_belt_global_xy_correction { Vec2d::Zero() }; // Belt printer: exact belt_floor_z_shift computed during posSlice from a // vertex-level scan of the post-transform mesh. Cached separately from // m_slicing_params so that rebuilding m_slicing_params on a non-belt-affecting @@ -733,8 +725,6 @@ private: bool m_belt_floor_z_shift_cache_valid { false }; public: double belt_global_z_offset() const { return m_belt_global_z_offset; } - double belt_min_z() const { return m_belt_min_z; } - Vec2d belt_global_xy_correction() const { return m_belt_global_xy_correction; } private: diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 5f70887c9c..06e71a60b3 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -902,7 +902,6 @@ void PrintObject::slice() // them. m_belt_min_z = 0.; m_belt_global_z_offset = 0.; - m_belt_global_xy_correction = Vec2d::Zero(); this->clear_layers(); m_layers = new_layers(this, generate_object_layers(m_slicing_params, layer_height_profile, m_config.precise_z_height.value)); this->slice_volumes(); @@ -995,7 +994,6 @@ 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. - 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 @@ -1003,7 +1001,7 @@ void PrintObject::slice() // no lift was applied, and an unclamped m_belt_min_z here would // leak straight into the layer Z values, floating the whole object // off the belt by exactly that amount. - double belt_z_shift = std::min(m_belt_min_z, 0.) - belt_surface_z; + double belt_z_shift = std::min(m_belt_min_z, 0.); // the belt surface is Z=0 in centered slicer space double global_z_offset = belt_z_shift; // Centering correction: trafo_centered pretranslates by @@ -1034,7 +1032,6 @@ void PrintObject::slice() Vec3d d(unscale(inst_shift.x()), unscale(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()); BOOST_LOG_TRIVIAL(trace) << "Belt preslice_global: correction=(" << c.x() << ", " << c.y() << ", " << c.z() << ")" @@ -1042,7 +1039,7 @@ void PrintObject::slice() } BOOST_LOG_TRIVIAL(trace) << "Belt global: z_offset=" << global_z_offset - << " (relative to min across " << this->print()->objects().size() << " objects)"; + << " (" << this->print()->objects().size() << " objects on the plate)"; m_belt_global_z_offset = global_z_offset; if (std::abs(global_z_offset) > EPSILON) { for (Layer *layer : m_layers) From a564ec23fe2a001cb9bfb63ecd196d44542c39a6 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 16:09:26 -0500 Subject: [PATCH 215/224] Belt: refresh comments that described earlier code BeltBrim.cpp still described the brim as running inside the parallel support step; it runs sequentially after it (generate_belt_brim). The GCodeWriter, calib.cpp and calib.hpp comments referred to an inheritance layout and a dynamic_cast that no longer exist. Co-Authored-By: Claude Fable 5.1 --- src/libslic3r/BeltBrim.cpp | 15 ++++++--------- src/libslic3r/GCodeWriter.hpp | 4 ++-- src/libslic3r/calib.cpp | 3 +-- src/libslic3r/calib.hpp | 6 +++--- 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/libslic3r/BeltBrim.cpp b/src/libslic3r/BeltBrim.cpp index 3f2507f24e..66a271384f 100644 --- a/src/libslic3r/BeltBrim.cpp +++ b/src/libslic3r/BeltBrim.cpp @@ -376,15 +376,12 @@ static void belt_brim_band_paths(const BeltBrimContext &bc, // overhang outside the belt footprint and land in the brim ring, which the flattened // brim_object_gap - a belt-plane separation - does not cover. // -// THREADING: this runs inside posSupportMaterial, which Print::process() executes for all -// objects in a tbb::parallel_for (Print.cpp). Object slices are finished by then and safe -// to read across objects, but SUPPORT layers are not: another object's thread may be -// inside clear_support_layers() - which deletes the SupportLayer pointers - right now, so -// touching a foreign object's support_layers() here is a use-after-free. Only this -// object's own supports are consulted; they are complete, because make_belt_brim() runs at -// the tail of this object's own generate_support_material(). The cost is that the brim -// does not dodge a *different* object's support at the same Z, which needs the objects to -// overlap in the belt direction in the first place. +// SEQUENCING: this reads the layers and support layers of every object on the plate, so +// it must not overlap with another object's support step, which rebuilds them. +// Print::process() therefore generates the belt brims one object after the other once the +// parallel support step is over (PrintObject::generate_belt_brim()), and an object that +// arrives on or leaves the plate invalidates every other object's support step +// (PrintApply.cpp) so the brims are clipped against what is there now. // `region_bbox` bounds the brim; anything outside it cannot clip a brim line, so whole // objects are skipped without materialising their polygons. On a typical plate the // objects do not overlap and every foreign object drops out here, which matters because diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index 88be72a6f1..12a56dacdc 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -236,8 +236,8 @@ protected: Vec3d apply_axis_remap(const Vec3d &pos) const; // Motion uses the global/base process variant until a filament becomes active. - // Protected so subclasses index the per-extruder speed options (travel_speed, - // travel_speed_z, initial_layer_travel_speed) exactly as the base writer does. + // Indexes the per-extruder speed options (travel_speed, travel_speed_z, + // initial_layer_travel_speed). size_t m_cached_extruder_idx; private: diff --git a/src/libslic3r/calib.cpp b/src/libslic3r/calib.cpp index cdf105fa69..3217edf1da 100644 --- a/src/libslic3r/calib.cpp +++ b/src/libslic3r/calib.cpp @@ -896,8 +896,7 @@ void CalibPressureAdvancePattern::_refresh_writer(bool is_bbl_machine, const Mod } m_writer = std::move(belt_writer); } else if (m_writer && dynamic_cast(&m_writer->kinematics()) != nullptr) { - // Previously configured for a belt printer; drop back to a plain writer, - // exactly as the old dynamic_cast check did. + // Previously configured for a belt printer; drop back to a plain writer. m_writer = std::make_shared(); } diff --git a/src/libslic3r/calib.hpp b/src/libslic3r/calib.hpp index 1f1c7844dc..4ef676cdb1 100644 --- a/src/libslic3r/calib.hpp +++ b/src/libslic3r/calib.hpp @@ -370,9 +370,9 @@ private: const Calib_Params &m_params; - // Polymorphic so belt printers get belt kinematics in world-coordinates - // mode (_refresh_writer); shared_ptr keeps the class copyable — the writer - // is rebuilt by refresh_setup() before every use anyway. + // Belt printers get belt kinematics installed on it (_refresh_writer); + // shared_ptr keeps the class copyable — the writer is rebuilt by + // refresh_setup() before every use anyway. std::shared_ptr m_writer{std::make_shared()}; Vec3d m_starting_point; bool m_is_start_point_fixed = false; From cfad587c2d6662666a72a1ab52de0aedeb701b5c Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 17:31:48 -0500 Subject: [PATCH 216/224] Belt: follow-ups from a second review pass Guard the layer count and the per-object layer collection against an object that is left without a layer to print on a belt (the counting loop stepped before begin() and front() was taken of an empty vector). Check the belt temperature tower's embossed model before the current project is replaced, not after. Only invalidate the support step of objects that own a belt brim when an object is added or removed. The empty-layers test now counts an extrusion only where material is laid down along a move. The BeltBrim.cpp SEQUENCING note says exactly which layers are read, and the machine-frame scale is 1/|sin|. Remove more code that nothing calls: the kinematics inverse (to_logical, apply_axis_remap_inverse, to_build_volume and the state kept for them), the world_coordinates(), is_active() and belt_brim_areas_by_layer() accessors, the PrintConfig overload of physical_tilt() and the DynamicPrintConfig overload of compute_belt_height_and_floor(). Comments in GCode.hpp, BeltSliceStrategy.hpp/.cpp and PrintObjectSlice.cpp that described the retired pre-slice remap and plane-evaluator still did; the purge-tower width tooltip named the wrong switch. Co-Authored-By: Claude Fable 5.1 --- src/libslic3r/BeltBrim.cpp | 14 ++++---- src/libslic3r/BeltGCode.cpp | 2 +- src/libslic3r/BeltSliceStrategy.cpp | 3 +- src/libslic3r/BeltSliceStrategy.hpp | 20 +++++------ src/libslic3r/BeltTransform.cpp | 34 +++---------------- src/libslic3r/BeltTransform.hpp | 11 ------ src/libslic3r/GCode.cpp | 5 +++ src/libslic3r/GCode.hpp | 13 ------- src/libslic3r/GCode/BeltBackTransform.hpp | 3 -- src/libslic3r/GCode/BeltKinematics.cpp | 15 +-------- src/libslic3r/GCode/BeltKinematics.hpp | 11 ------ src/libslic3r/GCode/MachineKinematics.cpp | 23 ------------- src/libslic3r/GCode/MachineKinematics.hpp | 14 -------- src/libslic3r/Print.hpp | 3 -- src/libslic3r/PrintApply.cpp | 5 +-- src/libslic3r/PrintConfig.cpp | 4 +-- src/libslic3r/PrintObjectSlice.cpp | 25 +++++--------- src/slic3r/GUI/Plater.cpp | 41 ++++++++++++++++------- tests/fff_print/test_print.cpp | 23 +++++++------ 19 files changed, 83 insertions(+), 186 deletions(-) diff --git a/src/libslic3r/BeltBrim.cpp b/src/libslic3r/BeltBrim.cpp index 66a271384f..a25659b30e 100644 --- a/src/libslic3r/BeltBrim.cpp +++ b/src/libslic3r/BeltBrim.cpp @@ -376,12 +376,14 @@ static void belt_brim_band_paths(const BeltBrimContext &bc, // overhang outside the belt footprint and land in the brim ring, which the flattened // brim_object_gap - a belt-plane separation - does not cover. // -// SEQUENCING: this reads the layers and support layers of every object on the plate, so -// it must not overlap with another object's support step, which rebuilds them. -// Print::process() therefore generates the belt brims one object after the other once the -// parallel support step is over (PrintObject::generate_belt_brim()), and an object that -// arrives on or leaves the plate invalidates every other object's support step -// (PrintApply.cpp) so the brims are clipped against what is there now. +// SEQUENCING: this reads every object's layers and this object's own support layers. +// Another object's support step shifts that object's layer Z into the object frame for +// the duration of the run (PrintObject::_generate_support_material()), so the brims must +// not overlap with the parallel support step: Print::process() generates them one object +// after the other once that step is over (PrintObject::generate_belt_brim()), and an +// object that arrives on or leaves the plate invalidates the other brim owners' support +// step (PrintApply.cpp) so their brims are clipped against what is there now. Only this +// object's supports are dodged; another object's support at the same Z is not. // `region_bbox` bounds the brim; anything outside it cannot clip a brim line, so whole // objects are skipped without materialising their polygons. On a typical plate the // objects do not overlap and every foreign object drops out here, which matters because diff --git a/src/libslic3r/BeltGCode.cpp b/src/libslic3r/BeltGCode.cpp index 54d9abe30f..e76e76b84f 100644 --- a/src/libslic3r/BeltGCode.cpp +++ b/src/libslic3r/BeltGCode.cpp @@ -25,7 +25,7 @@ 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); - // Machine-frame transform: shear (cot) + scale (1/sin) derived from the belt + // Machine-frame transform: shear (cot) + scale (1/|sin|) 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); file.write_format("; belt_frame_tilt_angle = %.1f\n", print.config().belt_frame_tilt_angle.value); diff --git a/src/libslic3r/BeltSliceStrategy.cpp b/src/libslic3r/BeltSliceStrategy.cpp index 8aac729f1b..7b25ac53ec 100644 --- a/src/libslic3r/BeltSliceStrategy.cpp +++ b/src/libslic3r/BeltSliceStrategy.cpp @@ -73,8 +73,7 @@ void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo, z_shift.matrix()(2, 3) = z_shift_val; trafo = z_shift * trafo; } - // out_belt_min_z is only meaningful in belt mode; the standalone-remap path - // never reported it. + // out_belt_min_z is only meaningful in belt mode. if (out_belt_min_z && config.belt_printer.value) { *out_belt_min_z = (min_z != std::numeric_limits::max()) ? min_z : 0.; } diff --git a/src/libslic3r/BeltSliceStrategy.hpp b/src/libslic3r/BeltSliceStrategy.hpp index 33a7ed4979..1385818b5c 100644 --- a/src/libslic3r/BeltSliceStrategy.hpp +++ b/src/libslic3r/BeltSliceStrategy.hpp @@ -8,25 +8,23 @@ namespace Slic3r { -// Belt printer / pre-slice transform strategy. +// Belt printer pre-slice transform strategy. // -// Composes, in order, the pre-slice mesh transforms applied before slicing: -// 1. Pre-slice axis remap (standalone — works without belt mode) -// 2. Belt rotation (the sole mesh-side belt transform; shear & scale are a +// Composes, in order, the mesh transforms applied before slicing on a belt printer: +// 1. Belt rotation (the sole mesh-side belt transform; shear & scale are a // g-code-side stage, see MachineFrameTransform) -// 3. Per-object Z-shift that lifts the mesh above the build plate +// 2. Per-object Z-shift that lifts the mesh so its slicing frame starts at the +// belt below its footprint // -// Isolates this belt/remap-specific logic from the generic slicing pipeline in +// Isolates this belt-specific logic from the generic slicing pipeline in // PrintObjectSlice.cpp. class BeltSliceStrategy { public: - // Apply the pre-slice remap + belt rotation + Z-shift to `trafo` in place. - // No-op when neither a remap nor a belt rotation is configured. + // Apply the belt rotation + Z-shift to `trafo` in place. No-op when no belt + // rotation is configured. // - // out_belt_min_z (if non-null) receives the minimum mesh Z after the - // transforms, but only in belt-printer mode — the standalone-remap path - // never reported it. + // out_belt_min_z (if non-null) receives the minimum mesh Z after the transforms. static void apply_preslice_transforms(Transform3d &trafo, const PrintConfig &config, const ModelVolumePtrs &model_volumes, diff --git a/src/libslic3r/BeltTransform.cpp b/src/libslic3r/BeltTransform.cpp index d11e2fccdc..9863e80831 100644 --- a/src/libslic3r/BeltTransform.cpp +++ b/src/libslic3r/BeltTransform.cpp @@ -46,8 +46,6 @@ Transform3d BeltTransformPipeline::build_forward_transform(const PrintConfig &co // ---- 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. @@ -81,33 +79,15 @@ inline double belt_floor_z(const BeltTransformPipeline::BeltFloorParams &fp, con } -template BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl( - const Config &config, const BoundingBoxf3 &bb, double original_height) + const PrintConfig &config, const BoundingBoxf3 &bb, double original_height) { BeltTransformPipeline::BeltHeightResult result; result.object_height = original_height; - // Extract the mesh rotation from config (the sole mesh-side belt transform). - BeltRotationAxis rot_axis; - double rot_angle; - - if constexpr (std::is_same_v) { - rot_axis = config.belt_slice_rotation.value; - rot_angle = config.belt_slice_rotation_angle.value; - } else { - // DynamicPrintConfig path - auto get_float = [&](const char *key) { - auto *opt = config.template option(key); - return opt ? opt->value : 0.0; - }; - auto get_rot_axis = [&](const char *key) { - auto *opt = config.template option>(key); - return opt ? opt->value : BeltRotationAxis::None; - }; - rot_axis = get_rot_axis("belt_slice_rotation"); - rot_angle = get_float("belt_slice_rotation_angle"); - } + // The mesh rotation (the sole mesh-side belt transform). + const BeltRotationAxis rot_axis = config.belt_slice_rotation.value; + const double rot_angle = config.belt_slice_rotation_angle.value; bool has_rotation = rot_axis != BeltRotationAxis::None && std::abs(rot_angle) > EPSILON; if (!has_rotation) @@ -159,12 +139,6 @@ BeltTransformPipeline::BeltHeightResult BeltTransformPipeline::compute_belt_heig return compute_belt_height_and_floor_impl(config, bbox, original_height); } -BeltTransformPipeline::BeltHeightResult BeltTransformPipeline::compute_belt_height_and_floor( - const DynamicPrintConfig &config, const BoundingBoxf3 &bbox, double original_height) -{ - return compute_belt_height_and_floor_impl(config, bbox, original_height); -} - bool BeltTransformPipeline::floor_shear(const PrintConfig &config, BeltFloorParams &out) { out = BeltFloorParams{}; diff --git a/src/libslic3r/BeltTransform.hpp b/src/libslic3r/BeltTransform.hpp index 7409fb2c7f..1aae8ba39b 100644 --- a/src/libslic3r/BeltTransform.hpp +++ b/src/libslic3r/BeltTransform.hpp @@ -82,12 +82,6 @@ public: return t; } - static PhysicalTilt physical_tilt(const PrintConfig &config) - { - return physical_tilt(config.belt_slice_rotation.value, - config.belt_slice_rotation_angle.value); - } - // ---- Matrix builders -------------------------------------------------- // Build the 3x3 rotation matrix from belt_slice_rotation* config. @@ -133,11 +127,6 @@ public: static BeltHeightResult compute_belt_height_and_floor( 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 &bbox, - double original_height); }; } // namespace Slic3r diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 6797f34821..c92ebc8e1c 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2431,6 +2431,9 @@ std::vector>> GCode::collec errors.push_back(e); continue; } + // On a belt an object may be left without a layer to print at all. + if (per_object[i].empty()) + continue; OrderingItem ordering_item; ordering_item.object_idx = i; ordering.reserve(ordering.size() + per_object[i].size()); @@ -3244,6 +3247,8 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Belt brim apron bands each get their own change_layer() call. for (const BeltBrimBand &band : object->belt_brim_prologue()) zs.push_back(band.print_z); + if (zs.empty()) + continue; std::sort(zs.begin(), zs.end()); //BBS: merge numerically very close Z values. auto end_it = std::unique(zs.begin(), zs.end()); diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 4aa96f9a4c..da78aa0a35 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -393,13 +393,6 @@ public: } }; - // Public accessor for the first-layer plane evaluator. Used by - // CoolingBuffer (which is constructed with a GCode reference and needs - // to read the plane for per-segment fan re-evaluation). All other - // 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. - protected: class GCodeOutputStream { public: @@ -839,12 +832,6 @@ protected: std::unique_ptr m_cooling_buffer; std::unique_ptr m_spiral_vase; - // First-layer plane evaluator. Constructed once per print from the - // 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. - // Plate origin, kept so a writer replaced during export can be given it again. - std::unique_ptr m_pressure_equalizer; std::unique_ptr m_pa_processor; diff --git a/src/libslic3r/GCode/BeltBackTransform.hpp b/src/libslic3r/GCode/BeltBackTransform.hpp index 3956450a9f..236ad84f51 100644 --- a/src/libslic3r/GCode/BeltBackTransform.hpp +++ b/src/libslic3r/GCode/BeltBackTransform.hpp @@ -28,9 +28,6 @@ public: // no back-transform is active. Vec3d apply(const Vec3d &pos) const; - // True if a non-identity back-transform is active. - bool is_active() const { return m_active; } - private: bool m_active = false; Transform3d m_inverse = Transform3d::Identity(); diff --git a/src/libslic3r/GCode/BeltKinematics.cpp b/src/libslic3r/GCode/BeltKinematics.cpp index c36c088658..27d5dad105 100644 --- a/src/libslic3r/GCode/BeltKinematics.cpp +++ b/src/libslic3r/GCode/BeltKinematics.cpp @@ -10,12 +10,8 @@ namespace Slic3r { BeltKinematics::BeltKinematics(const PrintConfig &config, bool world_coordinates) : m_world_coordinates(world_coordinates) { - m_back_active = m_back_transform.init_from_config(config); + m_back_transform.init_from_config(config); m_machine_frame.init_from_config(config); - if (m_back_active) - // BeltBackTransform stores the inverse of this; keep the forward so - // to_logical() can reverse the whole chain. - m_back_forward = BeltTransformPipeline::build_forward_transform(config); } Vec3d BeltKinematics::to_machine(const Vec3d &p) const @@ -25,15 +21,6 @@ Vec3d BeltKinematics::to_machine(const Vec3d &p) const return m_machine_frame.apply(after_remap); } -Vec3d BeltKinematics::to_logical(const Vec3d &machine) const -{ - const Vec3d before_frame = m_machine_frame.apply_inverse(machine); - const Vec3d before_remap = this->apply_axis_remap_inverse(before_frame); - if (m_world_coordinates || ! m_back_active) - return before_remap; - return m_back_forward * before_remap; -} - void install_belt_kinematics(GCodeWriter &writer, const PrintConfig &config, bool world_coordinates) { writer.set_kinematics(std::make_unique(config, world_coordinates)); diff --git a/src/libslic3r/GCode/BeltKinematics.hpp b/src/libslic3r/GCode/BeltKinematics.hpp index 8f10bd1074..7dc08635ab 100644 --- a/src/libslic3r/GCode/BeltKinematics.hpp +++ b/src/libslic3r/GCode/BeltKinematics.hpp @@ -31,12 +31,6 @@ public: explicit BeltKinematics(const PrintConfig &config, bool world_coordinates = false); Vec3d to_machine(const Vec3d &p) const override; - Vec3d to_logical(const Vec3d &machine) const override; - // Machine -> build-volume frame. Only the machine-frame shear/scale is undone, - // matching what GCodeProcessor's bounds validation wants. This is deliberately - // NOT to_logical(). - Vec3d to_build_volume(const Vec3d &machine) const override - { return m_machine_frame.apply_inverse(machine); } // A belt writer has always emitted full XYZ on every move, whether or not any // individual stage reports itself active. Making this conditional would change @@ -47,14 +41,9 @@ public: // coordinates and G2/G3 cannot describe it. bool supports_arc_moves() const override { return false; } - bool world_coordinates() const { return m_world_coordinates; } - private: BeltBackTransform m_back_transform; MachineFrameTransform m_machine_frame; - // Forward of what m_back_transform inverts, kept so to_logical() can undo it. - Transform3d m_back_forward { Transform3d::Identity() }; - bool m_back_active { false }; bool m_world_coordinates { false }; }; diff --git a/src/libslic3r/GCode/MachineKinematics.cpp b/src/libslic3r/GCode/MachineKinematics.cpp index 063020fe8a..d9b4b65513 100644 --- a/src/libslic3r/GCode/MachineKinematics.cpp +++ b/src/libslic3r/GCode/MachineKinematics.cpp @@ -17,32 +17,9 @@ Vec3d CartesianKinematics::apply_axis_remap(const Vec3d &pos) const return { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) }; } -// Inverse of the above. Output axis i is fed by source axis (r_i % 3); walking -// the three outputs therefore fills every source component exactly once, so long -// as the remap is a permutation (which set_axis_remap callers guarantee). -Vec3d CartesianKinematics::apply_axis_remap_inverse(const Vec3d &machine) const -{ - if (!has_axis_remap()) - return machine; - Vec3d out = Vec3d::Zero(); - const int r[3] = { m_remap_x, m_remap_y, m_remap_z }; - for (int i = 0; i < 3; ++i) { - const int axis = r[i] % 3; - if (r[i] < 3) out[axis] = machine[i]; - else if (r[i] < 6) out[axis] = -machine[i]; - else out[axis] = m_build_vol_max[axis] - machine[i]; - } - return out; -} - Vec3d CartesianKinematics::to_machine(const Vec3d &p) const { return this->apply_axis_remap(p); } -Vec3d CartesianKinematics::to_logical(const Vec3d &machine) const -{ - return this->apply_axis_remap_inverse(machine); -} - } // namespace Slic3r diff --git a/src/libslic3r/GCode/MachineKinematics.hpp b/src/libslic3r/GCode/MachineKinematics.hpp index 5c53bd9f58..fc13282998 100644 --- a/src/libslic3r/GCode/MachineKinematics.hpp +++ b/src/libslic3r/GCode/MachineKinematics.hpp @@ -24,17 +24,6 @@ public: // Logical placed point -> emitted machine point. virtual Vec3d to_machine(const Vec3d &p) const = 0; - // Inverse of to_machine(), back to the logical placed frame. Intended for - // consumers that must reconstruct model coordinates from emitted G-code - // (the G-code viewer's upright preview). - virtual Vec3d to_logical(const Vec3d &machine) const = 0; - - // Machine point -> build-volume frame, for bounds validation only. This is - // deliberately NOT to_logical(): the build-volume check wants the physical - // frame the printable area is expressed in, not the model frame. Keeping - // them separate stops the two contracts from being confused. - virtual Vec3d to_build_volume(const Vec3d &machine) const = 0; - // True when a move must emit X, Y and Z because omitting a word would be // wrong under this mapping. Deliberately not called "couples_axes": a pure // axis permutation forces full emission without physically coupling axes. @@ -72,8 +61,6 @@ class CartesianKinematics : public MachineKinematics { public: Vec3d to_machine(const Vec3d &p) const override; - Vec3d to_logical(const Vec3d &machine) const override; - Vec3d to_build_volume(const Vec3d &machine) const override { return machine; } bool must_emit_all_axes() const override { return this->has_axis_remap(); } bool suppress_lift_at_unknown_position() const override { return this->has_axis_remap(); } @@ -93,7 +80,6 @@ public: protected: Vec3d apply_axis_remap(const Vec3d &pos) const; - Vec3d apply_axis_remap_inverse(const Vec3d &pos) const; int m_remap_x { 0 }; int m_remap_y { 1 }; diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index c726f7b79f..fae1fcdb0b 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -431,10 +431,7 @@ public: // the whole list to 0-based afterwards). Lowest positive outer_wall_filament_id // over the printing regions, 1 if none is explicitly set. 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. const std::vector& belt_brim_by_layer() const { return m_belt_brim_by_layer; } - const std::vector& belt_brim_areas_by_layer() const { return m_belt_brim_areas_by_layer; } const std::vector& belt_brim_prologue() const { return m_belt_brim_prologue; } void clear_belt_brim(); void set_belt_brim(std::vector &&by_layer, diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index 53f93f5b69..096f505542 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1918,10 +1918,11 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ update_apply_status(this->invalidate_steps({ psSkirtBrim, psWipeTower, psGCodeExport })); // A belt brim is clipped against the other objects on the plate (BeltBrim.cpp, // belt_brim_obstacles), and it is rebuilt with its object's support step: an - // object that arrived or left changes every other object's brim. + // object that arrived or left changes every other brim owner's brim. if ((new_objects || deleted_objects) && m_config.belt_printer.value) for (PrintObject *object : m_objects) - update_apply_status(object->invalidate_step(posSupportMaterial)); + if (object->has_belt_brim()) + update_apply_status(object->invalidate_step(posSupportMaterial)); if (new_objects) update_apply_status(false); print_regions_reshuffled = true; diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 6723b903fd..c95ee6f5ce 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -7415,7 +7415,7 @@ void PrintConfigDef::init_fff_params() def->label = L("Machine-frame tilt angle"); def->category = L("Printable space"); def->tooltip = L("Tilt angle (degrees) used to derive the machine-frame shear (cot) and " - "scale (1/sin) applied to G-code. Only used when 'Decouple machine-frame " + "scale (1/|sin|) applied to G-code. Only used when 'Decouple machine-frame " "tilt' is enabled; otherwise the belt tilt angle is used."); def->sidetext = L("°"); def->min = -89.9; @@ -7477,7 +7477,7 @@ void PrintConfigDef::init_fff_params() def->label = L("Belt purge tower width"); def->category = L("Printable space"); def->tooltip = L("Width (machine X, across the belt) of the purge prism that is automatically " - "generated on belt printers when the prime tower is enabled and multiple " + "generated on belt printers when the belt purge tower is enabled and multiple " "filaments are used. Filament-change purging is routed into this prism's " "extrusions instead of a classic wipe tower. Its height is computed " "automatically from the worst-case purge volume per layer: a wider prism " diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 06e71a60b3..facce01787 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -909,19 +909,10 @@ void PrintObject::slice() // Belt floor Z-shift: where is the belt surface in final slicer space? // - // The belt surface is at model_Y=0 (XZ belt plane). After the full - // pipeline (trafo_centered → pre_remap → shear → z_shift), the belt - // surface equation in slicer space is: - // Z_belt = sf * from_axis + belt_surface_z_centered + z_shift_val - // - // belt_surface_z_centered = remapped_bbox.min.z() (the Z position of - // the belt surface in centered-pre-shear slicer space, which is 0 - // without pre-remap but nonzero when e.g. Y↔Z swap shifts the belt - // surface away from Z=0 by the centering offset). - // - // z_shift_val = max(0, -m_belt_min_z) (lifts mesh above Z=0). - // - // So: belt_floor_z_shift = remapped_bb.min.z() + z_shift_val + // The belt surface is the model's Z=0 plane. After the belt rotation and the + // Z-shift it is the plane Z_belt = shear_factor * from_axis + z_shift_val in + // slicer space, with z_shift_val = max(0, -m_belt_min_z), the lift that starts + // the slicing frame at the belt below the footprint. 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.; // The belt surface is at Z=0 in centered slicer space and bb.min.z() is @@ -967,10 +958,10 @@ void PrintObject::slice() if (m_layers.empty()) throw Slic3r::SlicingError(L("No layers were detected. You might want to repair your STL file(s) or check their size or thickness and retry.\n")); - // Belt printer global mode: offset all layer Z values so objects at - // different bed positions print at different heights on the tilted belt. - // This is a post-slicing adjustment — the sliced geometry is identical - // regardless of global mode, only the output Z coordinates change. + // Belt printer: offset all layer Z values so objects at different positions + // along the belt print at different heights on the tilted belt. This is a + // post-slicing adjustment: the sliced geometry is the same, only the output Z + // coordinates change. { const auto &pcfg = this->print()->config(); BOOST_LOG_TRIVIAL(trace) << "Belt global check: belt_printer=" << pcfg.belt_printer.value diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 6542abb8f6..fadcd11f56 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -17017,11 +17017,38 @@ void Plater::calib_flowrate(bool is_linear, int pass, InfillPattern pattern) { } +// The belt provini tower (Calib_Params::test_model 1) is one embossed model per +// temperature range. +static std::string belt_temp_tower_asset(const Calib_Params ¶ms) +{ + const int t_start = (int) lround(params.start); + const int t_end = (int) lround(params.end); + return Slic3r::resources_dir() + "/calib/temperature_tower/belt_temp_tower_" + + std::to_string(t_start) + "_" + std::to_string(t_end) + ".stl"; +} + void Plater::calib_temp(const Calib_Params& params) { constexpr double base_temp_tower_nozzle_diameter = 0.4; constexpr double base_temp_tower_block_height = 10.0; constexpr int base_temp_tower_temp_step = 5; + // A belt provini tower exists only for the ranges it was embossed for, and another + // range's model would print numbers that do not match its temperatures. Refuse + // before the current project is replaced. + if (params.mode == CalibMode::Calib_Temp_Tower && params.test_model >= 1) { + const auto &printer_config = wxGetApp().preset_bundle->printers.get_edited_preset().config; + if (printer_config.has("belt_printer") && printer_config.opt_bool("belt_printer") && + ! boost::filesystem::exists(belt_temp_tower_asset(params))) { + MessageDialog dlg(static_cast(wxGetApp().mainframe), + format_wxstr(_L("No belt temperature tower is available for the range %1% to %2% °C. " + "Use a range the tower models cover, for example 230 to 190."), + (int) lround(params.start), (int) lround(params.end)), + _L("Temperature tower"), wxICON_ERROR | wxOK); + dlg.ShowModal(); + return; + } + } + const auto calib_temp_name = _L("Nozzle temperature test"); new_project(false, false, calib_temp_name); wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); @@ -17077,19 +17104,9 @@ void Plater::calib_temp(const Calib_Params& params) { temps.push_back(t); if (temps.empty()) temps.push_back(t_start); - const std::string calib_dir = Slic3r::resources_dir() + "/calib/temperature_tower/"; - std::string asset = calib_dir + "belt_temp_tower_" + std::to_string(t_start) + "_" + std::to_string(t_end) + ".stl"; - if (!boost::filesystem::exists(asset)) { - // The embossed numbers are part of the model, so another model's tower would - // print numbers that do not match its temperatures. - MessageDialog dlg(static_cast(wxGetApp().mainframe), - format_wxstr(_L("No belt temperature tower is available for the range %1% to %2% °C. " - "Use a range the tower models cover, for example 230 to 190."), - t_start, t_end), - _L("Temperature tower"), wxICON_ERROR | wxOK); - dlg.ShowModal(); + const std::string asset = belt_temp_tower_asset(params); + if (!boost::filesystem::exists(asset)) // refused above, before new_project() return; - } if (!add_model(false, asset) || model().objects.empty()) return; diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index a84bc3b451..58f8d12e39 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -1381,26 +1381,27 @@ TEST_CASE("Belt G-code has no layer that prints nothing", "[Print][belt][GCode][ size_t layers = 0, empty = 0, total_header = 0, total_count = 0; bool extruded = true; // before the first layer change - std::istringstream in(gc); - std::string line; auto close_layer = [&]() { if (! extruded) ++ empty; }; - while (std::getline(in, line)) { - if (line.rfind(";LAYER_CHANGE", 0) == 0) { + GCodeReader reader; + reader.apply_config(config); + reader.parse_buffer(gc, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) { + const std::string &raw = line.raw(); + if (raw.rfind(";LAYER_CHANGE", 0) == 0) { close_layer(); ++ layers; extruded = false; - } else if (line.rfind("; total layer number: ", 0) == 0) { + } else if (raw.rfind("; total layer number: ", 0) == 0) { // Counted by the G-code processor from the layer changes it saw. - total_header = size_t(std::atoi(line.c_str() + 22)); - } else if (line.rfind("; total layers count = ", 0) == 0) { + total_header = size_t(std::atoi(raw.c_str() + 22)); + } else if (raw.rfind("; total layers count = ", 0) == 0) { // GCode::m_layer_count, counted up front from the objects' layers; it also // drives the M73 progress and the total_layer_count placeholder. - total_count = size_t(std::atoi(line.c_str() + 23)); - } else if (! extruded && line.rfind("G1 ", 0) == 0 && line.find('E') != std::string::npos - && (line.find('X') != std::string::npos || line.find('Y') != std::string::npos)) { + total_count = size_t(std::atoi(raw.c_str() + 23)); + } else if (! extruded && line.extruding(self) && line.dist_XY(self) > EPSILON) { + // Material laid down along a move: a wipe or an unretraction does not count. extruded = true; } - } + }); close_layer(); INFO("layers " << layers << ", header " << total_header << ", count " << total_count << ", layers without extrusion " << empty); From ed8c1f92d7c43b4f38c598b59a5da273660d48bd Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 19:08:58 -0500 Subject: [PATCH 217/224] Belt brim: cut the leading-edge brim at the first contact layer The leading-edge-only brim is the outer brim cut down to the part's first contact with the belt, and the cut was taken at layers().front(). Since the slicing frame starts at the belt below the footprint (#16236) that is an empty lead-in layer whose contact lies ahead of the part, so the cut removed the whole region and the brim type produced no brim at all. Take the cut at the first layer with geometry. The all-brim-types test now includes leading_edge_only, and a new test checks that the brim starts no later than the part and covers fewer layers than the outer brim. Co-Authored-By: Claude Fable 5.1 --- src/libslic3r/BeltBrim.cpp | 18 +++++++++++---- tests/fff_print/test_skirt_brim.cpp | 36 ++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/src/libslic3r/BeltBrim.cpp b/src/libslic3r/BeltBrim.cpp index a25659b30e..920790891c 100644 --- a/src/libslic3r/BeltBrim.cpp +++ b/src/libslic3r/BeltBrim.cpp @@ -488,11 +488,19 @@ void make_belt_brim(PrintObject &object) width, gap, leading, lateral, bc.frame), bc.frame); - if (bt == btLeadingEdgeOnly && ! bc.region.empty()) - // The cut is the uphill edge of the first layer's contact band: everything - // past it belongs to later contacts. - bc.region = belt_brim_clip_leading_edge(bc.region, bc.frame, - bc.ctx.cutoff_u(object.layers().front()->print_z)); + if (bt == btLeadingEdgeOnly && ! bc.region.empty()) { + // The cut is the uphill edge of the first contact's band: everything past it + // belongs to later contacts. The first contact is the first layer with + // geometry, not layers().front(): the slicing frame starts at the belt below + // the footprint, so the leading layers are empty and their contact lies ahead + // of the part. + const Layer *first_contact = nullptr; + for (const Layer *layer : object.layers()) + if (! layer->lslices.empty()) { first_contact = layer; break; } + if (first_contact == nullptr) + return; + bc.region = belt_brim_clip_leading_edge(bc.region, bc.frame, bc.ctx.cutoff_u(first_contact->print_z)); + } if (bc.region.empty()) return; diff --git a/tests/fff_print/test_skirt_brim.cpp b/tests/fff_print/test_skirt_brim.cpp index 34f71a71a7..639c36d6c7 100644 --- a/tests/fff_print/test_skirt_brim.cpp +++ b/tests/fff_print/test_skirt_brim.cpp @@ -1140,7 +1140,7 @@ TEST_CASE("Every brim type slices on a belt printer", "[SkirtBrim][belt]") // Auto / Mouse ear / Painted collapse to outer-only rather than crashing or // silently producing nothing. const char *brim_type = GENERATE("auto_brim", "brim_ears", "painted", "outer_only", - "inner_only", "outer_and_inner", "no_brim"); + "inner_only", "outer_and_inner", "leading_edge_only", "no_brim"); DYNAMIC_SECTION("brim_type " << brim_type) { DynamicPrintConfig config = belt_brim_config(); config.set_deserialize_strict({ @@ -1157,6 +1157,40 @@ TEST_CASE("Every brim type slices on a belt printer", "[SkirtBrim][belt]") } } +// The leading-edge-only brim is the outer brim cut down to the part's first contact +// with the belt. The cut has to be taken at the first layer with geometry: the slicing +// frame starts at the belt below the footprint, so layers().front() is an empty lead-in +// layer whose contact lies ahead of the part, and a cut taken there left no brim at all. +TEST_CASE("Leading-edge-only brim is laid at the first contact and nowhere else", "[SkirtBrim][belt][Regression]") +{ + auto brim_gcode = [](const char *brim_type) { + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", brim_type }, + { "brim_width", 5 }, + { "leading_brim_length", 10 }, + { "extra_brim_width", 0 }, + { "brim_object_gap", 0 }, + }); + return slice({ cube(20) }, config); + }; + const std::string leading = brim_gcode("leading_edge_only"); + const std::string outer = brim_gcode("outer_only"); + + const double brim_z = first_role_z(leading, "brim"); + const double peri_z = first_role_z(leading, "perimeter"); + REQUIRE(brim_z < std::numeric_limits::max()); + REQUIRE(peri_z < std::numeric_limits::max()); + // At the first contact: the brim starts no later than the part does... + CHECK(brim_z <= peri_z + EPSILON); + // ...and stops there, while the outer brim keeps following the footprint. + const int leading_layers = role_layers(leading, "brim"); + const int outer_layers = role_layers(outer, "brim"); + INFO("brim layers: leading-edge " << leading_layers << ", outer " << outer_layers); + CHECK(leading_layers > 0); + CHECK(leading_layers < outer_layers); +} + TEST_CASE("An untilted belt printer gets no brim", "[SkirtBrim][belt]") { // Belt brim needs a tilt to have a belt plane to lie on, and the flat plate brim From c59eb1bcf9d9fead326b3cf17e9c075c76b0ec56 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Thu, 8 Oct 2026 13:40:52 -0500 Subject: [PATCH 218/224] Belt brim: cut the leading-edge brim at the first layer on the belt The leading-edge-only brim is the outer brim cut down to the part's first contact with the belt. The cut was taken at the first layer with geometry, but with an overhang on the leading side that layer is the overhang's tip, which is sliced before the part reaches the belt and does not touch it. The cut then lay ahead of the part: a 30 mm fin left a sliver of brim well ahead of a 20 mm cube, and a 40 mm fin, or a leading brim length of 0, left none at all. The footprint loop already finds the layers that touch the belt (their contact band is not empty); record the first of them and cut there. The new test slices a cube with and without a 30 or 40 mm fin over its leading end and checks that the leading-edge brim is the same. It fails without the fix. Reported by raistlin7447 in the review of #14394. Co-Authored-By: Claude Opus 5.5 --- src/libslic3r/BeltBrim.cpp | 25 ++++++++-------- tests/fff_print/test_skirt_brim.cpp | 45 +++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 15 deletions(-) diff --git a/src/libslic3r/BeltBrim.cpp b/src/libslic3r/BeltBrim.cpp index 920790891c..d51f1248de 100644 --- a/src/libslic3r/BeltBrim.cpp +++ b/src/libslic3r/BeltBrim.cpp @@ -443,6 +443,8 @@ void make_belt_brim(PrintObject &object) // own contact band. This is the object's bottom face, which on a belt is // spread over every layer instead of sitting in layer 0. ExPolygons footprint_acc; + // The first layer that touches the belt: where the leading-edge brim is cut. + const Layer *first_contact = nullptr; for (size_t i = 0; i < nlayers; ++ i) { const Layer &layer = *object.layers()[i]; if (layer.lslices.empty()) @@ -458,7 +460,12 @@ void make_belt_brim(PrintObject &object) const Polygon band = band_box(bb, bc.frame.from_axis, u_lo, u_hi); if (band.empty()) continue; - expolygons_append(footprint_acc, intersection_ex(layer.lslices, Polygons{ band })); + ExPolygons contact = intersection_ex(layer.lslices, Polygons{ band }); + if (contact.empty()) + continue; + if (first_contact == nullptr) + first_contact = &layer; + expolygons_append(footprint_acc, std::move(contact)); } const ExPolygons footprint = union_ex(footprint_acc); if (footprint.empty()) @@ -488,19 +495,13 @@ void make_belt_brim(PrintObject &object) width, gap, leading, lateral, bc.frame), bc.frame); - if (bt == btLeadingEdgeOnly && ! bc.region.empty()) { + if (bt == btLeadingEdgeOnly && first_contact != nullptr && ! bc.region.empty()) // The cut is the uphill edge of the first contact's band: everything past it - // belongs to later contacts. The first contact is the first layer with - // geometry, not layers().front(): the slicing frame starts at the belt below - // the footprint, so the leading layers are empty and their contact lies ahead - // of the part. - const Layer *first_contact = nullptr; - for (const Layer *layer : object.layers()) - if (! layer->lslices.empty()) { first_contact = layer; break; } - if (first_contact == nullptr) - return; + // belongs to later contacts. The first contact is the first layer that touches + // the belt (step 1), neither layers().front(), an empty lead-in layer, nor the + // first layer with geometry, which is an overhang's tip when the part overhangs + // its leading end: both lie ahead of the part. bc.region = belt_brim_clip_leading_edge(bc.region, bc.frame, bc.ctx.cutoff_u(first_contact->print_z)); - } if (bc.region.empty()) return; diff --git a/tests/fff_print/test_skirt_brim.cpp b/tests/fff_print/test_skirt_brim.cpp index 639c36d6c7..c51dd5ebc6 100644 --- a/tests/fff_print/test_skirt_brim.cpp +++ b/tests/fff_print/test_skirt_brim.cpp @@ -1158,9 +1158,10 @@ TEST_CASE("Every brim type slices on a belt printer", "[SkirtBrim][belt]") } // The leading-edge-only brim is the outer brim cut down to the part's first contact -// with the belt. The cut has to be taken at the first layer with geometry: the slicing -// frame starts at the belt below the footprint, so layers().front() is an empty lead-in -// layer whose contact lies ahead of the part, and a cut taken there left no brim at all. +// with the belt. The cut has to be taken at the first layer that touches the belt: the +// slicing frame starts at the belt below the footprint, so layers().front() is an empty +// lead-in layer whose contact lies ahead of the part, and a cut taken there left no brim +// at all. TEST_CASE("Leading-edge-only brim is laid at the first contact and nowhere else", "[SkirtBrim][belt][Regression]") { auto brim_gcode = [](const char *brim_type) { @@ -1191,6 +1192,44 @@ TEST_CASE("Leading-edge-only brim is laid at the first contact and nowhere else" CHECK(leading_layers < outer_layers); } +// An overhang on the leading side is sliced before the part reaches the belt, so the +// first layer with geometry is the overhang's tip, above the belt. A leading-edge cut +// taken there lies ahead of the part: the brim shrank to a sliver well ahead of it, or +// vanished once the overhang reached further forward than the brim. The overhang does +// not touch the belt, so it must not change the brim at all. +TEST_CASE("Leading-edge-only brim ignores an overhang ahead of the part", "[SkirtBrim][belt][Regression]") +{ + const double fin_length = GENERATE(30., 40.); + CAPTURE(fin_length); + // A 20 mm cube, with or without a 2 mm thick fin leaving its top edge and reaching + // `fin` toward -Y, the end of the part that prints first. The fin overlaps the cube + // by 1 mm so the two shells merge instead of sharing a face. + auto brim_layers = [](double fin) { + indexed_triangle_set its = its_make_cube(20., 20., 20.); + if (fin > 0.) { + indexed_triangle_set fin_its = its_make_cube(20., fin + 1., 2.); + its_translate(fin_its, Vec3f(0.f, float(-fin), 18.f)); + its_merge(its, fin_its); + } + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", "leading_edge_only" }, + { "brim_width", 5 }, + { "leading_brim_length", 10 }, + { "extra_brim_width", 0 }, + { "brim_object_gap", 0 }, + }); + return role_layers(slice({ TriangleMesh(std::move(its)) }, config), "brim"); + }; + const int plain = brim_layers(0.); + const int with_fin = brim_layers(fin_length); + INFO("leading-edge brim layers: plain cube " << plain << ", with the fin " << with_fin); + REQUIRE(plain > 0); + // One layer of slack: the fin widens the part's footprint on the plate, which can + // move the layer grid by a fraction of a layer. + CHECK(std::abs(with_fin - plain) <= 1); +} + TEST_CASE("An untilted belt printer gets no brim", "[SkirtBrim][belt]") { // Belt brim needs a tilt to have a belt plane to lie on, and the flat plate brim From d29e3be0a0c3f0755f781e3db5433fe71a0c61d5 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Thu, 8 Oct 2026 13:40:52 -0500 Subject: [PATCH 219/224] Belt G-code: drop the empty layers in one place The by-layer collect_layers_to_print() dropped every merged layer group in which nothing prints, and the per-object overload drops every belt entry that prints nothing. The merged groups are built only from the per-object entries, so after the second drop every group holds at least one entry that prints and the first never removed anything. Remove it and keep its explanation at the drop that does the work. No output changes. Reported by raistlin7447 in the review of #14394. Co-Authored-By: Claude Opus 5.5 --- src/libslic3r/GCode.cpp | 34 +++++++--------------------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 453317ef2c..c2db414fc5 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2370,8 +2370,13 @@ std::vector GCode::collect_layers_to_print(const PrintObjec PrintStateBase::WarningLevel::CRITICAL, warning, PrintStateBase::SlicingEmptyGcodeLayers); } - // Belt printers: drop the layers that print nothing (see the by-layer overload), so - // the by-object export writes the same layer changes as the by-layer one. + // Belt printers: drop the layers that print nothing at all. An object's slicing + // frame starts at the belt below its leading end, so its first layers are empty, + // and with several objects along the belt those empty layers fall between other + // objects' printing layers. A layer change with no moves is noise in the file, and + // the preview (libvgcode) numbers its layers from the moves it sees, so a gap folds + // every later layer into the one before it. Both print sequences collect their + // layers here, so neither writes such a layer. if (object.print()->config().belt_printer.value) layers_to_print.erase( std::remove_if(layers_to_print.begin(), layers_to_print.end(), [&object](const LayerToPrint <p) { @@ -2446,31 +2451,6 @@ std::vector>> GCode::collec layers_to_print.emplace_back(std::move(merged)); } - // Belt printers: drop the layers that print nothing at all. An object's - // slicing frame starts at the belt below its leading end, so its first layers - // are empty, and with several objects along the belt those empty layers fall - // between other objects' printing layers. A layer change with no moves is - // noise in the file, and the preview (libvgcode) numbers its layers from the - // moves it sees, so a gap folds every later layer into the one before it. - if (print.config().belt_printer.value) { - auto prints_something = [](const LayerToPrint <p) { - if (ltp.object_layer != nullptr && ltp.original_object != nullptr && - belt_object_layer_prints_something(*ltp.original_object, *ltp.object_layer)) - return true; - if (ltp.support_layer != nullptr && ltp.support_layer->has_extrusions()) - return true; - if (ltp.belt_brim_band != nullptr && ! ltp.belt_brim_band->fills.empty()) - return true; - return false; - }; - layers_to_print.erase( - std::remove_if(layers_to_print.begin(), layers_to_print.end(), - [&prints_something](const std::pair> &group) { - return std::none_of(group.second.begin(), group.second.end(), prints_something); - }), - layers_to_print.end()); - } - return layers_to_print; } From 679638fca37f42fbbedd349976254c2448a501bd Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Thu, 8 Oct 2026 13:40:52 -0500 Subject: [PATCH 220/224] Belt G-code: correct the first-layer point test comment The comment said the lambda takes off "the instance part" of m_origin. On a belt m_origin has been rotated by on_set_origin() by then, so m_origin minus the plate offset is not the instance shift. Say what the code does: undo what point_to_gcode() added and what the writer took off. Comment only. Reported by raistlin7447 in the review of #14394. Co-Authored-By: Claude Opus 5.5 --- src/libslic3r/GCode.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index c2db414fc5..d29363e639 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -3173,8 +3173,8 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato 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(); - // The writer hands over the point with the plate origin (its XY offset) already - // taken off, while m_origin still carries it: take off the instance part only. + // Undo what point_to_gcode() added (m_origin, minus the extruder offset) and + // what the writer then took off (its XY offset, the plate origin). const Vec2d plate_offset = m_writer.get_xy_offset().cast(); return this->on_first_layer(Vec3d(point_logical.x() - (m_origin.x() - plate_offset.x()) + extruder_offset.x(), point_logical.y() - (m_origin.y() - plate_offset.y()) + extruder_offset.y(), From 86ff2a9de7a1b88e8a36043d7809be7aa54a0c91 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Fri, 9 Oct 2026 15:20:38 +0800 Subject: [PATCH 221/224] fix: avoid substring denies in audit path keywords (#16243) ## Summary Fixes #15944. The plugin audit deny-list matched `secret`, `cert`, and `conf` as substrings of every path component. This blocked valid imports during plugin capability execution, for example `numpy/__config__.py`, because `conf` appeared inside the module filename. This PR changes deny keyword matching to use whole path components instead of substring matches. It keeps the intended protections for sensitive locations and config files, while allowing dependency and stdlib modules whose names merely contain those strings. ## Changes - Match denied path keywords as whole components instead of substrings. - Keep denying sensitive directory names such as: - `secret` - `secrets` - `cert` - `certs` - `certificate` - `certificates` - `conf` - `config` - Keep denying config files by extension: - `.conf` - `.ini` - Allow legitimate Python module/package paths such as: - `numpy/__config__.py` - `numpy/_core/_ufunc_config.py` - `configparser.py` - `sysconfig.py` - `logging/config.py` - `certifi/cacert.pem` - Include the denied target and reason in `PermissionError` messages when the audit hook blocks an operation. - Remove an unused `` include from `PluginAuditManager.hpp`. ## Why The previous substring matching caused false positives for common dependency and standard-library paths. It also made failures hard to diagnose because the Python exception did not include the refused path. The new behavior is narrower: it blocks sensitive path components and config file extensions without treating unrelated names like `__config__.py`, `configparser.py`, `Conference`, or `Concert` as secrets. ## Testing - Added/updated unit coverage in `tests/slic3rutils/test_plugin_audit.cpp` for: - whole-component keyword matches - `.conf` / `.ini` blocking - case-insensitive matching - false-positive paths from #15944 Plugin used for testing: [orca_audit_numpy_config_repro.py](https://github.com/user-attachments/files/33143848/orca_audit_numpy_config_repro.py) --- src/slic3r/plugin/PluginAuditManager.cpp | 31 +++++++++++++++++++----- src/slic3r/plugin/PluginAuditManager.hpp | 19 ++++++--------- tests/slic3rutils/test_plugin_audit.cpp | 15 +++++++++--- 3 files changed, 45 insertions(+), 20 deletions(-) diff --git a/src/slic3r/plugin/PluginAuditManager.cpp b/src/slic3r/plugin/PluginAuditManager.cpp index efb730da8c..34ae514fba 100644 --- a/src/slic3r/plugin/PluginAuditManager.cpp +++ b/src/slic3r/plugin/PluginAuditManager.cpp @@ -351,8 +351,20 @@ std::vector PluginAuditManager::default_denied_path_keywords() // must never be able to reach a secret, a certificate, or a configuration file just because // it happens to live inside an otherwise-allowed root (e.g. the bundled TLS client cert at // resources_dir()/cert/..., which would become reachable the moment resources_dir() is - // granted as a read-only allowed root). - return {"secret", "cert", "conf"}; + // granted as a read-only allowed root). Match as whole path components, not substrings, so + // imports such as numpy/__config__.py and stdlib configparser.py remain usable. + return {"secret", "secrets", "cert", "certs", "certificate", "certificates", "conf", "config"}; +} + +static bool has_denied_config_extension(std::string name) +{ + const size_t stream_pos = name.find(':'); + if (stream_pos != std::string::npos) + name.erase(stream_pos); + + const boost::filesystem::path path(name); + const std::string extension = path.extension().string(); + return extension == ".conf" || extension == ".ini"; } bool PluginAuditManager::is_denied_path_keyword(const boost::filesystem::path& candidate) const @@ -377,7 +389,7 @@ bool PluginAuditManager::is_denied_path_keyword(const boost::filesystem::path& c continue; std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) { return std::tolower(c); }); for (const auto& keyword : m_denied_path_keywords) { - if (name.find(keyword) != std::string::npos) + if (name == keyword || (keyword == "conf" && has_denied_config_extension(name))) return true; } } @@ -810,7 +822,8 @@ bool persist_permission(const std::string& plugin_key, int report_denied(PluginAuditManager& mgr, const std::string& event_name, - const AuditDecision& decision) + const AuditDecision& decision, + const std::string& target = {}) { AuditViolation violation; violation.plugin_key = mgr.current_plugin(); @@ -818,7 +831,13 @@ int report_denied(PluginAuditManager& mgr, violation.reason = decision.reason; mgr.report_violation(violation); - PyErr_SetString(PyExc_PermissionError, "Plugin attempted an audited operation without permission"); + std::string message = "Plugin attempted audited operation \"" + event_name + "\" without permission"; + if (!decision.reason.empty()) + message += ": " + decision.reason; + if (!target.empty()) + message += ": " + target; + + PyErr_SetString(PyExc_PermissionError, message.c_str()); return -1; } @@ -987,7 +1006,7 @@ int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user if (fs_category) { for (const auto& target : targets) { if (mgr->is_denied_path(boost::filesystem::path(target))) - return PluginAuditDetail::report_denied(*mgr, event_name, {false, "denied path"}); + return PluginAuditDetail::report_denied(*mgr, event_name, {false, "denied path"}, target); } } diff --git a/src/slic3r/plugin/PluginAuditManager.hpp b/src/slic3r/plugin/PluginAuditManager.hpp index 49866d2fa9..41d3dca1e8 100644 --- a/src/slic3r/plugin/PluginAuditManager.hpp +++ b/src/slic3r/plugin/PluginAuditManager.hpp @@ -4,7 +4,6 @@ // Via pybind11 so this file requests the same python3xx.lib as everything else. #include #include -#include #include #include #include @@ -104,22 +103,20 @@ public: bool is_denied_filename(const boost::filesystem::path& candidate) const; // --- denied-path-keyword registry --- - // Keywords that categorically deny a path if ANY of its components (directory or file - // name), not just the base name, contains one case-insensitively -- e.g. a "secrets" - // subfolder, a "certificates" folder, or a "conf"/"config" file anywhere the plugin can - // otherwise reach, including inside an allowed root. This is intentionally broader and - // fuzzier than the exact-name is_denied_filename registry: it exists to categorically rule - // out whole classes of sensitive paths (secrets, certificates, config) rather than name - // specific known files, at the cost of over-blocking an unrelated name that happens to - // contain the keyword -- the fail-safe direction, same rationale as is_denied_filename. + // Keywords that categorically deny a path if ANY component matches one case-insensitively -- + // e.g. a "secrets" subfolder, a "certificates" folder, a "conf"/"config" directory, or a + // .conf/.ini file anywhere the plugin can otherwise reach, including inside an allowed root. + // This is broader than the exact-name is_denied_filename registry, but it is not a substring + // match: importable modules such as numpy/__config__.py, configparser.py, sysconfig.py, or + // user folders such as "Conference" and "Concert" are unrelated names and must stay promptable. void add_denied_path_keyword(const std::string& keyword); // The list install_hook() seeds into the keyword registry. Exposed so tests seed the exact // same set without a live interpreter. static std::vector default_denied_path_keywords(); - // True when any component of candidate's (canonicalized) path contains a registered - // keyword, case-insensitively. + // True when any component of candidate's (canonicalized) path matches a registered keyword, + // case-insensitively. A registered "conf" keyword also blocks .conf/.ini file components. bool is_denied_path_keyword(const boost::filesystem::path& candidate) const; // is_denied_filename(candidate) || is_denied_path_keyword(candidate). Convenience for diff --git a/tests/slic3rutils/test_plugin_audit.cpp b/tests/slic3rutils/test_plugin_audit.cpp index ca8c46fabd..267cdb4099 100644 --- a/tests/slic3rutils/test_plugin_audit.cpp +++ b/tests/slic3rutils/test_plugin_audit.cpp @@ -222,18 +222,19 @@ TEST_CASE("Plugin audit denies secret/certificate/config-like paths by keyword", CHECK(mgr.is_denied_path_keyword(fs::path("/resources/certificates/ca.pem"))); } - SECTION("a 'conf'/'config' directory or file component is denied") + SECTION("a 'conf'/'config' directory or config file component is denied") { CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/conf/settings.json"))); CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/config/settings.json"))); CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/plugin.conf"))); + CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/plugin.ini"))); } SECTION("matching is case-insensitive") { CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/SECRETS/token.txt"))); - CHECK(mgr.is_denied_path_keyword(fs::path("/resources/CertBundle/ca.pem"))); - CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/CONFIG.JSON"))); + CHECK(mgr.is_denied_path_keyword(fs::path("/resources/Certificates/ca.pem"))); + CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/PLUGIN.CONF"))); } SECTION("matching is not limited to the base name -- any ancestor component counts") @@ -245,6 +246,14 @@ TEST_CASE("Plugin audit denies secret/certificate/config-like paths by keyword", { CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/plugin/output/model.gcode"))); CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/plugin/storage/state.json"))); + CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/python/packages/cp312/numpy/__config__.py"))); + CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/python/packages/cp312/numpy/_core/_ufunc_config.py"))); + CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/python/Lib/configparser.py"))); + CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/python/Lib/sysconfig.py"))); + CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/python/Lib/logging/config.py"))); + CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/python/packages/cp312/certifi/cacert.pem"))); + CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/users/Conference/output.txt"))); + CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/users/Concert/output.txt"))); } SECTION("an empty path is not denied") From 6026100f830b0b0415e31e49165b8ac2d5c7b385 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Fri, 9 Oct 2026 16:23:53 +0800 Subject: [PATCH 222/224] Stop publishing separate belt-printer nightly builds --- .github/workflows/build_all.yml | 8 ++------ .github/workflows/build_orca.yml | 25 ++++++++++--------------- README.md | 11 ----------- 3 files changed, 12 insertions(+), 32 deletions(-) diff --git a/.github/workflows/build_all.yml b/.github/workflows/build_all.yml index ab4cd57f5f..b849979de0 100644 --- a/.github/workflows/build_all.yml +++ b/.github/workflows/build_all.yml @@ -5,7 +5,6 @@ on: branches: - main - release/* - - belt-printer paths: - 'deps/**' - 'src/**' @@ -262,9 +261,6 @@ jobs: date: ver: ver_pure: - # Belt-printer nightlies share the main nightly release but carry a `_belt` - # suffix so they never overwrite the main assets. - nightly_suffix: ${{ github.ref == 'refs/heads/belt-printer' && '_belt' || '' }} steps: - name: "Remove unneeded stuff to free disk space" run: @@ -439,13 +435,13 @@ jobs: name: OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak path: '/__w/OrcaSlicer/OrcaSlicer/OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak' - name: Deploy Flatpak to nightly release - if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/belt-printer') + if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' uses: WebFreak001/deploy-nightly@v3.2.0 with: upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label} release_id: 137995723 asset_path: /__w/OrcaSlicer/OrcaSlicer/OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak - asset_name: OrcaSlicer-Linux-flatpak_nightly${{ env.nightly_suffix }}_${{ matrix.variant.arch }}.flatpak + asset_name: OrcaSlicer-Linux-flatpak_nightly_${{ matrix.variant.arch }}.flatpak asset_content_type: application/octet-stream max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted # The asset is /app (the exes link it at runtime) plus the build tree diff --git a/.github/workflows/build_orca.yml b/.github/workflows/build_orca.yml index 7b001a8c48..7115efab09 100644 --- a/.github/workflows/build_orca.yml +++ b/.github/workflows/build_orca.yml @@ -33,11 +33,6 @@ jobs: ubuntu-ver: '2404' ubuntu-ver-str: '_Ubuntu2404' ORCA_UPDATER_SIG_KEY: ${{ secrets.ORCA_UPDATER_SIG_KEY }} - # Branches whose builds are published to the nightly release. The - # belt-printer branch ships alongside main but its assets carry a `_belt` - # suffix (nightly_suffix) so they never overwrite the main nightly assets. - deploy_nightly: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/belt-printer' }} - nightly_suffix: ${{ github.ref == 'refs/heads/belt-printer' && '_belt' || '' }} steps: - name: Checkout @@ -260,7 +255,7 @@ jobs: # Thanks to RaySajuuk, it's working now - name: Sign app and notary - if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/belt-printer' || startsWith(github.ref, 'refs/heads/release/')) && runner.os == 'macOS' && inputs.macos-combine-only + if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && runner.os == 'macOS' && inputs.macos-combine-only timeout-minutes: 30 working-directory: ${{ github.workspace }} env: @@ -348,7 +343,7 @@ jobs: fi - name: Create DMG without notary - if: github.ref != 'refs/heads/main' && github.ref != 'refs/heads/belt-printer' && runner.os == 'macOS' && inputs.macos-combine-only + if: github.ref != 'refs/heads/main' && runner.os == 'macOS' && inputs.macos-combine-only working-directory: ${{ github.workspace }} run: | # Load the `retry` helper (retries flaky commands such as `hdiutil create`). @@ -394,13 +389,13 @@ jobs: if-no-files-found: ignore - name: Deploy Mac release - if: github.repository == 'OrcaSlicer/OrcaSlicer' && env.deploy_nightly == 'true' && runner.os == 'macOS' && inputs.macos-combine-only && !vars.SELF_HOSTED + if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'macOS' && inputs.macos-combine-only && !vars.SELF_HOSTED uses: WebFreak001/deploy-nightly@v3.2.0 with: upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label} release_id: 137995723 asset_path: ${{ github.workspace }}/OrcaSlicer_Mac_universal_${{ env.ver }}.dmg - asset_name: OrcaSlicer_Mac_universal_nightly${{ env.nightly_suffix }}.dmg + asset_name: OrcaSlicer_Mac_universal_nightly.dmg asset_content_type: application/octet-stream max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted @@ -543,24 +538,24 @@ jobs: path: ${{ github.workspace }}/build/src/Release/OrcaSlicer_profile_validator.exe - name: Deploy Windows release portable - if: github.repository == 'OrcaSlicer/OrcaSlicer' && env.deploy_nightly == 'true' && runner.os == 'Windows' && !vars.SELF_HOSTED + if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && !vars.SELF_HOSTED uses: WebFreak001/deploy-nightly@v3.2.0 with: upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label} release_id: 137995723 asset_path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer_Windows_${{ env.ver }}${{ env.ARCH_SUFFIX }}_portable.zip - asset_name: OrcaSlicer_Windows${{ env.ARCH_SUFFIX }}_nightly${{ env.nightly_suffix }}_portable.zip + asset_name: OrcaSlicer_Windows${{ env.ARCH_SUFFIX }}_nightly_portable.zip asset_content_type: application/x-zip-compressed max_releases: 1 - name: Deploy Windows release installer - if: github.repository == 'OrcaSlicer/OrcaSlicer' && env.deploy_nightly == 'true' && runner.os == 'Windows' && !vars.SELF_HOSTED + if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && !vars.SELF_HOSTED uses: WebFreak001/deploy-nightly@v3.2.0 with: upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label} release_id: 137995723 asset_path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer_Windows_Installer_${{ env.ver }}${{ env.ARCH_SUFFIX }}.exe - asset_name: OrcaSlicer_Windows_Installer${{ env.ARCH_SUFFIX }}_nightly${{ env.nightly_suffix }}.exe + asset_name: OrcaSlicer_Windows_Installer${{ env.ARCH_SUFFIX }}_nightly.exe asset_content_type: application/x-msdownload max_releases: 1 @@ -701,13 +696,13 @@ jobs: path: './build/src/dev-utils/Release/generate_system_cache' - name: Deploy Ubuntu release - if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && env.deploy_nightly == 'true' && runner.os == 'Linux' && !vars.SELF_HOSTED }} + if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED }} uses: WebFreak001/deploy-nightly@v3.2.0 with: upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label} release_id: 137995723 asset_path: ./build/OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}${{ env.arch_suffix }}_${{ env.ver }}.AppImage - asset_name: OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}${{ env.arch_suffix }}_nightly${{ env.nightly_suffix }}.AppImage + asset_name: OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}${{ env.arch_suffix }}_nightly.AppImage asset_content_type: application/octet-stream max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted - name: Deploy Ubuntu release diff --git a/README.md b/README.md index 40ba7637b9..c35648558e 100644 --- a/README.md +++ b/README.md @@ -89,17 +89,6 @@ Visit our GitHub Releases page for the latest stable version of OrcaSlicer, reco 🌙 **[Download the Latest Nightly Build](https://github.com/OrcaSlicer/OrcaSlicer/releases/tag/nightly-builds)** Explore the latest developments in OrcaSlicer with our nightly builds. Feedback on these versions is highly appreciated. -### Belt Printer Builds - -The [nightly release](https://github.com/OrcaSlicer/OrcaSlicer/releases/tag/nightly-builds) ships **two parallel builds**: the standard build and a belt-printer build. Both are attached to the same release — tell them apart by the filename suffix: - -- **Standard** — no suffix (e.g. `OrcaSlicer_Windows_Installer_x64_nightly.exe`) -- **Belt** — `_belt` suffix (e.g. `OrcaSlicer_Windows_Installer_x64_nightly_belt.exe`) - -The `_belt` builds add **experimental support for belt / conveyor (infinite-Z) printers**, where the model is sliced against a tilted belt surface instead of a flat horizontal bed. They include ready-to-use belt printer profiles, the full belt slicing pipeline (mesh rotation and G-code transforms), belt-aware support generation, and a tilted-bed preview. - -> ⚠️ Belt printer support is under active development and is **not yet merged into `main`** — it currently ships only in these parallel `_belt` builds, produced from the [`belt-printer`](https://github.com/OrcaSlicer/OrcaSlicer/tree/belt-printer) branch. See tracking PR [#14394](https://github.com/OrcaSlicer/OrcaSlicer/pull/14394) and the original documentation in [#12998](https://github.com/OrcaSlicer/OrcaSlicer/pull/12998). - # How to install ## Windows From 18b9b0840f3b9dca6886a01334d6b3c2fa93d9cb Mon Sep 17 00:00:00 2001 From: SoftFever Date: Fri, 9 Oct 2026 16:23:53 +0800 Subject: [PATCH 223/224] List belt printer support among the README's main features --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c35648558e..408efb57b7 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,8 @@ If you come across any of these in search results, please report them as Regular updates fueled by continuous community contributions. - **Wide Printer Compatibility** Supports a broad range of printers: Bambu Lab, Prusa, Creality, Voron, and more. +- **[Belt Printer Support](https://www.orcaslicer.com/wiki/belt_printing)** + Slice for belt / conveyor (infinite-Z) printers, with belt-aware supports and a tilted-bed preview. Contributed by [Joseph Robertson (@HarrierPigeon)](https://github.com/HarrierPigeon). - Additional features can be found in the [change notes](https://github.com/OrcaSlicer/OrcaSlicer/releases/). # Wiki From 62b829d1cb3823a28872e9cf4f86a49aed5ebdc5 Mon Sep 17 00:00:00 2001 From: ExPikaPaka <112851715+ExPikaPaka@users.noreply.github.com> Date: Fri, 9 Oct 2026 11:52:35 +0200 Subject: [PATCH 224/224] Texture displacement: mix filament colours in the slicer (#16242) --- resources/images/delete_all_filaments.svg | 1 + .../110/texture_displacement_shaded.fs | 76 +------- .../140/texture_displacement_shaded.fs | 76 +------- .../TextureBake/TextureBakeDecimate.cpp | 4 + .../TextureBake/TextureBakeDecimate.hpp | 7 + src/libslic3r/TextureBake/TextureBakeMesh.cpp | 13 +- src/libslic3r/TextureBake/TextureBakeMesh.hpp | 5 +- .../TextureBake/TextureBakePipeline.cpp | 49 +++-- .../TextureBake/TextureBakePipeline.hpp | 18 ++ .../TextureBake/TextureBakeRepair.cpp | 24 ++- .../TextureBake/TextureBakeRepair.hpp | 7 +- src/libslic3r/TextureDisplacement.cpp | 33 ++-- src/libslic3r/TextureDisplacement.hpp | 40 +--- .../GUI/Gizmos/GLGizmoTextureDisplacement.cpp | 183 +++++++----------- .../GUI/Gizmos/GLGizmoTextureDisplacement.hpp | 20 +- .../GUI/Jobs/TextureDisplacementBakeJob.cpp | 3 - .../Jobs/TextureDisplacementPreviewJob.cpp | 3 - src/slic3r/GUI/Plater.cpp | 80 ++++++++ src/slic3r/GUI/Plater.hpp | 11 ++ 19 files changed, 324 insertions(+), 329 deletions(-) create mode 100644 resources/images/delete_all_filaments.svg diff --git a/resources/images/delete_all_filaments.svg b/resources/images/delete_all_filaments.svg new file mode 100644 index 0000000000..e2f3641300 --- /dev/null +++ b/resources/images/delete_all_filaments.svg @@ -0,0 +1 @@ + diff --git a/resources/shaders/110/texture_displacement_shaded.fs b/resources/shaders/110/texture_displacement_shaded.fs index 4a2980cd64..44ea89b844 100644 --- a/resources/shaders/110/texture_displacement_shaded.fs +++ b/resources/shaders/110/texture_displacement_shaded.fs @@ -29,19 +29,13 @@ uniform vec3 palette_lab[64]; uniform vec3 palette_rgb[64]; uniform int palette_count; uniform bool pure_only; // match against single filaments only (flat-colour image) -// How each entry prints. A pure entry is one filament (a == b); a mix interleaves filaments a and b, -// num parts of a in every den, and the print shows that interleave rather than the entry's average -// colour. The fragment resolves it exactly as GLGizmoTextureDisplacement::make_mix_resolver() does -// per triangle on the CPU, so the preview shows the pattern the bake will print. +// How each entry prints. Every entry names a single filament: a mix is given its own mixed filament +// slot, whose components the slicer alternates per print layer, so the fragment just looks that slot's +// colour up. uniform int palette_a[64]; uniform int palette_b[64]; -uniform int palette_num[64]; -uniform int palette_den[64]; uniform vec3 filament_rgb[16]; uniform int filament_count; -uniform int mix_mode; // ColorMixMode: 0 Z bands, 1 XY dither, 2 auto -uniform float layer_height; // mm; one Z band per print layer -uniform float dither_cell; // mm; one XY dither cell uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height uniform bool has_color_tex; uniform bool volume_mirrored; @@ -229,63 +223,16 @@ int nearest_palette_entry(vec3 rgb) } // One 2x2 Bayer cell, {0, 2; 3, 1}, for x and y in {0, 1}. -float bayer2(float x, float y) { return 2.0 * x + 3.0 * y - 4.0 * x * y; } -// The colour the printer lays down at world point `pos` for palette entry `index`: its filament, or -// for a mix whichever of its two filaments this point falls on. Mirrors make_mix_resolver() on the -// CPU, floors on the band/cell size included. All the modular arithmetic is done in floats with -// mod(), which wraps negative coordinates the way the CPU's ((v % n) + n) % n does and needs no -// integer % (not available on every GLSL 1.10 target). -vec3 printed_color(int index, vec3 pos, vec3 normal, vec3 footprint) +// The colour the printer lays down at world point `pos` for palette entry `index`. Every entry names a +// single filament: a mix is given its own mixed filament slot, whose components the slicer alternates +// per print layer, so there is nothing left to interleave here. +vec3 printed_color(int index) { int a = palette_a[index]; - int b = palette_b[index]; - if (a < 0 || a >= filament_count || b < 0 || b >= filament_count) + if (a < 0 || a >= filament_count) return palette_rgb[index]; // no filament to resolve to: the entry's own colour - if (a == b) - return filament_rgb[a]; - float num = float(palette_num[index]); - float den = float(palette_den[index]); - // Auto: bands where the surface is steeper than ~45 degrees, the dominant filament elsewhere. - if (mix_mode == 2 && abs(normal.z) >= 0.7) - return filament_rgb[(num * 2.0 >= den) ? a : b]; - - // Pre-filter. The interleave is an ordered dither the eye is meant to blend away, and no dither - // blends when it is drawn at less than a few pixels per period - it aliases, which is what turned - // every upright wall into horizontal streaks: the Z band cycle is den * layer_height (around a - // millimetre), and every pixel of a row on a vertical wall shares one z, so each row came out as a - // 1-bit threshold of the image at that row's phase. `footprint` is mm of world position per pixel, - // so this is zoom- and resolution-correct rather than a tuned constant: where the print's own - // pattern is finer than this view can resolve, show what the print looks like from here, which is - // the entry's perceptual average. The Normal view remains where the per-facet truth lives. - float period = (mix_mode == 1) ? 2.0 * max(dither_cell, 0.01) : den * max(layer_height, 0.01); - float px = (mix_mode == 1) ? max(footprint.x, footprint.y) : footprint.z; - float sharp = clamp(period / max(4.0 * px, 1e-6) - 0.5, 0.0, 1.0); - if (sharp <= 0.0) - return palette_rgb[index]; - - vec3 picked; - if (mix_mode == 1) { - // Ordered 4x4 Bayer over floor(x / cell), floor(y / cell). The CPU's table - // 0 8 2 10 - // 12 4 14 6 - // 3 11 1 9 - // 15 7 13 5 - // is 4 * bayer2(x % 2, y % 2) + bayer2(x / 2, y / 2), which needs no array (GLSL 1.10 has - // no constant arrays). - float cell = max(dither_cell, 0.01); - float gx = mod(floor(pos.x / cell), 4.0); - float gy = mod(floor(pos.y / cell), 4.0); - float bayer = 4.0 * bayer2(mod(gx, 2.0), mod(gy, 2.0)) + bayer2(floor(gx / 2.0), floor(gy / 2.0)); - picked = filament_rgb[(num / den > (bayer + 0.5) / 16.0) ? a : b]; - } else { - // Z bands: one per band height, the band's phase in the a/b cycle picks the filament. Both - // operands are integer-valued, so the half keeps "phase < num" exact under float rounding. - float slot = floor(pos.z / max(layer_height, 0.01)); - float phase = mod(slot, den); - picked = filament_rgb[(phase < num - 0.5) ? a : b]; - } - return mix(palette_rgb[index], picked, sharp); + return filament_rgb[a]; } void main() @@ -296,9 +243,6 @@ void main() // World millimetres throughout, like the bake - see the 140 variant. vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz))); vec3 tex_pos = world_pos.xyz - tex_anchor; // the frame the texture is projected in, as the bake does - // World mm per pixel, for pre-filtering the interleave in printed_color(). Taken here because the - // albedo branch at the end of main() is non-uniform control flow, where derivatives are undefined. - vec3 pos_fwidth = fwidth(world_pos.xyz); if (volume_mirrored) triangle_normal = -triangle_normal; @@ -420,6 +364,6 @@ void main() // orientation and scale about the volume's origin, see texture_displacement_bake_frame()), so // measuring z from the bed instead shifted the band phase by the volume origin's height - a // different filament in the same place than the bake produces. - albedo = printed_color(nearest_palette_entry(texture2D(color_tex, color_uv).rgb), tex_pos, triangle_normal, pos_fwidth); + albedo = printed_color(nearest_palette_entry(texture2D(color_tex, color_uv).rgb)); gl_FragColor = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a); } diff --git a/resources/shaders/140/texture_displacement_shaded.fs b/resources/shaders/140/texture_displacement_shaded.fs index 1d8bee10fa..b2936598cd 100644 --- a/resources/shaders/140/texture_displacement_shaded.fs +++ b/resources/shaders/140/texture_displacement_shaded.fs @@ -88,19 +88,13 @@ uniform vec3 palette_lab[64]; uniform vec3 palette_rgb[64]; uniform int palette_count; uniform bool pure_only; // match against single filaments only (flat-colour image) -// How each entry prints. A pure entry is one filament (a == b); a mix interleaves filaments a and b, -// num parts of a in every den, and the print shows that interleave rather than the entry's average -// colour. The fragment resolves it exactly as GLGizmoTextureDisplacement::make_mix_resolver() does -// per triangle on the CPU, so the preview shows the pattern the bake will print. +// How each entry prints. Every entry names a single filament: a mix is given its own mixed filament +// slot, whose components the slicer alternates per print layer, so the fragment just looks that slot's +// colour up. uniform int palette_a[64]; uniform int palette_b[64]; -uniform int palette_num[64]; -uniform int palette_den[64]; uniform vec3 filament_rgb[16]; uniform int filament_count; -uniform int mix_mode; // ColorMixMode: 0 Z bands, 1 XY dither, 2 auto -uniform float layer_height; // mm; one Z band per print layer -uniform float dither_cell; // mm; one XY dither cell uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height uniform bool has_color_tex; uniform bool volume_mirrored; @@ -295,63 +289,16 @@ int nearest_palette_entry(vec3 rgb) } // One 2x2 Bayer cell, {0, 2; 3, 1}, for x and y in {0, 1}. -float bayer2(float x, float y) { return 2.0 * x + 3.0 * y - 4.0 * x * y; } -// The colour the printer lays down at world point `pos` for palette entry `index`: its filament, or -// for a mix whichever of its two filaments this point falls on. Mirrors make_mix_resolver() on the -// CPU, floors on the band/cell size included. All the modular arithmetic is done in floats with -// mod(), which wraps negative coordinates the way the CPU's ((v % n) + n) % n does and needs no -// integer % (not available on every GLSL 1.10 target). -vec3 printed_color(int index, vec3 pos, vec3 normal, vec3 footprint) +// The colour the printer lays down at world point `pos` for palette entry `index`. Every entry names a +// single filament: a mix is given its own mixed filament slot, whose components the slicer alternates +// per print layer, so there is nothing left to interleave here. +vec3 printed_color(int index) { int a = palette_a[index]; - int b = palette_b[index]; - if (a < 0 || a >= filament_count || b < 0 || b >= filament_count) + if (a < 0 || a >= filament_count) return palette_rgb[index]; // no filament to resolve to: the entry's own colour - if (a == b) - return filament_rgb[a]; - float num = float(palette_num[index]); - float den = float(palette_den[index]); - // Auto: bands where the surface is steeper than ~45 degrees, the dominant filament elsewhere. - if (mix_mode == 2 && abs(normal.z) >= 0.7) - return filament_rgb[(num * 2.0 >= den) ? a : b]; - - // Pre-filter. The interleave is an ordered dither the eye is meant to blend away, and no dither - // blends when it is drawn at less than a few pixels per period - it aliases, which is what turned - // every upright wall into horizontal streaks: the Z band cycle is den * layer_height (around a - // millimetre), and every pixel of a row on a vertical wall shares one z, so each row came out as a - // 1-bit threshold of the image at that row's phase. `footprint` is mm of world position per pixel, - // so this is zoom- and resolution-correct rather than a tuned constant: where the print's own - // pattern is finer than this view can resolve, show what the print looks like from here, which is - // the entry's perceptual average. The Normal view remains where the per-facet truth lives. - float period = (mix_mode == 1) ? 2.0 * max(dither_cell, 0.01) : den * max(layer_height, 0.01); - float px = (mix_mode == 1) ? max(footprint.x, footprint.y) : footprint.z; - float sharp = clamp(period / max(4.0 * px, 1e-6) - 0.5, 0.0, 1.0); - if (sharp <= 0.0) - return palette_rgb[index]; - - vec3 picked; - if (mix_mode == 1) { - // Ordered 4x4 Bayer over floor(x / cell), floor(y / cell). The CPU's table - // 0 8 2 10 - // 12 4 14 6 - // 3 11 1 9 - // 15 7 13 5 - // is 4 * bayer2(x % 2, y % 2) + bayer2(x / 2, y / 2), which needs no array (GLSL 1.10 has - // no constant arrays). - float cell = max(dither_cell, 0.01); - float gx = mod(floor(pos.x / cell), 4.0); - float gy = mod(floor(pos.y / cell), 4.0); - float bayer = 4.0 * bayer2(mod(gx, 2.0), mod(gy, 2.0)) + bayer2(floor(gx / 2.0), floor(gy / 2.0)); - picked = filament_rgb[(num / den > (bayer + 0.5) / 16.0) ? a : b]; - } else { - // Z bands: one per band height, the band's phase in the a/b cycle picks the filament. Both - // operands are integer-valued, so the half keeps "phase < num" exact under float rounding. - float slot = floor(pos.z / max(layer_height, 0.01)); - float phase = mod(slot, den); - picked = filament_rgb[(phase < num - 0.5) ? a : b]; - } - return mix(palette_rgb[index], picked, sharp); + return filament_rgb[a]; } void main() @@ -364,9 +311,6 @@ void main() // world position and perturb the world normal. vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz))); vec3 tex_pos = world_pos.xyz - tex_anchor; // the frame the texture is projected in, as the bake does - // World mm per pixel, for pre-filtering the interleave in printed_color(). Taken here because the - // albedo branch at the end of main() is non-uniform control flow, where derivatives are undefined. - vec3 pos_fwidth = fwidth(world_pos.xyz); if (volume_mirrored) triangle_normal = -triangle_normal; @@ -508,6 +452,6 @@ void main() // orientation and scale about the volume's origin, see texture_displacement_bake_frame()), so // measuring z from the bed instead shifted the band phase by the volume origin's height - a // different filament in the same place than the bake produces. - albedo = printed_color(nearest_palette_entry(texture(color_tex, color_uv).rgb), tex_pos, triangle_normal, pos_fwidth); + albedo = printed_color(nearest_palette_entry(texture(color_tex, color_uv).rgb)); out_color = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a); } diff --git a/src/libslic3r/TextureBake/TextureBakeDecimate.cpp b/src/libslic3r/TextureBake/TextureBakeDecimate.cpp index c03607e99c..56551b8e84 100644 --- a/src/libslic3r/TextureBake/TextureBakeDecimate.cpp +++ b/src/libslic3r/TextureBake/TextureBakeDecimate.cpp @@ -551,9 +551,13 @@ DecimateResult decimate(const TriSoup &geometry, size_t target_triangles, bool h // Rebuild from the surviving faces, with per-face normals. TriSoup &out = result.geometry; + if (!face_color.empty()) + result.face_color.reserve(active_faces); for (size_t f = 0; f < face_count; ++f) { if (faces[f * 3] < 0) continue; + if (!face_color.empty()) + result.face_color.push_back(f < face_color.size() ? face_color[f] : -1); const Vec3f a = pos[size_t(faces[f * 3])].cast(); const Vec3f b = pos[size_t(faces[f * 3 + 1])].cast(); const Vec3f c = pos[size_t(faces[f * 3 + 2])].cast(); diff --git a/src/libslic3r/TextureBake/TextureBakeDecimate.hpp b/src/libslic3r/TextureBake/TextureBakeDecimate.hpp index 8b31dbb642..273ad9af35 100644 --- a/src/libslic3r/TextureBake/TextureBakeDecimate.hpp +++ b/src/libslic3r/TextureBake/TextureBakeDecimate.hpp @@ -46,6 +46,13 @@ using DecimateProgressFn = std::function; struct DecimateResult { TriSoup geometry; + // One entry per output face, carried from the `face_color` handed in: a colour difference is a + // crease, so no collapse ever merges two faces of different colour and every survivor keeps exactly + // the colour it came with. Empty when no `face_color` was given. + // + // This is what lets the caller colour the simplified mesh by *provenance* rather than by sampling it + // again: the input colours were masked by the paint on the fine mesh, where that mask is exact. + std::vector face_color; // The locked faces alone met the target, so it was unreachable without touching preserved // geometry. bool locked_over_budget = false; diff --git a/src/libslic3r/TextureBake/TextureBakeMesh.cpp b/src/libslic3r/TextureBake/TextureBakeMesh.cpp index caf6e22d36..0d0a097f98 100644 --- a/src/libslic3r/TextureBake/TextureBakeMesh.cpp +++ b/src/libslic3r/TextureBake/TextureBakeMesh.cpp @@ -3,6 +3,7 @@ #include "libslic3r/Point.hpp" #include +#include #include #include #include @@ -42,7 +43,7 @@ TriSoup to_soup(const indexed_triangle_set &its, const std::vector &fac return out; } -indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup) +indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup, std::vector *face_color) { indexed_triangle_set out; const size_t n = soup.pos.size(); @@ -54,12 +55,22 @@ indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup) if (map.inserted()) out.vertices.push_back(soup.pos[i]); } + const bool track_color = face_color != nullptr && !face_color->empty(); + std::vector kept_color; + if (track_color) + kept_color.reserve(face_color->size()); for (size_t t = 0; t + 2 < n; t += 3) { // Welded-together corners carry no area. if (id[t] == id[t + 1] || id[t + 1] == id[t + 2] || id[t] == id[t + 2]) continue; out.indices.emplace_back(id[t], id[t + 1], id[t + 2]); + if (track_color) { + const size_t src = t / 3; + kept_color.push_back(src < face_color->size() ? (*face_color)[src] : -1); + } } + if (track_color) + *face_color = std::move(kept_color); return out; } diff --git a/src/libslic3r/TextureBake/TextureBakeMesh.hpp b/src/libslic3r/TextureBake/TextureBakeMesh.hpp index 860aa1445d..7b05ed2451 100644 --- a/src/libslic3r/TextureBake/TextureBakeMesh.hpp +++ b/src/libslic3r/TextureBake/TextureBakeMesh.hpp @@ -15,7 +15,10 @@ namespace TextureBake { TriSoup to_soup(const indexed_triangle_set &its, const std::vector &face_excluded = {}); // Welds at the geometry grid. -indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup); +// `face_color`, when given, is read as one entry per soup triangle and rewritten to match the output. +// Welding can leave a triangle with no area, and those are dropped here, so the two would otherwise +// fall out of step. +indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup, std::vector *face_color = nullptr); } // namespace TextureBake } // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakePipeline.cpp b/src/libslic3r/TextureBake/TextureBakePipeline.cpp index b44ae837bc..d7f9d9a7b9 100644 --- a/src/libslic3r/TextureBake/TextureBakePipeline.cpp +++ b/src/libslic3r/TextureBake/TextureBakePipeline.cpp @@ -290,6 +290,34 @@ PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample, return result; } + // Colour per face, taken here and carried from here on. This is the only point where the paint mask + // is exact: `exclude_weight` says which faces the paint left out, and the mesh is still the refined + // one the displacement produced. Everything downstream (the collapse, the T-junction repair) carries + // these along rather than sampling again, and the caller uses them as they are. + // + // It also gives the collapse its crease criterion: an edge between two colours is never collapsed + // across, which is what keeps a survivor's colour well defined. + if (color_sample) { + const size_t nf = displaced.triangle_count(); + result.face_color.assign(nf, -1); + const bool have_w = !displaced.exclude_weight.empty(); + tbb::parallel_for(tbb::blocked_range(0, nf), [&](const tbb::blocked_range &r) { + for (size_t t = r.begin(); t < r.end(); ++t) { + // Unpainted faces take no colour at all, which is what stops the texture appearing on + // surfaces the paint never covered. + if (have_w && (displaced.exclude_weight[t * 3] + displaced.exclude_weight[t * 3 + 1] + + displaced.exclude_weight[t * 3 + 2]) / 3.f > 0.99f) + continue; // stays FACE_UNPAINTED + const Vec3f &a = displaced.pos[t * 3], &b = displaced.pos[t * 3 + 1], &c = displaced.pos[t * 3 + 2]; + const int sampled = color_sample((a + b + c) / 3.f, displaced.nrm[t * 3]); + // Painted either way. The sampler expects a point on the base surface and these are on + // the displaced one, so off the patch by more than its tolerance it simply says "no + // colour" - which must not be confused with "not painted". + result.face_color[t] = (sampled >= 0) ? sampled : FACE_NO_COLOUR; + } + }); + } + // 4. Decimate - export only. A bake needs the face-parent map, which a collapse destroys. std::vector parent = std::move(sub.face_parent_id); const size_t displaced_before_decimate = displaced.triangle_count(); @@ -323,24 +351,8 @@ PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample, // unless the budget was lowered until decimation had to run. Only collapses costing less than // harvest_tol are taken, so this does not reach the relief. const bool harvest_only = !over_budget && settings.harvest_flat && displaced.triangle_count() > 0; + std::vector &face_color = result.face_color; if (over_budget || harvest_only) { - // Colour per face on the fine mesh, so colour boundaries become creases the collapse - // respects. Excluded (unpainted) faces take no colour. - std::vector face_color; - if (color_sample) { - const size_t nf = displaced.triangle_count(); - face_color.assign(nf, -1); - const bool have_w = !displaced.exclude_weight.empty(); - tbb::parallel_for(tbb::blocked_range(0, nf), [&](const tbb::blocked_range &r) { - for (size_t t = r.begin(); t < r.end(); ++t) { - if (have_w && (displaced.exclude_weight[t * 3] + displaced.exclude_weight[t * 3 + 1] + - displaced.exclude_weight[t * 3 + 2]) / 3.f > 0.99f) - continue; - const Vec3f &a = displaced.pos[t * 3], &b = displaced.pos[t * 3 + 1], &c = displaced.pos[t * 3 + 2]; - face_color[t] = color_sample((a + b + c) / 3.f, displaced.nrm[t * 3]); - } - }); - } // Harvesting alone is asked for by handing it the count it already has: nothing is then // over the target, so the loop only ever pops collapses under the tolerance. const size_t before = displaced.triangle_count(); @@ -350,6 +362,7 @@ PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample, result.locked_over_budget = dec.locked_over_budget; result.budget_limited = result.simplified = dec.target_cost_detail; displaced = std::move(dec.geometry); + face_color = std::move(dec.face_color); lap("decimate", displaced, over_budget ? "over budget, simplified" : "flat faces harvested"); BOOST_LOG_TRIVIAL(info) << "TextureBake decimate: " << before << " -> " << displaced.triangle_count() << (over_budget ? " (budget " : " (flat harvest, budget ") << target << ")"; @@ -377,7 +390,7 @@ PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample, // 6. Close the T-junctions decimation left behind. Only meaningful when it ran. if (mode == PipelineMode::Export && parent.empty()) { - displaced = resolve_t_junctions(displaced); + displaced = resolve_t_junctions(displaced, {}, &result.face_color); lap("repair", displaced); } diff --git a/src/libslic3r/TextureBake/TextureBakePipeline.hpp b/src/libslic3r/TextureBake/TextureBakePipeline.hpp index dbcc46259a..1fc4e4aae9 100644 --- a/src/libslic3r/TextureBake/TextureBakePipeline.hpp +++ b/src/libslic3r/TextureBake/TextureBakePipeline.hpp @@ -108,9 +108,27 @@ using PipelineProgressFn = std::function; +// Sentinels for PipelineResult::face_color. +static constexpr int FACE_UNPAINTED = -1; // the paint did not cover this face's origin +static constexpr int FACE_NO_COLOUR = -2; // painted, but the sampler returned nothing at this point + struct PipelineResult { TriSoup geometry; + // One entry per output face, carried through decimation, the T-junction repair and the weld. + // FACE_UNPAINTED means the paint never covered the geometry this face came from; anything else means + // it did, and is the palette index `color_sample` returned there (FACE_NO_COLOUR when it returned + // none). The distinction matters: the sampler answers for points on the *base* surface, and these + // are sampled on the displaced one, so a painted face can easily come back without a colour. Only + // the painted/unpainted split is reliable here, and that is what a caller should use it for. + // + // Empty unless the caller gave a `color_sample`. + // + // A caller that needs per-face colour must use this rather than sampling the result again. The + // result is displaced geometry: a point on it is no longer where its base surface was, so matching + // it back by proximity colours whatever base surface happens to be nearest - which on a part thinner + // than the relief depth is the *opposite* face, picking up the texture meant for the painted one. + std::vector face_color; // Output face -> input face. Empty in Export mode, where decimation invalidates it. std::vector face_parent_id; bool safety_cap_hit = false; diff --git a/src/libslic3r/TextureBake/TextureBakeRepair.cpp b/src/libslic3r/TextureBake/TextureBakeRepair.cpp index 49f427d461..877379b574 100644 --- a/src/libslic3r/TextureBake/TextureBakeRepair.cpp +++ b/src/libslic3r/TextureBake/TextureBakeRepair.cpp @@ -72,7 +72,8 @@ size_t count_area_slivers(const TriSoup &geometry) return n; } -TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts) +TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts, + std::vector *face_color) { const size_t n_tri = geometry.triangle_count(); const double on_tol2 = opts.on_seg_tol * opts.on_seg_tol; @@ -98,7 +99,12 @@ TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts) // grid. A needle reads as watertight yet is deleted downstream, and dropping it leaves exactly // the on-edge-vertex topology the pass below closes. std::vector> faces; + // Parallel to `faces` throughout, so a split or a dropped degenerate keeps the two in step. + const bool track_color = face_color != nullptr && !face_color->empty(); + std::vector colors; faces.reserve(n_tri); + if (track_color) + colors.reserve(n_tri); for (size_t t = 0; t < n_tri; ++t) { const int a = vid[t * 3], b = vid[t * 3 + 1], c = vid[t * 3 + 2]; if (a == b || b == c || a == c) @@ -108,6 +114,8 @@ TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts) if (u.cross(w).squaredNorm() < DEGENERATE_AREA_SQ) continue; faces.push_back({ a, b, c }); + if (track_color) + colors.push_back(t < face_color->size() ? (*face_color)[t] : -1); } for (int iter = 0; iter < opts.max_iters; ++iter) { @@ -166,11 +174,16 @@ TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts) break; std::vector> next; + std::vector next_colors; next.reserve(faces.size() + splits.size() * 2); + if (track_color) + next_colors.reserve(next.capacity()); for (size_t fi = 0; fi < faces.size(); ++fi) { const auto it = splits.find(fi); if (it == splits.end()) { next.push_back(faces[fi]); + if (track_color) + next_colors.push_back(colors[fi]); continue; } const auto &f = faces[fi]; @@ -195,11 +208,18 @@ TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts) seq.insert(seq.end(), sp.mids.rbegin(), sp.mids.rend()); seq.push_back(sp.a); } - for (size_t s = 0; s + 1 < seq.size(); ++s) + for (size_t s = 0; s + 1 < seq.size(); ++s) { next.push_back({ seq[s], seq[s + 1], apex }); + if (track_color) + next_colors.push_back(colors[fi]); // every piece of a split face keeps its colour + } } faces.swap(next); + if (track_color) + colors.swap(next_colors); } + if (track_color) + *face_color = std::move(colors); TriSoup out; out.pos.reserve(faces.size() * 3); diff --git a/src/libslic3r/TextureBake/TextureBakeRepair.hpp b/src/libslic3r/TextureBake/TextureBakeRepair.hpp index 07d5323059..ed949dacff 100644 --- a/src/libslic3r/TextureBake/TextureBakeRepair.hpp +++ b/src/libslic3r/TextureBake/TextureBakeRepair.hpp @@ -43,7 +43,12 @@ struct RepairOptions int max_iters = 16; }; -TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts = {}); +// `face_color`, when given, is read as one entry per input face and rewritten to match the output: a +// face split to close a T-junction hands its colour to every piece, and a degenerate face dropped on +// the way takes its entry with it. Without this the caller would have no way to keep a per-face colour +// across this pass, which changes the triangle count. +TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts = {}, + std::vector *face_color = nullptr); } // namespace TextureBake } // namespace Slic3r diff --git a/src/libslic3r/TextureDisplacement.cpp b/src/libslic3r/TextureDisplacement.cpp index 5ec7ccdcfb..d0bbb114c6 100644 --- a/src/libslic3r/TextureDisplacement.cpp +++ b/src/libslic3r/TextureDisplacement.cpp @@ -2372,12 +2372,11 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set // // The *palette* index, not the printed filament. The decimation treats any edge whose two faces // differ as a crease (TextureBakeDecimate.cpp), so it must only ever see where the **perceived** - // colour changes - which is exactly what ColorResolveFn's own contract says the interleaving may - // never be fed into. Handing it the resolved filament made every Z band boundary a crease: on an - // upright wall that is one crease per band, so the collapse ran along those lines and left a stack - // of horizontal slivers, each printing in a single filament. Those were the horizontal colour - // lines in the baked result, and they also spent the triangle budget drawing a pattern the eye is - // meant to blend away. Faces the paint excludes are skipped by the pipeline itself. + // colour changes. A mix is one perceived colour however its components are laid down, which is why + // it has to be the palette index here: back when this was handed a per-triangle interleave instead, + // every band boundary read as a crease, the collapse ran along those lines and left a stack of + // horizontal slivers, and the triangle budget went on drawing a pattern the eye is meant to blend + // away. Faces the paint excludes are skipped by the pipeline itself. const TextureBake::ColorSampleFn color_sample = color_sampler ? TextureBake::ColorSampleFn([&color_sampler](const Vec3f &p, const Vec3f &n) { return color_sampler(p, n); @@ -2403,7 +2402,7 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set stats->triangles_budget = result.triangles_budget; stats->budget_limited = result.budget_limited; } - indexed_triangle_set out = TextureBake::to_indexed_triangle_set(result.geometry); + indexed_triangle_set out = TextureBake::to_indexed_triangle_set(result.geometry, &result.face_color); if (out.indices.empty()) return mesh; if (flip_normals) @@ -2424,6 +2423,8 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set max_depth = std::max(max_depth, std::abs(layer.depth_mm)); const float relief_tol = max_depth + paint_tol; std::vector palette(out.indices.size(), -1); + const std::vector &face_mask = result.face_color; + const bool have_face_mask = face_mask.size() == out.indices.size(); tbb::parallel_for(tbb::blocked_range(0, out.indices.size()), [&](const tbb::blocked_range &r) { for (size_t i = r.begin(); i < r.end(); ++i) { const stl_triangle_vertex_indices &t = out.indices[i]; @@ -2439,8 +2440,19 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set // reason; this path was the inconsistent one. Vec3f foot = centroid, base_n = Vec3f::UnitZ(); const float d2 = painted_closest(centroid, &foot, &base_n); - if (!all_painted && d2 >= relief_tol * relief_tol) + // Which faces may be coloured comes from the pipeline, which recorded it on the + // refined mesh where the paint mask is exact, and carried it through the collapse, + // the T-junction repair and the weld. Proximity cannot answer this: a displaced face + // is no longer where its base was, so on a part thinner than the relief depth the + // nearest painted surface to the *opposite* face is the painted one, and the texture + // appeared there too. Only the position to sample at still comes from the base + // surface, for the projection reason above. + if (have_face_mask) { + if (face_mask[i] == TextureBake::FACE_UNPAINTED) + continue; + } else if (!all_painted && d2 >= relief_tol * relief_tol) { continue; + } palette[i] = sampler(foot, base_n); } }); @@ -2462,7 +2474,7 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set Vec3f normal = (b - a).cross(c - a); const float nl = normal.norm(); normal = (nl > 0.f) ? Vec3f(normal / nl) : Vec3f::UnitZ(); - const int filament = color->resolve ? color->resolve(palette[i], centroid, normal) : palette[i]; + const int filament = palette[i]; if (filament >= 0) out_color[i] = uint8_t(std::min(filament + 1, 255)); } @@ -2919,8 +2931,7 @@ static indexed_triangle_set build_texture_displacement_in_place( Vec3f normal = (b - a).cross(c - a); const float nl = normal.norm(); normal = (nl > 0.f) ? Vec3f(normal / nl) : Vec3f::UnitZ(); - const int filament = color->resolve ? color->resolve(triangle_palette[i], centroid, normal) - : triangle_palette[i]; + const int filament = triangle_palette[i]; if (filament >= 0) out_color[i] = uint8_t(std::min(filament + 1, 255)); } diff --git a/src/libslic3r/TextureDisplacement.hpp b/src/libslic3r/TextureDisplacement.hpp index 94d717dea6..34c3543088 100644 --- a/src/libslic3r/TextureDisplacement.hpp +++ b/src/libslic3r/TextureDisplacement.hpp @@ -321,25 +321,6 @@ struct TextureDisplacementLayer } }; -// How a *mixed* palette entry - one that names two filaments rather than one - is turned into real -// per-facet paint. An MMU extrudes one filament at a time, so an intermediate colour exists only by -// interleaving two of them finely enough that the eye does the blending. -enum class ColorMixMode : int -{ - // Horizontal bands: which of the two filaments a point takes depends on its height, so - // consecutive print layers alternate. This is how filament-blend prints actually work, and on a - // vertical-ish surface it reads as a genuinely smooth colour. On a near-horizontal surface a whole - // layer is one band, so the blend disappears - that is what XYDither is for. - ZBands = 0, - // An ordered (Bayer) checkerboard across the surface, at any orientation. Independent of layer - // height, but its cell is around the size of one facet, so a fine mix can read as texture rather - // than as a clean blend. - XYDither = 1, - // Per triangle, by its orientation: bands where the surface is upright enough for consecutive - // layers to alternate, the checkerboard where it faces up or down and a layer would be one band. - // The default - a flat-topped part with a mix on top gets no blend at all from bands alone. - Auto = 2, -}; // Settings that apply to the whole layer stack rather than to one layer, held per ModelVolume next // to texture_displacement_layers and consumed by build_texture_displacement(). @@ -406,7 +387,6 @@ struct TextureDisplacementOptions // image (TextureDetail::flat_colors): a texture of flat colours prints in single filaments, a // photograph or gradient in mixes. Off forces single filaments everywhere. bool color_mix_enabled = true; - ColorMixMode color_mix_mode = ColorMixMode::Auto; // Majority-filter passes over the assigned colours. See TextureColorRequest::despeckle_passes - // this is the control for it, and 2 is enough to clear the salt-and-pepper an image with detail // finer than the mesh leaves behind, without eating features that are genuinely a facet wide. @@ -414,11 +394,9 @@ struct TextureDisplacementOptions template void serialize(Archive &ar) { - int mix_mode = int(color_mix_mode); ar(displace_border, smooth_enabled, smooth_strength, smooth_iterations, smooth_skip_border, pipeline_v2, v2_refine_mm, v2_regularize, v2_max_triangles_k, - v2_relocate, color_mix_enabled, mix_mode, color_despeckle); - color_mix_mode = ColorMixMode(mix_mode); + v2_relocate, color_mix_enabled, color_despeckle); } }; @@ -511,17 +489,9 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer // is. See GLGizmoTextureDisplacement::make_palette_quantizer(). using ColorQuantizeFn = std::function; -// Resolves a palette index plus a surface position to the filament index that position should print -// in. A pure entry ignores the position; a mixed one interleaves its two filaments per ColorMixMode. -// -// Deliberately separate from ColorQuantizeFn, and deliberately *not* used by the subdivision's colour -// criterion: that criterion asks where the **perceived** colour changes, and must not see the -// interleaving. Refining on every band or dither-cell boundary would spend the whole triangle budget -// drawing a pattern the eye is supposed to blend away. -using ColorResolveFn = std::function; // One printable colour: either a loaded filament on its own, or a blend of two of them realised by -// interleaving (see ColorMixMode). Plain data, so it can be captured into a background job. +// interleaving, which the slicer does per print layer. Plain data, so it can be captured into a job. struct PrintableColor { Vec3f rgb = Vec3f::Zero(); // what it looks like; for a mix, the perceptual average of the two @@ -538,9 +508,6 @@ struct TextureColorSettings { std::vector palette; std::vector palette_pure; // the filaments alone, for flat-colour images - ColorMixMode mix_mode = ColorMixMode::ZBands; - float layer_height = 0.2f; // sizes the Z bands - float dither_cell_mm = 0.4f; // sizes the XY dither cells int despeckle_passes = 2; bool empty() const { return palette.empty(); } @@ -780,9 +747,6 @@ struct TextureColorRequest // made of flat colours (TextureDetail::flat_colors) is matched with this one, so a tile or a logo // prints in single filaments while a photograph on another layer may still use mixes. ColorQuantizeFn quantize_pure; - // Palette index + position -> filament. Optional: without it a palette index is taken to be a - // filament index directly, which is the no-mixing case. - ColorResolveFn resolve; // Majority-filter passes over the *perceived* colour, before any interleaving is resolved. // // Sampling a detailed image once per triangle leaves salt-and-pepper wherever the image's own diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp index c467a7768d..b6d68383fe 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp @@ -1514,11 +1514,8 @@ void GLGizmoTextureDisplacement::render_shaded_preview_mesh() // How the entry prints: its filament, or for a mix the two it interleaves and in what ratio. shader->set_uniform(("palette_a" + idx).c_str(), e.a); shader->set_uniform(("palette_b" + idx).c_str(), e.b); - shader->set_uniform(("palette_num" + idx).c_str(), e.num); - shader->set_uniform(("palette_den" + idx).c_str(), e.den); } // The filaments those indices refer to, and the interleave the shader resolves a mix with - the - // same inputs make_mix_resolver() gets, so the preview shows the pattern that prints rather than // the mix's smooth average colour. m_palette_filaments is what m_shaded_preview_palette was built from. const int filament_count = (palette_count > 0) ? int(std::min(m_palette_filaments.size(), size_t(PALETTE_MAX_FILAMENTS))) : 0; @@ -1527,9 +1524,6 @@ void GLGizmoTextureDisplacement::render_shaded_preview_mesh() const ColorRGBA &c = m_palette_filaments[size_t(i)]; shader->set_uniform(("filament_rgb[" + std::to_string(i) + "]").c_str(), Vec3f(c.r(), c.g(), c.b())); } - shader->set_uniform("mix_mode", int(mv->texture_displacement_options.color_mix_mode)); - shader->set_uniform("layer_height", color_band_mm(*mv)); // as color_settings_for() - shader->set_uniform("dither_cell", std::max(m_subdivide_color_mm, 0.05f) * 2.f); // as color_settings_for() if (color_tex != nullptr) { shader->set_uniform("color_tex", 1); glsafe(::glActiveTexture(GL_TEXTURE1)); @@ -2074,7 +2068,13 @@ void GLGizmoTextureDisplacement::queue_preview_job() // The filament list the result's indices refer to, captured with the job rather than read back // when it lands - loading a filament meanwhile must not recolour a preview computed against a // different list. - const std::vector filaments = m_palette_filaments; + // Every extruder, not the palette's physical-only list: the bake writes the filament it resolved + // to, and a mix resolves to a *mixed filament slot*, which is an extruder past the physical ones. + // Grouping against the shorter list dropped every triangle carrying such a slot out of the mesh + // entirely - the relief vanished and left only the few triangles that happened to print in a plain + // filament. The palette still has to be built from physical filaments alone (see filament_palette()), + // which is why these two are not the same list. + const std::vector filaments = wxGetApp().plater()->get_extruders_colors(); m_preview_job_running = true; auto &worker = wxGetApp().plater()->get_ui_job_worker(); @@ -4271,19 +4271,44 @@ bool GLGizmoTextureDisplacement::any_layer_colors(const ModelVolume &mv) return false; } +void GLGizmoTextureDisplacement::bind_mixes_to_filament_slots(std::vector &palette) +{ + Sidebar *sidebar = &wxGetApp().plater()->sidebar(); + if (sidebar == nullptr) + return; + for (PaletteEntry &e : palette) { + if (!e.is_mix()) + continue; + // Components are 1-based in the config; the ratios are percentages summing to 100, which is the + // form create_mixed_filament_from_result() normalises from. + const int a_pct = int(std::lround(100.0 * double(e.num) / double(e.den))); + const int slot = sidebar->ensure_mixed_filament({ unsigned(e.a + 1), unsigned(e.b + 1) }, + { a_pct, 100 - a_pct }); + if (slot >= 0) { + e.a = e.b = slot; + e.num = e.den = 1; + } else { + // No room for another slot. Collapse to the component that dominates the blend, which is what + // the old per-triangle path did on a surface it could not band anyway. + const int dominant = (e.num * 2 >= e.den) ? e.a : e.b; + e.a = e.b = dominant; + e.num = e.den = 1; + } + } +} + TextureColorSettings GLGizmoTextureDisplacement::color_settings_for(const ModelVolume &mv) { TextureColorSettings out; if (!any_layer_colors(mv)) return out; // nothing is colouring: every colour path stays switched off out.palette = cached_palette(); - out.palette_pure = make_palette(m_palette_filaments, /* mixing */ false); - out.mix_mode = mv.texture_displacement_options.color_mix_mode; + out.palette_pure = make_palette(m_palette_filaments, /* mixing */ false, PALETTE_MAX_ENTRIES); + // Done here rather than in cached_palette(): this runs when a preview or a bake is queued, off a + // user action, while that one is also touched from the render path - and creating filament slots + // there would mutate the project mid-frame. + bind_mixes_to_filament_slots(out.palette); out.despeckle_passes = mv.texture_displacement_options.color_despeckle; - out.layer_height = color_band_mm(mv); - // The dither cell is tied to the colour-detail target: a cell much smaller than a facet cannot be - // drawn at all, and one much larger stops reading as a blend and starts reading as a check. - out.dither_cell_mm = std::max(m_subdivide_color_mm, 0.05f) * 2.f; return out; } @@ -4296,10 +4321,15 @@ const std::vector &GLGizmoTextureDispl const ModelVolume *mv = texture_volume(); const bool mixing = mv != nullptr && mv->texture_displacement_options.color_mix_enabled; std::vector filaments = filament_palette(); - if (m_palette_cache.empty() || filaments != m_palette_filaments || mixing != m_palette_mixing) { + // Every mix costs a filament slot once they are bound to one, and the mask can name only so many + // states, so the palette has to leave room beside the physical filaments it already counts. + const int cap = int(EnforcerBlockerType::ExtruderMax); + if (m_palette_cache.empty() || filaments != m_palette_filaments || mixing != m_palette_mixing || + cap != m_palette_cap) { m_palette_filaments = std::move(filaments); m_palette_mixing = mixing; - m_palette_cache = make_palette(m_palette_filaments, mixing); + m_palette_cap = cap; + m_palette_cache = make_palette(m_palette_filaments, mixing, cap); m_palette_quantizer = make_palette_quantizer(m_palette_cache); } return m_palette_cache; @@ -4307,40 +4337,29 @@ const std::vector &GLGizmoTextureDispl std::vector GLGizmoTextureDisplacement::filament_palette() { - std::vector palette = wxGetApp().plater()->get_extruders_colors(); - // mmu_segmentation_facets encodes the filament in a 6-bit prefix code and stops at Extruder16. + std::vector all = wxGetApp().plater()->get_extruders_colors(); + + // Physical filaments only. The mixes this palette produces each become a mixed filament slot of + // their own (see bind_mixes_to_filament_slots()), and those slots are extruders too - so taking the + // list as it comes meant the next rebuild mixed *them* again, and handed components naming a + // virtual slot to a blend that can only name physical ones. That is what left entries reading + // "filament 1 plus nothing" and raised "Mixed filament has invalid or mismatched components". + const auto *is_mixed = wxGetApp().preset_bundle->project_config.option("filament_is_mixed"); + std::vector palette; + palette.reserve(all.size()); + for (size_t i = 0; i < all.size(); ++i) + if (is_mixed == nullptr || i >= is_mixed->values.size() || !is_mixed->values[i]) + palette.push_back(all[i]); + + // A paint mask can only name so many states, and every mix spends one beside these. if (palette.size() > size_t(EnforcerBlockerType::ExtruderMax)) palette.resize(size_t(EnforcerBlockerType::ExtruderMax)); return palette; } -float GLGizmoTextureDisplacement::color_band_mm(const ModelVolume &mv) -{ - const float lh = print_layer_height(); - const float edge = (mv.texture_displacement_options.v2_refine_mm > 0.f) ? mv.texture_displacement_options.v2_refine_mm - : v2_recommendation(mv).edge_mm; - if (edge <= 0.f || lh <= 0.f) - return lh; - // A refined triangle of edge e stacks in rows about 0.87 * e apart (an equilateral triangle's - // height), and a dither needs at least two rows per period to be a dither at all. - constexpr float ROW_PER_EDGE = 0.87f; - return lh * std::max(1.f, std::ceil(2.f * ROW_PER_EDGE * edge / lh)); -} - -float GLGizmoTextureDisplacement::print_layer_height() -{ - try { - const DynamicPrintConfig &cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; - if (const ConfigOptionFloat *opt = cfg.option("layer_height"); opt != nullptr) - if (opt->value > 1e-3) - return float(opt->value); - } catch (...) { - } - return 0.2f; -} std::vector GLGizmoTextureDisplacement::make_palette( - const std::vector &filaments, bool mixing) + const std::vector &filaments, bool mixing, int max_entries) { std::vector out; const int n = int(filaments.size()); @@ -4357,7 +4376,7 @@ std::vector GLGizmoTextureDisplacement const int pairs = n * (n - 1) / 2; int steps = 0; for (int s = 5; s >= 1; --s) - if (n + pairs * s <= PALETTE_MAX_ENTRIES) { + if (n + pairs * s <= max_entries) { steps = s; break; } @@ -4379,51 +4398,6 @@ std::vector GLGizmoTextureDisplacement return out; } -ColorResolveFn GLGizmoTextureDisplacement::make_mix_resolver(const std::vector &palette, - ColorMixMode mode, float layer_height, - float cell_mm) -{ - if (palette.empty()) - return nullptr; - auto entries = std::make_shared>(palette); - const float band = std::max(layer_height, 0.01f); - const float cell = std::max(cell_mm, 0.01f); - - return [entries, mode, band, cell](int index, const Vec3f &pos, const Vec3f &normal) -> int { - if (index < 0 || size_t(index) >= entries->size()) - return -1; - const PaletteEntry &e = (*entries)[size_t(index)]; - if (!e.is_mix()) - return e.a; - - // Which of the two filaments this point falls on. Both patterns are *ordered*, never random: - // the eye blends a regular pattern into a flat colour, and turns a random one into noise. - // Auto: bands wherever the surface is steeper than ~45 degrees - consecutive layers alternate - // there, which is how a blend prints and reads. On a flat-facing surface a layer is one band - // and the only way to interleave is a checkerboard across the surface, which at print scale - // reads as a pattern rather than a colour; there the mix falls back to its dominant filament. - const bool upright = std::abs(normal.z()) < 0.7f; - if (mode == ColorMixMode::Auto && !upright) - return e.num * 2 >= e.den ? e.a : e.b; - const bool bands = mode == ColorMixMode::ZBands || mode == ColorMixMode::Auto; - if (bands) { - // One band per print layer. floorf, not a cast, so this stays correct below z = 0. - const int slot = int(std::floor(pos.z() / band)); - const int phase = ((slot % e.den) + e.den) % e.den; - return phase < e.num ? e.a : e.b; - } - // Ordered 4x4 Bayer over the surface, indexed by position so the pattern is stable in space - // rather than in triangle order (which would move under any remesh, and read as noise). - static const int BAYER[16] = { 0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5 }; - const int gx = ((int(std::floor(pos.x() / cell)) % 4) + 4) % 4; - const int gy = ((int(std::floor(pos.y() / cell)) % 4) + 4) % 4; - // A third axis would be ideal, but the two dominant ones are enough for a surface pattern and - // keep the cell square on the faces that matter. - const float threshold = (float(BAYER[gy * 4 + gx]) + 0.5f) / 16.f; - return (float(e.num) / float(e.den)) > threshold ? e.a : e.b; - }; -} - ColorQuantizeFn GLGizmoTextureDisplacement::make_palette_quantizer(const std::vector &palette) { if (palette.empty()) @@ -4460,10 +4434,15 @@ ColorQuantizeFn GLGizmoTextureDisplacement::make_palette_quantizer(const std::ve best_pure = int(i); } } - // A mix is an interleave that only reads as its colour from a distance; up close - // it is stripes. Spend it only where it buys a clearly better match than the nearest - // single filament: ten Delta E is a visible step, less is not worth the stripes. - constexpr float PREFER_PURE_DE = 10.f; + // A mix is an interleave that only reads as its colour from a distance; up close it is + // stripes. So it is spent only where it buys a better match than the nearest single + // filament - but "better" was set at ten Delta E, which is not a visible step, it is a + // different colour. Measured over the whole cube that threshold turned 94% of the + // lookups that wanted a mix back into a pure filament, leaving 38%; along a greyscale + // ramp, the shape a height texture actually traces, it cut 80% to 66%. Two Delta E is + // about where a side-by-side difference stops being arguable, which is the right place + // to start paying for stripes. + constexpr float PREFER_PURE_DE = 2.f; if (best_pure >= 0 && palette[size_t(best)].is_mix() && best_pure_d - best_d < PREFER_PURE_DE) best = best_pure; (*lut)[(size_t(r) * E + size_t(g)) * E + size_t(b)] = uint8_t(best); @@ -4563,7 +4542,7 @@ TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh( if (params.subdiv_color_edge_mm > 0.f && !palette.empty()) color = make_combined_color_sampler(mesh.its, layers, current, make_palette_quantizer(palette)); // Note the sampler is built on the *quantizer* alone - the refinement follows perceived - // colour, never the interleaving that realises a mix (see ColorResolveFn). + // colour, never the interleaving that realises a mix - the slicer does that per layer. // "Min edge" is a feature-mode control (it is the floor the curvature test refines down // to); in plain adaptive mode the target edge length is the only criterion, so the floor // must not be allowed to silently override a target the user set below it. @@ -6148,24 +6127,6 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float "of filaments can cover a photo or a gradient. An image of flat colors " "prints the same either way. Off uses one filament per area.")); if (opts.color_mix_enabled) { - slider_label(_L("Mix by")); - const std::string mix_z = _u8L("Layers"); - const std::string mix_xy = _u8L_CONTEXT("Surface", "Texture Displacement"); - const std::string mix_auto = _u8L("Automatic"); - const char *mix_items[] = { mix_z.c_str(), mix_xy.c_str(), mix_auto.c_str() }; - int mix_mode = int(opts.color_mix_mode); - ImGui::SetNextItemWidth(-card_pad); - if (scoped_combo("##color_mix_mode", &mix_mode, mix_items, IM_ARRAYSIZE(mix_items))) { - opts.color_mix_mode = ColorMixMode(mix_mode); - m_preview_params_dirty = true; - } - hover_tip(_u8L("Layers: the two filaments alternate between print layers, which " - "blends smoothly on upright surfaces but disappears on flat-facing " - "ones, where a whole layer is a single band.\n" - "Surface: a fine checkerboard across the surface, which works at " - "any angle but can read as texture rather than as a blend.\n" - "Automatic: layers on upright faces; flat-facing faces take the nearer " - "single filament, since a checkerboard there shows as a pattern.")); ImGui::TextDisabled("%s", Slic3r::format(_u8L("%1% printable colors from %2% filaments"), int(cached_palette().size()), int(m_palette_filaments.size())).c_str()); } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp index 2c6f5bc6c7..2406ddbe84 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp @@ -81,7 +81,11 @@ public: // How many ratios depends on how many filaments there are, so the palette stays bounded: the // quantizer's lookup cube costs one DeltaE00 per cell per entry to fill, and with sixteen // filaments there are already plenty of colours without mixing any of them. - static std::vector make_palette(const std::vector &filaments, bool mixing); + // `max_entries` bounds the whole palette. It is normally the quantizer's own limit, but when the + // mixes become filament slots it has to be the paint mask's instead: a mask can name only + // EnforcerBlockerType::ExtruderMax states, and every mix now occupies one of them. + static std::vector make_palette(const std::vector &filaments, bool mixing, + int max_entries); // Maps an image colour to the closest entry of `palette`, perceptually (CIEDE2000 over CIELAB - a // plain RGB distance picks visibly wrong filaments, most obviously between a saturated colour and @@ -95,9 +99,6 @@ public: // Turns a palette index plus a position into the filament to print there, interleaving the two // filaments of a mixed entry per `mode`. `layer_height` sizes the Z bands; `cell_mm` the dither - // cells. See ColorResolveFn for why this is separate from the quantizer. - static ColorResolveFn make_mix_resolver(const std::vector &palette, ColorMixMode mode, - float layer_height, float cell_mm); // Everything the jobs need to colour with, for the current volume: palette, mix mode, layer // height, despeckle. Empty when no layer is actually colouring. @@ -106,16 +107,20 @@ public: // The printable palette for the current filaments and mixing setting, rebuilt only when either // actually changes - see the definition for why that caching is not optional. const std::vector &cached_palette(); + // Turns every mix in `palette` into a mixed filament slot and rewrites the entry to name that slot + // as a plain filament, so nothing downstream has to know a mix is involved: is_mix() goes false and + // the resolver simply returns it. The per-layer interleaving then happens in the slicer, where it is + // not limited by how fine the mesh is. Entries whose slot could not be created (the paint-state cap) + // fall back to the nearer of the two components. + void bind_mixes_to_filament_slots(std::vector &palette); std::vector m_palette_cache; std::vector m_palette_filaments; + int m_palette_cap = 0; // the max_entries m_palette_cache was built with bool m_palette_mixing = false; ColorQuantizeFn m_palette_quantizer; // The loaded filaments, clamped to the sixteen mmu_segmentation_facets can address. static std::vector filament_palette(); - // The print's layer height, which sizes ColorMixMode::ZBands. Falls back to 0.2 mm if it cannot be - // read - a wrong band size is a cosmetic error, not a reason to refuse to colour anything. - static float print_layer_height(); // The Z band height, in mm. One print layer is the ideal, but the interleave is realised per // *facet*: a band thinner than the mesh can resolve does not dither, it beats against the triangle // grid and comes out as broad horizontal stripes - and since MMU segmentation reads facet colour, @@ -123,7 +128,6 @@ public: // diagonal and knows nothing about the layer height, so the band is rounded up to a whole number of // layers at least two facet rows tall: still exact on the printer, and representable by the mesh // that has to carry it. Used by both the bake settings and the preview shader, so the two agree. - float color_band_mm(const ModelVolume &mv); // The Normal preview's triangles, grouped by the filament they will print in. Colour is per facet // and there are at most sixteen filaments, so the mesh is uploaded once with its index buffer diff --git a/src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.cpp b/src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.cpp index 71c4f4afe7..b876acb62e 100644 --- a/src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.cpp +++ b/src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.cpp @@ -53,9 +53,6 @@ void TextureDisplacementBakeJob::process(Ctl &ctl) color_request.quantize = GLGizmoTextureDisplacement::make_palette_quantizer(m_input.color.palette); if (!m_input.color.palette_pure.empty()) color_request.quantize_pure = GLGizmoTextureDisplacement::make_palette_quantizer(m_input.color.palette_pure); - color_request.resolve = GLGizmoTextureDisplacement::make_mix_resolver( - m_input.color.palette, m_input.color.mix_mode, m_input.color.layer_height, - m_input.color.dither_cell_mm); color_request.despeckle_passes = m_input.color.despeckle_passes; color_request.out_triangle = &m_triangle_color; if (color_request.quantize) diff --git a/src/slic3r/GUI/Jobs/TextureDisplacementPreviewJob.cpp b/src/slic3r/GUI/Jobs/TextureDisplacementPreviewJob.cpp index 49cbae4e81..4243cdd9d3 100644 --- a/src/slic3r/GUI/Jobs/TextureDisplacementPreviewJob.cpp +++ b/src/slic3r/GUI/Jobs/TextureDisplacementPreviewJob.cpp @@ -32,9 +32,6 @@ void TextureDisplacementPreviewJob::process(Ctl &ctl) color_request.quantize = GLGizmoTextureDisplacement::make_palette_quantizer(m_input.color.palette); if (!m_input.color.palette_pure.empty()) color_request.quantize_pure = GLGizmoTextureDisplacement::make_palette_quantizer(m_input.color.palette_pure); - color_request.resolve = GLGizmoTextureDisplacement::make_mix_resolver( - m_input.color.palette, m_input.color.mix_mode, m_input.color.layer_height, - m_input.color.dither_cell_mm); color_request.despeckle_passes = m_input.color.despeckle_passes; color_request.out_triangle = &m_result.triangle_color; if (color_request.quantize) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index a666e23c9c..f557ee9427 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -937,6 +937,7 @@ struct Sidebar::priv StaticLine* m_text_mixed_title{nullptr}; ScalableButton* m_btn_mixed_add{nullptr}; ScalableButton* m_btn_mixed_del{nullptr}; + ScalableButton* m_btn_mixed_del_all{nullptr}; wxScrolledWindow* m_mixed_scroll_area{nullptr}; // independent scrollbar for mixed rows wxPanel* m_panel_mixed_content{nullptr}; wxBoxSizer* m_sizer_mixed_filaments{nullptr}; // two-column, mirrors sizer_filaments @@ -3451,6 +3452,11 @@ Sidebar::Sidebar(Plater *parent) }); title_sizer->Add(p->m_btn_mixed_del, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::IconSpacing())); + p->m_btn_mixed_del_all = new ScalableButton(p->m_panel_mixed_title, wxID_ANY, "delete_all_filaments"); + p->m_btn_mixed_del_all->SetToolTip(_L("Remove all mixed filaments")); + p->m_btn_mixed_del_all->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { remove_all_mixed_filaments(); }); + title_sizer->Add(p->m_btn_mixed_del_all, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::IconSpacing())); + p->m_btn_mixed_add = new ScalableButton(p->m_panel_mixed_title, wxID_ANY, "add_filament"); p->m_btn_mixed_add->SetToolTip(_L("Add mixed filament")); p->m_btn_mixed_add->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { add_mixed_filament(); }); @@ -5124,6 +5130,58 @@ static bool create_mixed_filament_from_result( return true; } +int Sidebar::ensure_mixed_filament(const std::vector &components, const std::vector &ratios) +{ + if (components.size() < 2 || components.size() != ratios.size()) + return -1; + if (p->combos_filament.size() < 2) + return -1; + + // Normalise the way create_mixed_filament_from_result() stores them, so the comparison below sees + // the same text the config holds rather than two spellings of one blend. + int ratio_sum = 0; + for (const int r : ratios) + ratio_sum += r; + if (ratio_sum <= 0) + return -1; + + std::string comp_str, ratio_str; + { + CNumericLocalesSetter c_locale_setter; + for (size_t i = 0; i < components.size(); ++i) { + if (i > 0) { comp_str += ","; ratio_str += ","; } + comp_str += std::to_string(components[i]); + char buf[32]; + std::snprintf(buf, sizeof(buf), "%.4f", float(ratios[i]) / float(ratio_sum)); + ratio_str += buf; + } + } + + const auto &project_config = wxGetApp().preset_bundle->project_config; + const auto *is_mixed_opt = project_config.option("filament_is_mixed"); + const auto *comp_opt = project_config.option("filament_mixed_components"); + const auto *ratios_opt = project_config.option("filament_mixed_sublayer_ratios"); + if (is_mixed_opt != nullptr && comp_opt != nullptr && ratios_opt != nullptr) + for (size_t i = 0; i < is_mixed_opt->values.size(); ++i) + if (is_mixed_opt->values[i] && i < comp_opt->values.size() && i < ratios_opt->values.size() && + comp_opt->values[i] == comp_str && ratios_opt->values[i] == ratio_str) + return int(i); + + if (wxGetApp().preset_bundle->filament_presets.size() >= size_t(EnforcerBlockerType::ExtruderMax)) + return -1; + + std::vector color_strs, names, types; + collect_physical_filament_info(color_strs, names, types); + + MixedFilamentResult result; + result.components = components; + result.ratios = ratios; + const size_t created_at = wxGetApp().preset_bundle->filament_presets.size(); + if (!create_mixed_filament_from_result(this, result, color_strs)) + return -1; + return int(created_at); +} + void Sidebar::add_mixed_filament() { auto* plater = dynamic_cast(GetParent()); @@ -5301,6 +5359,28 @@ void Sidebar::edit_mixed_filament(size_t panel_idx) } } +void Sidebar::remove_all_mixed_filaments() +{ + auto *plater = dynamic_cast(GetParent()); + if (plater == nullptr) + return; + const size_t count = plater->mixed_filament_config_indices().size(); + if (count == 0) + return; + + // Worth a confirmation: this drops filament slots the model may be painted with, and anything + // painted in one falls back to a plain filament. + MessageDialog dlg(this, format_wxstr(_L("Remove all %1% mixed filaments?"), count), _L("Mixed Filament"), + wxYES_NO | wxNO_DEFAULT | wxICON_QUESTION); + if (dlg.ShowModal() != wxID_YES) + return; + + // Back to front: delete_mixed_filament_at() indexes the list as it stands, so removing from the end + // leaves the indices of everything still to go untouched. + for (size_t i = count; i-- > 0;) + delete_mixed_filament_at(i); +} + void Sidebar::delete_mixed_filament_at(size_t panel_idx) { auto* plater = dynamic_cast(GetParent()); diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index c3b4523c49..46192794c6 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -291,8 +291,19 @@ public: // Mixed-color filament sidebar section void add_mixed_filament(); + // The filament slot that blends `components` (1-based physical filament indices) in `ratios` + // (percentages), creating it when no existing mixed slot already describes that blend. Returns the + // 0-based filament index, or -1 when the paint-state cap leaves no room for another one. + // + // Exists so a feature that needs a blend can ask for one without going through the modal dialog: + // the texture displacement gizmo turns each mix in its palette into a slot, which is what moves the + // interleaving from its own paint mask to the slicer, where it happens per layer. + int ensure_mixed_filament(const std::vector &components, const std::vector &ratios); void edit_mixed_filament(size_t idx); void delete_mixed_filament_at(size_t idx); + // Drops every mixed filament at once, after confirming. The texture displacement gizmo can create + // one slot per colour in its palette, so clearing them one at a time is tedious. + void remove_all_mixed_filaments(); void decompose_filament_color(int filament_idx); void recalc_filament_scroll_sizes(); void update_mixed_filament_list();