From 08aa277974c18ab32a81e3c017452ce445e75b80 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 11 Mar 2026 00:27:14 -0500 Subject: [PATCH 01/60] 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 02/60] 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 03/60] 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 04/60] 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 05/60] 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 06/60] 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 07/60] 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 08/60] 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 09/60] 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 10/60] 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 11/60] 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 12/60] 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 13/60] 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 14/60] 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 15/60] 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 16/60] 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 17/60] 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 18/60] 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 19/60] 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 20/60] 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 21/60] 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 22/60] 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 23/60] 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 24/60] 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 25/60] 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 26/60] 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 27/60] 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 28/60] 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 29/60] 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 30/60] 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 31/60] 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 32/60] 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 33/60] 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 34/60] 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 35/60] 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 36/60] 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 37/60] 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 38/60] 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 z6XuG-)rKy4)F)Nt(l~*5KlthH_a%)o|I^ZwQO|9#zv2U zvp1@*5bo6fQ9vX-CINRpRX6`9vjz|49~stAguIYP)5H)MO1t0cO46e8aEmyY1qb#I zmio}~Er2J-;{B2RlgsoqK@2G^h{Md-&9@|_;22m+ek#5m^Dm5nLrRR%SUD7QJ@uuw zXd*J*SnOlm;k~B)^;!;*ea-eag@4(Li<+MUbg?1WkW&9+_GEAKof&}b1_<%Vn+)kJ zX3glRt+DVP_lafd8YJ%e}H)1`?2D=9)V5F3z9 zo9(5#g*eGGFwkg6nv5p$`W4?UwH8!^OZY3#WI8G&nynqC{c|)XiEzmKxwc=bPr4~C zNB)obHb>#!ozuTczoMtI1L+QeGg}5Ip9ki+FuoNil5BhRMx?n=KxK2QuSgI#oK?MLQbhEP;%K%L7( z$opl%v*oI^+2TRlzx+_ZNhuK}E+8?mki&F0KD2$`k;=M38}uWGYu@;Uqq6qY^Pu%r71LFA?Y*aiY_%FE9(J}ef22Zd0_F9jvc(tydVVe=IJYXM z5WG7GZo48nR`e#{X-Mx_^sovR{MP^75g#dxK~|}{n5vkd?avfkG+q=tc~rruJ>!4u zNZt3vk7o-7fGOX)?L-srx#DXUaSQD28#LYjdI7*x1iyp}ZZG^_7X1@PKL@#h2=$Td3a{LVnQ1+vKAmoyiwnWiX>6ug~(mQX2Rz*bV?mH>bJxdq0k}NEuO+% zHIG!V503>#uR#H<(|W6GYn*n7nt*e~!$FFtfibbmcZhN=;n4N=ZMkJPk&z92eB??J z6C=^SKS(Cdod)|gJ2wt;8{@loMN6V_e|XN?86O)J42+*@G0N}h@csx!8>z`3OnNFJ zh<~i?X)hFS>y}w)iLQ2Kv*@vu3bY+CI(*|0OUNv>*S*)%5zkz-`=nbG$M&S>zEPsU zlj-n}qMT6CHqw)91sW*eB}PF!^6Y&Hw9^N{D?*p*n15Pd!|g@Qh(~E#`matfN)i+y z-ppXlYo%QW##ZM0wt;kjd{|+PN2OH?qkbp|E;{{-aArRmJ-TP`zGrYtWj_&ntc}RC zxmMvF^;-{3Sw-Z$-g@`#r0cy*o@yC5iFnWh?Srmz0;EN z*uf{R?($JfUY+t4=TUKiVZZ=1mH%!whu~tmiJSy9w&n5gZmlV6%Pk;CfM9QzR-;1x zPUdH|PC+LTrPWNnAUp^?oH|3r-HPzA7{1#CA?R~#|ed;_J8Az73n%8TvW!KWe z`h*w|$rj|+U>24}xD;cgX#Rj5x8duoE2+E;reAfGm{ab*If0UDcDTCbys7I#wZ zvgj8=OrQGw{Ep8lj>_g=IYG?IHz6(d@0awHr2S^6f|e!(r8c8EAn}u84Bzv~EbJw( z?^q@~aQN-DZn8e`&Gev1!>G8niPrF^Zk08vKw+si36+Tn1*0t=ixqN(yrW!<%m7FQ z;gk@9m7t|bFvYBQudZKpKaH2d%tUFcFEckdE-6n|)e~7doVyG46|>CacVq1KAuJ{3 zim~tWzc0;m03J@$afE>!I9UK*^ScZ{)5B-;;ggul&3%Q;5hT|3Vod23yw@?Sg4uxM zef zyYyoSGMU>a){fiE>+4ZgMtfo6`?lPsw?X1|AlQQzqp@I#0U5jh``r*GmQgOdE*ob= z$;iWUO8&Rp1|` z@CpjfpBJ6HJ>{HCfy7X?Gx8zTCrMqed+XPOBAqODm@+s4Ux86vPo~qrPwZEO$|yXI zOND%O1>O{Ws0Q>kwMiKXDTsujY`xrV*ttAzI~XbM-;x8iPp1SETn805a&ehmIL&2o z8x4D)mbHmQJjDVa8doCXi}P<4xp=0(0)N2?#9;_@y-%RB*c8bqI>$Ba0$A{L;Tl#g z7Q&O=W@m@b-QD6|dQmpZq~MEur+?4C&U?986?|o4k>3HwH`fzwi>Sqx-MQMO$SVHrf(_4!VqpFjE zr+U&u0gRfZX&;m)thMsHX;)o+ZjF0@_HcMhq6>}%foSUap6Ga2A3#!nf=&MI`KL=Q;#jq%L0DonPCC4@ zZMT}CugJwaAKL*8J4Xz!d>8^wG|s&-Nd@GI!H)elUA_ul43U3(Q8n@W6S|t{0Sqq^A#`EoMH*1b8`He_0TphO(wu}Q9obqXhUzAR#}LaAass~X zU)1JlqwzeRjWpS&?vCw5*w8~v-A>tH0USh!u1SOl{_WUso;)q9a%%d$H@$FZ!*?zY9I z+N@xZ0v8zy>rek0)*Ip!7Ld2L8AqJl4@JNX$hp-++8^c4s)5G>L1PpAYJtjnsM6CR z@%(30P>`>cF5vKciKkM|>MR^BYVZ9vQE8MiPAVPW`o2b43GwFbCvd)LlY#Pj-F;I% z&93@(>~h$QYq2WA8Gd53xoofaL9S8iN^hp>ziR7GiP6SO*?%aIOfUcbEftVu*Mk#|CcH)SQ3! zg64z?!ufT%{L8Z~@_yl;1pR{cZY}UV@+4i>u65n)w^!W%{KYXwXzfOAwXkc zqb z>f=MXR$XCv%ZFbK_n5rMH9Y6UwxzL zfZuXslIDTz{tyQiAtH58z@^V=&D~zjT^s_}Er~`~GOyO)M?}#q8<{-flwVb)Vd@sLU@!52Fxdt!k1Pr+SPvhpYy3# zbU%j(55Z+3JN71FxS>i7sV3B1$(ens7CB~n{+dF^fmf?6bnA?D`*sC+lSf&jVl;An zeI{%Z#r@kwM$&-Ac)YvG>l~br4)&I=u8WPAkuX}4)Zph?B_ZbRx5=k8IIxI?&8F$5 zWD%7@W-UlQ?D0JQs0TG&k1H3we|Y>&+g4vFtg=^mFHa435k~#=^HN~lY@Aq|#fbBy zkOpL-gkCS1#K|F#v3Nf_VKs;%6)VC;1=~R>9sx@2v!XF2pingtHK8GF>&-M3sE3M` z!3>F;@5RB+Pg;kxng8OsHPyqB&_7XOce_s}FN6^feGUZUKhN^=c$I!Cmd+B@p|6ji z(A&OpRKCzGUFhwROjGb)bTU`*i`<+B8q-BJZFA+cUafmS6K3FmCNmsurJw^z5qf8; zx}LOyVU;P3sVeVG{04ZIw7mMG*ms0rx z%4K3#Bd7T;iU-{-p609k)%AVvxbaygAf*t&I`-IrGl&#L<${Vex4zeM@SAy3mt$C`6!lx;8B{3?Q{Ngpbj60&c@BSERpB|4DNr8l0x5)#FSscAp^OWWZE;qLstlE zVbSo1X-7MlDMNFmm()^AB-KVt8O-%WYbW3|m&NhPsgKz=eW?qyVm=fUCcj`58wbr^ zRd_#_6pcXJ6ycYia7!za9uQ~;#3B?MSDQ>ppK+|^4?i2ipLrSo9VuhHMKdb?{v{?9 z-N^XQGlTbTujEm&rfKiv<-~O_66??*R^OLi=GojXo8}1KXJ-2FpG?*}-}zk9RGGs6 zdc3|rbg_n`l8hmUK$HwaHyrnA+vk9uh?Ah@{Tx`?zVwpqjPeuDBjRGjXC^Up`bG_- zMK)&F66BLyA7*BV%h1qbh$)o{&Btje2VE++!;lGd!hUZn*_|ee)ZZ++cHYP_iYq^3 z3Hiu%`uuT}m8Na5-0n54lA@+LaIjgdoKNh>_=YI;Z}*J!!~%dFFg_4P$wqyo6S6>4 zOC|k@M66hG=mH`jXw;Z2(FfCS((Xt0g`<|Np~&-YOCstdHpoR_9aDYZ;3ZOiWA2U6 zxDnzE;FdDH*sFBS9c&(*2|nhGrWjxuiuX_Rt?=CqtW(y zT)gkGLft@Ao*%LeOO{yi(m&!}6olmJoos*74NC}p1@L_%ZyaHBrV`;v?+GJkA`I7d zW^34;kH;yL)$XNp9pG%=Z6&B?|K0mV$mB=vnB)>_8}-7f*h41|MhbEj7tj8r@PqDh zvyr&xVNbXz(zUa2p}60F`)kPN-Y@|%%sZFqTgS(S_uf*??u>R$fhd(?V-oFDW%&?y z$SXrmt+N|QcPM`6F@XN}#DLd(=)tJS+pNRh2ecfwUH)a$1()KI^YQ(Icc%sok-!=6 zrcWIa-}h!DV1aLUKhk#JT%o))9y86>@7?dmoW^-)sueoEGhnUYVEkOQj*HTYVn^{X zhEkh4_fdp=OjGssDeL#`ku-T0B4DH2`1||5H#Ou(A z`TLJO2fCbRodA0GQpUhiIOPP`r{$vTl&;Q)-(vo^2Dv1r8)hZynniGZ@x;cI#^B+- zAH8!7CbJ0t{<`FZVoakA7o}$>z}fO1z0+UbpiqpjNdGn3MPu=qkd-zzTiHk*=abtZ zxRCbWAk4&h+KQnb-0-F|<44vnZ*iE0&Vc}W>^73FKk{U zl-lq3&DAH4U~y3iayJVx{Ur0ohL`b(>Q@ys8(3%)`VVzHw5>_6oziR-S^mu7=(2bc@*x}xQ{yw@D5nLGvjNgP z!xBs)K3&PBilsv39_f9n(APWfe6crkWmla1&8IezCBhnmJP9E= ze><(SemUE$QY8t&860Fu#;>DD!kfk)8J55ymmw$0yhK}(f;mrO{nAGLrpXagFa2jg znv%m0L$~W;!+=uU@qF$2F#qH)xMoW)X6#9Z@*fR6c!&TL+6yxbZ@pNFSPUh{2c)F_ z8bjz2mn*x4otYU?>frh{UIO-c2qH(1+EjIZBk?e-SQ-@%M@#L}2+lWjp>GQOndY5h@MSi(pESd(N}i@{+x#5d3` z>bHOksZ5E_XPxQ~!|T3J>+X!$B>Rk-|C~Z!(W!0-a)r;aS0pdgU0!0 zoG_CuzRxU5Lnr@w`)|}C;G5sDJHwFGE%LrAG6GG@kzxMv{Uq>B_iaF%UcK#WOz^o0 z2D+h&u-j`I6A$Q&NZ6&>@EYMTmfy()O1D~V!V&sY@h_F2QpfY(#5u-0KULojROj3& ztmSv&BnmZ~w!$3}tB(7fP@R5JcT!}K}pnsxoecC&>(H8iI zk;AM$F(5O^f#TX1qMh+syZ%Eq`D;r8cx9C)n^>GMx^>J}5Xm+wiPiFHYX>3hQar)| zZ2!a&QTY1joB(Hpx=|3uU$LolKiO9XyFqz{H7E)0SvJ2W7K#2y2vjf%W^`AXhHS!w z0w)v?!RMpaEFs9A*FBJZOq6qO+X++jHZP6RCCc<83r;gjc-Ayfk`faYrN(Pcpv~q< zZ*N+fM12@9cKo9{}mnOlbxv}L6n;1dNa*RR+q}+#e0Lz$aPzaO>VNK z{ux(%tpO*YfT`H$1UD$bZa2BZcB{mdAaA_h_cB30I?I-s?aJ+wN;iIu*O&Q;JB9$+ zw^mFw)EAV)f@1`idxYS^7C0~o(h^+z{jr031e9q1n}GXyIp89D(?_irvQtkc)fG)9 zK|*iI%$YopbKy(l=~`$0>3W-nGrMHN6?-u89T6!R>;kQeK}aDt2?rKW``Esvt%BEF zxiGFDs@rIH4dSU5>7!!kL_VPPC58t!;PXDB=IR<-&6N~Qt=Ub=_1vsdGB#t%$Dh?_ z6A?j_Vn5r_^}K8oT;KiM2Br1$Wmcj9>;gbJRva*R&bm+~V;rlw;C&E8F&8U5>&o zY69*fi(%DlwwO86_5*X3qv-f;3x|~Svg6gdA<=oCR1W_HRUjnx>OTEM*=d7~|74F! zO4k+c+;i3j)m6Se=xx~cnO5Oe!DF0&LbJzeZG>^!#pdK?bB$7mj#2W=omV z>q4akrRT*O1dQ8nxfT`!zoJNa!ZT4S*%EmCv?^ZlYcfH>pefMs)kXQUX6xLYJn;}2 zz~D2%-#!N{K`(`GTOsa5tA8%6#kj`G$q@nQoccfn8gJcxK0Xfxo^bG#1i zNU%yj;-8)38l19Z`_!tj94nZa+%0)ksa^cJfccB)YqZ7 z74z3>EQKm1Dl;+)qLxeDqZLh-UtAx(_$G4D%APAJj=lR-+Huy}X#^Q37WZFYM3xF_ zrh3uuSMRTQJd=HH#uuTq{`e<;Bi>^&H<$WGYj}y;Rc3e;d8F_ZIP<5&Tnm-P6&Zv- zbsDMJJ{2Z-^@x=bz3f!p;ctf%-r#t3(N*D4$XJal|D&jt(*#O(#&K#QBmlmHw*#KCY?sGr60+Q8e z@AP=yz_kNoct}ni&ZcpOi4v74mm0k2Er^YPpOaVa8{W@zB~zrt0@^HQfxB!H(Xd$E z-m5>U&H9BHxoAM+3BwZ0#p`WdQ+PpVK|PeeK%4~}RPJl!0pTeT=u`pn$hu*S2#e?p zJFh^!s8lBUoBfFx4&{I;l0*4I)~LzSLziRo)4aIL49S-SX_5p~q&Z=F^pR^USisgUUk9fMu4S}FiJ4FjnI#jUp_d&T1*A5_lul})1O zb_qAaMAB_i!Yqs`q{w_j!W5Q6MrRhxEQ|>rep>|qR^fu_kCOOB_Y{B!TR|fMgkm#L z3=ndYQjl;bhBHwN(Y}2BUoSu- zL%{Sh$e^~^OonIx*4U)i>Y=J%>G1p0Y_>itsqbSh%mBG%AE)emBWONsl%aVsV5TE%0(Mn`scFW@k0PATSZVf`Xt!;81^ zRr%N@28%lcpeFg4<+SE$Tc9r+ZDCzbWlbBBilV;CL}ekxBPVuULUr zABKc!$6Fb`z2!@bZ5}vF;Bs5Ih2RkP?zC!>LDw_I*rf4sx+vkYE+F_{Zp>fE48+`o z6^LcSrBP6#31N57J{I@;OFV}9$3d>!kfZqA!)2; zGU@ImhbUi2w@!I04*%;VJn3A_$|aL7px@wCV{VKdRlEcW5tah zH&IDn$Mfri=#PL189`y45XR3ov3LoCTl0Cn~EjHCJUy^S>$V7Siv9GoP10*4lD(<4Wl2^;v#0^A~eD@Zp^h> zy6U~>QRKyLKgugyrTwe#Ws%Oxt2IG9T(#>UY$zPIMv2Y87E?~ZiO)2KK&){HoUF7Q z$xsUJg=BlaFi~v&K2=l{CQU4{I_a5Dj@a6IXp0Fn!22)X*8|sg z9}#Dpud5HY;o4tmC`K;?Dei~v#Wko!rB(Jvu|~#vH?6MnBum>6ydNRehAGhS_h@Fl z+BF@wM$m?#v$gl5d9Iv_e(Mi;W`$$64c*Zli z%H|^XpNc*%Lk5`ro+S_a{vfK21J1@Y0lzr;sR)jsL}7wbW}O)x^4eFaeV6p_&)pLA zZCy*u-4Y9%gbuG-5ui+72bCXbgOE}HoimlcpaNbZ|ETB{ko%wqYTpD1m^WyG`W4r8 zKEHlX>X6}V7*l(7La}J&%cF6}7p$nU$0(Eld_EpNTWw?1R=Q0wd9$m0FGy=1?0#_I zyXPMY$UQ!7OtH>GA1)3EHCIItnaG^qH1Oh}>#|h-x;_pF~PiZ<|Z@$vt0TnkJtRaJwy9@R;T)(OooNaZlf=fK_v?PLII?&x^vIe@;!6%Ws zcm26Q5mRH^vc>i-m0w6aDzRQ}G)1Ngn+K+-^s|r)X_lJz=bJdh5N7RmWnbA8SlkJX zL~EC?hZ%+neEL2l+1ZBXR4WdmIONRFj9WlAgqw*nA+Md=ulb{F9s$z7LF4pV&}H3A zTi#RRPN+sh-r8br6E&%&8$K}L=|(X-s!Gg<-@QlQDYRNrDy}!5qKV7< z0-VI6I00%=tuERKzX79GRL4P+M$aqQe<~}VwO+Nf`K`8kPm2AjvX0eG=Cxht9PMlM z;micIuH_jgJIl7%CrhbH zOHTQ$0ZR(U~gDB4D+R-XE(>FPNtg^JV4PxYs~3oKM=eLN~RA33_r% zVbHjEH&=%BR>;mj9gFJnQVIQe6{yqbq6Jk}7BiH6|6nD0H@k9?^2Wt($A6L=91c^u zq~&|NEiIdAAy$M}CeY-r66ioctghNLA17OK%kl+Ff!0pFmDQQZEIKB}3PW73H?QXV zo~-9?`|Y*dUI}eB_;XM{H%^GoJT-68%KaV%0ZxPND=75E-NlQH+TERG6kZY@(NJ?T$WJFP&KkL+iWzJ0zyA93ym= zF)G35dR;GyzX)pnl=BrSTvI;p=Bf@6hgDZIg{4D51p50_X_{xpLgw~O_oY5^69T5H6RExuq*kT>8< z29v(u<7p7o6X`TD*$$tHc$XmWGPUVVhTNUZLs;sKXEaK%@QEy{+89FpfD?F}YM?SX ze{mYW86ztmc{)1G&ELznBeilyRO@ff5LY?jb7sVv$0j>JH#5km>Uy*0G`#5k{4qru z7Uc&)fOXzmlmFxOKW|r+SLT+X#WJbR#T{972(4OM-1N!Uj*An#w)8wI*aV&WG@s>P zFVkkHuy#Gm?)IU@1(z0F>t<89%6&Pcv`fjIm^13JUa<;eNn~9?l8ad|{D`E&B+QUv zcbi9582=~4c%C3{x?31ii_>!^f>%eFdCv<`HCzi{LOamTJ|}XK!}^ra@j}T3rWB$i z&m}o2P$~Le%xnpHoVtwuAqBh1BO}7o6xS{w4n$`03Q!5h>4yPDwcOtQFYU~{_J?+>YMuA9S4V)=bL#7G_<3N78-i9TXSInUAnJSw)avttqSm`;4^ zkp@R(JO-;O5H5~jyrym6pG3CoJEaBP&?RpfS7HlQ6lP^hKfp8OoUi~DeFi`-a@APi zanI}3{JUjrRvoE?oqm3QfJw;b zKA78Z!Q*$bWrh{l1irIalpyO5Gn#BHKBCeXr}>9S0+_FT05!r!+w^kAKn6~@ZQ`Vi z;en`8KgkY9lV@LG>8I9VZ8tc{L(XLKlv8-a@WoP6EhsYPeJGuLV^Ns+nK|xQeAN5g z`N%E-Fj6pOJU4+h|AxtduZ|5KOyig?62-|R0_d*`HMBJw&e_)=h<&44ds{u?h?%<= z(rGbXX@{49pQum3Vt$&gM9Dcmt~2;B4lq8_Culm+25#Vduo+2?9odcrGLMs0p3n1< z#op|s@br`K<8=65tiU-_Ca>L$c`y~c=8<7(=hyC^gh_K;?7#I6(K%mz8wbLE$~A2zcGYIR0UPD&(O za>g=6KEenjl%PvVIac3(Ze$8nI72R|j;S|LnO~1+v%~N_joZ&rot@`A3b_JG-`U;% z9{?gj-M&%wSBZg>wV4Ku+NcY5fg-YJ#E4*sNQeVpD(W<$t6I5o#V_e@-0*9)ZGtm5 z(R}>%FMrhp`}Nmfm2bcKu2TBg004N+5Jfo6HUzPF8E^oYTJ2OREzKXre96|)qzbcv z(dXRRS`;ldweE+I-oix-#c09W5ZYtX2hh;t#1t)5VlOY*N}9uw%B9 zIpI6zYC@1FPr#U+6?lPBqe=jUj9nC(1)K%T`m1odEIJk?l3?UGj4-I3ZOEG?!cXriW%~g* z^n14`8?ac*rx0Fa+aO17z!aH<>7s8?CUBFL9UPWae^g=VD4%7J)g?Vd7_k%BM>Zq( z+DR3*$*`8JI{%Al>nZ`5v9YZ{uwhY4Z`-zy<>SUi1;M^UoNlH;VRsKmRZE5J7kzrF zX3{xBlhQM2h$72x4#RHB$Fal5%7KFi%O79=WBGso`Cnq7SFc*7EVU?iAEZNO!nlOv*P-J-mV*9b2ggMG=&p+UAa zR}POy9n5jvH^w90R8-t}`kAL?E{iRMqy;yKsG0^;*D^!=g5NWCF-#rNPvI*AU5(o= znRue*iY*d%w$b=NGRl^zpHb?@Tta=*nTJ134PV{%@*aR42QDHaNwH*by- z15a#-aG$lP)1kkg)Jtvb2^e4kB_#3YTW^Y;I&|oe(yUG6b6*I7VUYlf89RJ-{VYOZ zZGn!8e!;PLI&={X6}!}?mL4D?#nSBPtAz~>x7Nq^L%b@ znhJ6qfFhS%QOfm(x4GxyKB&i^_V@oS}p`|oK+Ss zTqMBHF)hgMZbmyQT9d{r-7bHo%X;Mc1WqrUSM5K>AZ%4=&=QckR$8;z8X&cdkr{y& zJhdSe|9vWkYVh?zkaoqeV_io>m~2DH?ijFLHhYb4s$kL93bQOuPIvp%*2t2(|tPt)obN}wQI$K2~Y)Ii8;8L zFVANJ(mU_ITV8+tb%8%X$=!FaD4REL()ly@laR9izyXDweccY>Gr-NTb->Ue=tzW+ z<&JG{+=*rb^CwQ864QgeVVsFB)1Ir2 zLSch++s^drSX?9g!T-?oXr!!<@Gdm8I-P*sUQ^QcWXqW;LhR1_?VW@e9Wcgg0$&${ zNdpiO_Cuh4aV*SAV*-v)ro`x3riP-<^X5{sFSJvLtI3R|t*qG!M35)bbG@GG*7m^^ z?{fPDki4LJeS~4qxJ5WH3LQ`N7}TG%VK@U2|33(}+G7rWuKT^c$f?gGbapBCN%3(o z)@_zsWIBdKz@WRUNL=A*-Ar{mDsSu}xgoOx#cm~QCFg&_PF<#>*(>X;*c5c{w~a%W zvM+{9HTVea&Mc!Y{GALfo}Zm#YfI zSTUedW7SZ)Tidb;_3K58q#uHzLR`+c0AScoLmL(VMZFgRhM3nseE$Pg$@uh>&(x=1 zc=3gT3pqfFtUt`2KmUAJnU4j>{CT;#fK3kj69nlV+cEaCF5e7Fm&FD=N?HvM{l)df z{gy~S*N57xvQKnj*BJly!eVZ)t`L9)#dMh8?`6mRb;hZ$T3gT)J>{DK7w z6;@#ph1r6^v!)~%)Qk8UZ4E8P6psL)0FmPP}K)hu|OM0IODAoH(N$9v`HOO zN)}z$WvOR2a@YSd^q63%f=&4*W#~%Tg-u<^ri4-T&PtmHK4g(NIy_h^`j^`-LdP@3 zPPKr_wlbYxTOUwWp0i=8T%;J0J7Mp~AAhVo|J)1Ogre&+fZ_V*q?q48Ru&Z_v&!B* zd&<{ee^WmA;QjK>JMZY6)~#P(R^Gd^ESNXHoIQW8y!*~O3SGjy@fowmk!^Y=OBhA? zuCvPt%Li+zocSwYNa)XFWFX2A8*K_dN9eG!+6`-I7Exoel+bo&0t`$Q79Ds~5&9(b z_reP=#66katXJ1z{`4WH0o8SE%vn+Oqk>_Vxbg)4G^unSw4qv47tjYxzjRrOH5a7oKpfee)E5Nmcq`fdUW`|vN zStt6oV|~>6J5lvs4*6Xv)Yj^8I5z=VwRvRD@pIT;z;;cJpSSFk!?VLcle6l~__W|Q zGPC08U|$W{XNPxMeubb->U^%L$y`Lqlc>Ppnr;?#cTxfw$fkDFfU#x84Nh8is}~Aw zPO!6ui4=YrP1++%*JU^FE}UtjO}-O$k?UKqaK4yLwI57TJ~~l)7L`vws<4>}hN>Sl zP9T06`+$&4D5+y~PFZvR>auR#da+Ar4CV2aAxuJm$Tb{tRgKm;bCh{Y6$HfTfBMs( z6qT62V1ddUVLyOg@vS%CEbqVnfkMwDUBDaz;K+!@FU=JLI5Pt(vsUhNHkEC)R5cRW z0NxH{HbtS`SlJd%NHJr`T0%QJfrfr~$L)8N`&Qjo{^_^>R9=4N<*W>V?a%d}Wv2nv z^>5Q#@`x-Jbku$O_G|Y-OdWA7)$KJv=Am#U`8OFK1M z?Gv13+OaHvgY>9z2v8s|Ueo&oydo6iau!}t=(6-fGMen^M99yEqV}vZI0(=%t1Mo; zL>Z^_e-;;PA)MjV160y?MfO^^(3qX=3g9~F$!v@PivcE~K751BbXY&tF?y4=HT8C9 z=){X0?Ji#^65N86(KgyH z{FaJBUifmsxaxCleY)0%D#K;<90q`4cduf{D{z_4__Q|y?7Nq*P}Xada0-hFGfLqj z80QGMLVFm3^6~QJ?%hg8Ik5kL?j^_CvURHrWhtmkwjc8XIxuvD^dAyHg4K;8+Xla_ z&|W~E@0%N?jr#p9n=ctfo58g#RoyT+@`2Tg6AjYNgzvNG9cIH^GiJMM*WO=Ve)*O1 z5C8CMB?*kq9+vj_(`Wo?K=sqx@E36eu}Nc>1fo9q^ix$SCzek3pR`oeZOUES;9##B ziDohPWM)8RW-93W09Q;B7N-+N0a!69Q%U^6haN01z4Vf3TqcPqP4J7j71!k=bJJU9 z=Dc$1H~y*Y_l#ReHXBruuHg9TO6Z;>fIEIpl;rcV1Ms#}eZ*lN%Nb7Yok& zm|ZIS359GEifKbk!Azjbnxz81gw+DjrN>NKG=xw*Zn*SKc4bB&9~{T)PpTcSVFHuAMtaJ98Ei!q8+e0j8XB7Y^`on*jSP zBJ&r_OLuoq_~^F6)ReJRfu@ZP)dq2mR1DyJ0J!`2?=NT1o)H^nAm#r%@X&RUHgxB| zbDmn<(p~=c+n1?a4%fq2mUd+#Y9l&yI~W3VA821^G3ETwceR_8%$_|ki7pj1gVfuiqz|Zo?4t5TTJzlN*{G0Ljl%w0`JKWvZ_M`XK7y9 zZv1*hjB}&WZYDkVab33d0HDZ?Jj18HTh7j;*JyUjNM_cE!`mU$ z_hv^MpR}X!_uG2Hx+NIU!-ozj^o_ACjMnN^tIHjC-YE+aHXbn7d-v`M53#wk#S~Im z0A{)-Nn~ihm345kHpcasH25+ZXdoT!5@A8R$9MKSaou5wL4rXz?(-m7h7BB>94vR% ztzTE3c>MA56yo&Fo2P9PuFDECeUP8j0@hLyD<>>ODm`_eV5(a4JUVxMG|~_=_f@c% zAwGrt#u{;YjOr>zjDoSji~y>bHc3$@-1pnpep`O~+uv#+MY!lEUE(k5!zP(h(pL8D z*(X+t=O;TqkxvfR6$rIOp973CCE3p9iHe#~LI5he=+mG8DwiW+a&E(X1`#k$)?5TL zQq-r&o&!`x=G@stC`F=o!VFB*go0tfqFjy3$Rraolj8)~^P&-9r(ix@1e)t0gn>2* z_Qxe|MqJM>Q|-la&6p{60sS6ekA4@n3z4pE-HR6oYr7WAN4&m3@3k>0g7VHw3U8kX zp~e{L>hJ;b9PT4S%toj!;PIm6bauCG`#(Qho5*(

~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 39/60] 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 40/60] 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 41/60] 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 42/60] 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 43/60] 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 44/60] 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 45/60] 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 46/60] 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 47/60] 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 48/60] 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 zcmb@PcX(9A*T)Azs$c`9sVgE7q)A8uXm*)drAtRp5EN+?8==L1EiOdPqb2$OEf-glyT$X}Q?shT-2cqI`{X+@jR;zc4I2uUyLZvy{#&)Ul;T7uk977|WqPmXA!t?EXj= zY&YI=NJl3I_US7}?VVsbq@xpC&J2=&w3}=>q@xqD#eH(sqN$ccIy%w5)p+^PCnGG! z{)ARzESEWPzA0DyKFV^@36#3hSjy?y=UUDrP|9=UH92DWT+97$ z#M+^?F5Ne7HN}czx5-z{=jt0)tuH%mg_sua!8v5|4YBMSEz;isNOuwJ#}oh^zNT& zx#$E+&3*YZnfLr6%S9&|xmJgz+D}QB?x`~^XA&6hz`R3c^T2e=MQ;<4K&j~MF1LLp zH;lF%`*q#D<(eA)-Ha91KFsOpxr(|EHFVPN3Alg|(#nrA3yDPM}oow;IaVuPw4%bONQ$ zcWWw}S6Xbj=mbjD9MW8N=&;yw(Fv4lnWeRSX3}EIMJG@y^}P;q*o%uT7o9MqR&|y2 zjxMqs6XrXJWvfe*rRp}lC#$`c6rq;j6C_G}mPKv$Cr2Rg2@<`Y&!&Dn;*UV!6C@s4 zkzHL&PKiL^6C~b#IhT6(&5aQVe1gP*U>-_`gWxO?_yh^|UppF!`0?ap;joZ+v(u9g z#>&!xVIhGbg-g&PFf1gN-Y8(^m5;>Ph2L8CR~=9=Tn7IeF+1W?2&~_*?vy#%D9rA?B7d^z$fl@{H%sfm#+}O9^3*A6&}zTRJ{?(o>;s zLyN#C_W0oe;zmx-m72{h!j=-k9zVF0?Pnc1THSGP545rfd}5CuoJ6UL7u;X0YHJa; zlo0m#!KLtbA+`_N@9v(flSSB4LfGR6m%`tLSnJ*B_8;$N5w?^NcJJd-_au20bzf@{>6*Zv)9x*u1jQ<#V`WPTVfUggW&7`^JfKsKPwd{(Nkp2z!Y`rr%fFGE zcPzAAMlFd%pj3I^cA5X>MV5M z+hL4C_3B2>P>EU@?XEonpc!-M&v7&$d@DbAGNjvfyFWJxq^*`Zs9S0~CPLWZx|Hqb z+?SXa17^C_`C2GY58t&!TpxO?o(X~dt~%mtxr(5J~knQ-M_jN{w@Ty za6#R|*-}E-y|YW<@4`pDK~VPwc1gLEO(0DP_vMK%8Crilqj$DPFD`{+AM^{~nvoSk z4LW9w0PK;BOWCwJiW1^5tz_hXIAb-#CP;gDyyH^F?}A1EAw2@H%UlRMCN71)3vq&0 zO!D5>VTUV(J-%`&+s`Tghls1>$PZ)cj8=_XJpSDv_y_vS82#BRKQ4v-3>p1jyY;bG zlz4?kuVCS9eFhVa&@=Q%M<;Nl0at{g6WqV*SRtJ`p;s`Fj!v{+_*Hn#g9%MXC(_4k zlYK|`w;Z?HGnNgsTy%ol-GBUjEQfS-0>kCLq}8n-ZRr4q+{6V_)En2uwu4u$gv(_|8u4wSlb z{#ZD+(Fv5o7&76rT2J=inmPNiO~e&!yY3>vWl*9}AIsqz%sL_yJbKaZInvRIUNgRv z1;6WOIi#Z#kM=koZgG%~PKX>ogv$fz=tSbk)8V%ZX(qzCOMbimV85NC_;xX)TLveg zXAlg141@{e-G=p{=xqtEN3fJ$gNjaI{o!|I$ld9di(cFLEqZs$9LphrAz8@?rvnL; z;(Dz0MJJ~2*)3nqwZL-x-V9wd-*V9ju6<{xFR&ca(TObc_sbGx7FZ7H=mfvDIv0?R zPGD-0HVJ&fZHZ28bRAr;bX{un<(Y6B6}?^|!BTqN5ot44NbozM*SU~pBKrGb&rI2| z&Cn5f{t9QHoNpA)U)i%!6mEEB3~AO9dHxD#ppa&QzH!3ySN3d_OQ9dMRnZS7;dltu~ z&~JytMC5s&jCVrM``Gk-B0TS7&*Hcg`f)zUidBSpADp>Cnsrdh!}C7&S2ccNBh7@} ztJ&Xb=KO{8J&*P!9PO*vs=MLY4|~o6=Pxpw{jld5s63+1e$YD@VP*nnBar5lN1Oey zzkbb`5Oapbp5;I()W1B1Is+-dKe(+}(LIfrc;xb9^k3g)tGfKX4=3|L;My!6N)snm9Fjnl>>ju2ipmQXH zh?EYL!ZcZgPwPM_rw*;}K^;2f7%rznr%8xe9Ue;li`Julr*xncpFJqsXS{s=gW*=n zZKB5O@t*Su_kuggVOUPWJ~vV5UVLa>*}Lw&g3;4XjE7cRjN z)shgV1J|H17lg>NA|bS<@*Oujj0vVZ`bi`tI97uuPnJD~kCGT$vz8#i;a)E}QT|+b zq(nM;tS}}>XP#F&R+w)>l>WJ@r@;regX~Z{pNRP4=c#h>@iA7c^a&LqwtZGP^xuwu z-0V<0#wz+5EF_#dbPVZy-H0dRFH`CLOSJ?E4tL?@$#Tl+Q4(o09r%R(`mBtZ^5=6@ z_r&%1ImiwPKBw_~yBV@T^YNArO$)KN;)XautGgE{Ru~f}q4Uiwb7xJ|Z4}o^-C{72 zv9ilPY<<|TTO21L1hrQYYr77Y3H{DLr~{=?hfO3?nOEHTkNaOonPWL)j?mtu-(LxB z`0l2A5IKB;gn91nxqmA8pQ7A7;K*H;%G-FV%u;=f#GDplD(UD*;j=>>7%tL63@B4C z^v%4ZZg%(tiRfqhkYK5PSEkD@XT~{V6-@_*i?mf9;T0@CuQK|PG4eK@(8RMVc!G}! zA-E6Ze#%Mc^=w{0*CDaIJpynNf%6mO`eP#ueKfn`&z;0A=p}~^J|FW%l?ifjnG8bH zLZs(;({p(C_8>dfp`YM+6aHPFaO7ACpH2tHm!&Wr+HVo!9>wYxN34*>7@7n=L4y4{ zO%~z1KV}Fy3B8`pziU73*Ie8zRPKc>ZgxnZ6zkAuj51=S$0e9oNHZ}a_hQ-a?ksDk zba+JeK&*7!nXqHUTC@&)bFlXDnWirX%#usGPBe9RP>N4fRVX=27B4(engq6@sDu6b z#38o9Ld65|}7Rr3S84^#WIL}(zwcSbR zQz=MuPDLkLEm|TQO`B`!)3gvBDOS%cecp4&5i5)dpPa1q&?1?5b0+0(HX@!Sf*n3_ z68iio$I322Ob62ZyN;m{r_`734+@kHvBM{Lx`uzZ(}6lLTs~d1b^TJgap^oO<+hF& zyqpLX>FW4^Ai<<6(z7*soJ-o*BJX-yfV#((L$bsD2Mn$~-fggLJU; zM28M1p+gel6G}&8N&`ELJDw{t)3M;u?!k`PDtOrOY0@Jr7Rz08XG@HUdCC+COox-u zAqlas)SAG5?^g1hB!@ciEGnNet$uQ`th#?T)hkz-HGzvH%?=5S3DQ=z3bzbc9vDN+ z5tBeEK2f!&&oUYMV7@eo3*U})e~?%%#15a}87$0cAu=Ph4y2>U3UdKtXc8#Jr?#$M zT_(@|IA8v61eO8PLVVnziaU3XiXqEE{@__MGaXYpR&ig?UNQ7IQ$S$2NDJ{$rz-B7 zM6g3C_UkihCQ{6V#l9THgk_;-E2T8NUXin`lU`0S8CDby##n3YA{JxH1zYDX#ag!HE2Sv(ht z7quJ`C}p1EEjA*HCyfYpeDXHw?x%9)wV4v7%oDjcNyiD2-s~VyAJS$`#FTS+EGe*D z&dRw!VmU=8PzTaNWLD-{2bMp*gRkce9;_7`VuxYji9?g%cai`(;>=9gfx7lWl;6 zW^?3AwI^F+IBiG4Bv6X|+M0l=qw|4}Jr|Cq2iPIOr`PAbG*3=^cB(|$EOU$%rWQk@ zIkMjl#%-mMezAw?8wcXf=i1MB=g33xlO^gCq7%g`+p+XOIdb>}&(xc-@--jm>De=v zdlES$_~iYsCl<(4`=(nu^tpW@h7r+$q}ibqp4CTv^p$$Do@Z~3BW`wB+mXPKsNH>O zU2;MC68@IHaWGbVKEKeAc{1;ZQ>96qt8pNXq(ek`Fh?=3Fb{>e*>i1jVbZbJp#urj zC&a2BdWAOBZtP}uvYuYceZC!uMxJWvPH>^EkktaF3RI;C{D4-nxwGStC-Jv)Rk z%%8ZBcc5k5uWQq9ZW|` zQ|5$@3DQj1^X(Wb_Uo893CsE(pGpaFtaMCRN|youuKlJCBv6X|x?Rag$Ne$&13Dt< zGj(8mo$0V+^?JP5^IQBScg}=MZa#g~@U3BT% zxX5#_K=5Aoqpe2C=}QI~v=BcwzU;oc#pTJj)nyPEu6aJ{)P3Sn2Z2(2HY5AiA@b_y zU8#3|bnW#deh00&aEKL3@g91eCLs=PEs^{;5q$2>NC!w^n&>3qy)K??KaF-5Bgf|r zexEQ_p7RZrcq$3cGzsx^V6bQA^ht?+a!5GOH|R4>8N^zWX6Gat9Gog&+d4)fO>b&& zuxE3}GL<dXDdo8-)$A}*JPpVp z!G0};`h=);`$*zjB+U+=I0=19Oo+|1#|Dq6a-l8cuGG4xcy)oo_-^ zx!fz6(?RR>2EiwIbX#l2D*N^^?k}2^3w=orpCEzf3Wa#3%;jMHJuXioIgAzibxh1N zanIhnUP<=`Nbrtd-NT_iA?lEhnh|thxJXlP@NCIA-GAT{KF6f{K~qNo((xMUc#|9w zP965i;sF)Bp(WKWx!ECMoX(@2%Q~lpC|<8sxV>@`S_)$*#3-t}hp67N!y1S66U)!++*{{#OA}z#@ZDZU)D(}^fGRJa8nrxKZuO(blFc;acPemh5Z|xQ3dHSnM zL3T)RYIWO=bmoLEL8R&2(wDtF2QD-Tvcp{D6K=XaH;Lg?f}c?7Epn6~mrb~w;iRSz z(~s2%zvo=ey43i@nI3VVZZlRG6ZY#9z-Fw@T-j#TUGAeWEYyJ^Q47~#h^OD`#sPNt z#CfVw>&u+b{Swk<|A9LAbRH7gFT_s`hj`ll+bEG8hozq|t$Ug1I*>peNDFcOUYF1x z=SI8PVR_&?$oXcE;cgJ|JxSko5GaK?Ekuj`tHNVyeBz9iu46*v4-5_!nlUM{GC3qr z2hu_mAi_n`?C^<`utxy38V?CgDAp*E9roeu*HYY{<4M-&U#a>#6Teu^GIxFz=`SJY zSE~7auEWJ|C!7_rbolgl6h2`>pA*Kp6C+k3C!u47G!y2xGfL^NEt9};JC4=aA3m1~ zb`-*+@F0!CgGlh0`d;zH^7_-Wt#1)c8|m=ihzv(z7!tLKQOX?48EI1ohJ{i*_PtPR zp*->G3>w2#j%k(rBaIod^PE0>rJxk1iDI>*SEx>oCT?~ZF2;&!eJ(jVfl{cC_LtB% z&ZCsG*Uya(qIUM{Q@q}UdjS&Af^Lm2!ehsoi z9Z0a>KD+b_5w}TtO9VnoVGM=1{~hI~?J_9Z z6CDsPj~O&A#Gpl&-S?7RA$AxRTC*}!&hgSKiWnA;mgj6=CMRr~?}*i8(s7f*7k*O* z5~$DYKd{wjzi!pc{-b_yaHwkiNdb1)in3p~iQJ;vwd8pkEuB>5JnS$X?AK*rp4DGS z#AcE{;~-FqM@t=wd?s7un?rBZT_U!VG&_9aB(!Y+Azr2N)#uI1dD!u&NS73zw&&k< z42Afa#&DZSdI>qy;UsiOLi8XaK+^1xV84zD(vF%4^#^GzO_M+=_UpB6lUQ@VmT*nM zC+yd@)XZA)O`W5Ole1R}u|oo_q#-Rtp*M~u4khW?4gzC^G}Xj`sx{pGd^6G09 zuX{BlFdaw>QL&LX&&ykK#D13HE zICW^+tnHXWq_MuyNWb6ZU?Rnc9X`RBV69}!rWr&P2Z1_}He-eT2i6#@Z$d=taj=&` zT8P{9{c)N`G3@XO5}0pf&6WuIS_#rui$NGtP>(qs_KFGr>#OcF!nYZHHgv7h_b!#| zzA5~BnD9A?{UvsX?H1egm<_*e&?5jOYQCW4OYbGA-E{X7|GseM$td?OVXW+!xD@_Q zsi(2%AM_=RI{5UVmU8Md?_ILP6~d0YOWA%7_XpDP2=B{iJ4_Dio~(w`&i>e*ZRGm> ze#Lh$J(tip{5#9j+$hCJ-@Ali*?Hwsw*OATf(<80M{b9XwY$2?yvI`%-@SzT$aY2b zX~{IJ9in&Lh*kc&z2#q{H>#)U?j@v!=u&uAM56s){p5G&HY&b*327mose8$#m+a4SCRw=yL#d99zkTsb#ceZSf#mzJ6>#~j+I-WfN6bbKI&B$T6AZ6qBB z3Qw2A2PZ4_iYbLO-g0w*MemE!j83p`%Xnr*Zm?!dPKU*uV9} zCRJWp$e(J;5{aJ|k*+JwlF-I<}ldM{k zZ9UW{#D`^@q+}!F91$oLsxwbse?3{Vy zMuq5&@}SgD-bFIGQL-vhaENSKVCo zZB(B$=Q4*@)W`qQEe`v!RkPpoq<#hbJhROwUr-9$DE8|%Mu=(ohQ{kyISDOwYi%9* z${T+50^PSoXZwD6B9_}kswsvJlxkGExt!lQMe%)GLd5RKnNosE@M{NwQpb)c+3b3n znnCw%36b!zKc31vMCI;5sh65nmyHXhE52{bB&ZVlr7OPK3TYwS-xc#`CE_OsA;!Kh%YV30-J~13&aP=B*H2DY zyz7ES$UXMP-#>TfL8<3bK9GZQY*Kt~aQzQ0<oD#tHQ zS0C({??}g23tmV#L19$C@50?UsF&QcHBIqdPeL?45})uT5tWI+mO8fRFnO$On&Mq_ zW{=b1ha*Y+eo);qh)Js^%Te{yRb$#SiZrF%HOE?gbrRSEAkD3Z5L64TnLB)fM9WDF zq5pWk!5u%ne<3>4>A}=Cs@_|kUXrSKXR(>`OW#aQIZi}AB2a45jo$Kh!8BEXZqG7_ zPXhnNbAL|dZj>NOVQEqexA38O?m4+UK3v&bKK^Dv@vQ-vrYkRXm#;4is5j_Vos5+0 zp7Sh?Me2Lg74Kfe97S4)Ls4`b8#zF(DjZN}>5c=F$Tw?gY$-}dkkWxtsLv$cd$oMZ zJ4B4-GKYNsrv6lUz8$qV$LBe8a7uJKkZ3t{y7UiESC2QCFOe4F`5zi2OrgAblWQXA zDDlut*N}Bk_5)28p!R<5*ZnyXu~}!!XI7@GHD~8bq-k$r^SbeR^oqn4&pdgS zTF$h}3nbD)v?8`(}8T9{>6HKllWR+J(Oj z-)UpU3ZEeHWcmH-f%|bR9ry%^=j$Gbat9ASL1Jr@FTO<-Q3 z)bgJ?$|p7j6yJX)#P@B7#D7QWc+!y$lybJzW-YO6gI$`e`{9R=gl{4;34EfJ$&@V#3mflrXAyydugpBF3k@qh3M60LKdc<`1ni@+yH6t4e6_`b4m ztXPG>CrGeg-^XGS#ZI3JhlRwc;%6R=m8Ap2LIOiFbzoRXWb5>!nO8niz%KmOvVZ@Y zv*9xM--uh2FN9m1x~+eXa`zd=q+j~QaI1#2l@1?8f}IhowrBnd-+hL(N#GMC3ZMNY zO1q0skSKNUSM@;K&XE#-JaIYPPoNaiW_j#;_DZ-fLOLShV;xAKRQ~6$KG+X3AW9Va zE!?M~6w+p_x@P}9Jco#MM8d~9kU*)5`>sd1`wYvx@0D}m)&T36S%OF;lsX@7agdHq zVC_TNy!#BtCH9EJrEEW^7Hvo46MMwrB+z!WErlZvm%`D9X*(L9*dvYy2(TS(O9^3* zI9$s1b2`v=G(NFM98LmlN83_#gG+cU;!^m#X*=4M62cyFkOjLi&9Oq;(Y6%L;)cf} zE@l5-`gX)I14=#p z%T!rpR6s3zhesJ{r(RANL$RVS3L{o1l{j;{jGY@$SywR;w>W>w8x*TKj##18hs=Al<8e@JmrN1nMamm98+vC3?qLM=gNlv-M1xjgZCKrQ}-Q=V&5|5%O{&mm*VQR@9hpUa_bXq>o*OK|Pv$OKBoZdfhv zl?kYqZ!RQ)|IjA6UHfpn<5D=nF>R9L6MIzSB+w?gEk!E?;qi`3;qRtRa$5>VH7Qx` z>r#de!zQ_11~{^GDg52Mka}fL)1h``WP@a0H=sFQ>OV|Euj3#sMDGnR#?k3EYmN*Fo=?{6 zoVdm+#Ee@9682_|3l+|8>PY{5vfMv4N&Q?fV`Ws+G){kZe#*Dh4;FI7D)F1CvPSPD zwW>@%i8P&?=s73hS&G#ztFN`wQN8AL*{5BSdUkDJiL?+eEp3-Fm2@<9=xEenrfk(9 zNws;nuS8mi;i{LP=63JTW!O4$)9pQ#t0t*RP5MZrg{ZLL`}Nc_-`@vd6Yh%hWWIzX zb-rpJi8QU(?wX!Zj)+Z;bQ~+PKpv}@L^ZyTM4EPaFZteIm14EVk&X|JFOt1$B&j07 zJ`!oNtXgsej1%ox9Xq^C{@Er;<(}JDA}z$FL;iT0j|tJdj8RJ_&RHq|IM%MZ>Nu?AWB$1}Q460K2$v$$B~Z$rROm(kJ)(C_PVY=m4o>pihWzd;b+jYdE2Y z94Q|;d9!?`OOkr~r@j*PQBAyhGnwb9d9@F9#N^p3>(otBd(QWfNYe;8VQ)f7(h+j# zsN&lupLs1wHC@t2B2BH^x;_3ENyj%19ZP%ekd>+=sYmPfkw|B>B9!v*EqvI1*!w`R zT{$fuN#NB!S}F73`ZBIN@7wvA?B6Jjs!}Ld~$_6v@ux?D?XHn)!xJLn}{guh!slJ-0-=4ypCTD?8@uTHTDnm z)4E;g4C#oSwklj3P@hSxC|p0~Ln3-Ph-2NC$g@csRUrQai8R?l8al(8<=pKcp4;-d z{N~#fB?pa>XB(}MzqZ(@POTe9vAXFk7M>BtSk)@LOm3dIQB@u^UZOrBa<>07JTHm_ zhKn>=x?TRPwNHjm17aOz|DW_mWnWKk6x4jGC2#ch>$Po^O1!d8*1M3RdY%~LAeuaJ zD*h0a2e0tXEw@?TnU$}Bg)+w z6e~l@w?&@mkgPg?J;Xue4xF~y5}ghm_x!uRln0Vi)qsLCB*u_hxXxqa^?EI)17m2$ z>h`6jNw+B-Y&H7|#Dqt$Z~nJYO?6Ln#A@lYKUu9P?L_jl|9iW9m3E;tIyqP#Z?r{D z@@`VS>Q9nJNMiF-yAo3G*Ij&ax7%iUy1*v&VgKgvi)&l_81_vV|_jn5_-X`=lk z=a+4WL|oauLmuq9Ne%jIl0-VA^@+Um40oI1?zW@bzfR^+hBH>jKHDAMi-Y>erfmH& zalEsQ+7nOgQwQYHcFC&P`XQ2u>%BIqd-*3@{&n|v^3Vt%Yk>s4dk2Xb{SV1+mn5t1 z2gvsQj|XMWVw+U6U2Ma&&m+Y)=+!=q$xjdMl4p8sQc2k+OEXp<&1;pSV}%693Tay5 zJ(ewA_fZ%t_OEWRN&P&0l947MzI(2ALL>s?jsE^lYl<^gIwqK= zaIa?VDZ==o4%Xed%0~5b32t|35Kg_a_2=W^@9%YZOKI`_aoLMVLx%T&wNRw@<38Rx~|B7|q8DrJ6?qPYULz4RX>%J0c zivK@vRn#Sj#L^Yt%i=kb)s(bB5@`bRdaZi__;e-`Yv-Smv$Ffuj26Qr(ws-(yduAy z4))`i!Ja9u>AM^oDOODAZyeOeI_!G2^lh4TqIU4*ITPii^Q&a%)l}Pmn=ES{TrP{W z*`${3nnWeIul`YMZxrt@F^EfF$brwMsu$9x8Z_4BBlf5Zl?V3uIYZ->l6B(%LBr#`PS$1t=eg7VW(LR9W9TJuy%#f znAd|+X9p~oXWFDI|K52HqU-vv6KISWqOqbutX;fPp1z!>)?J$;kv7X5=f!YVE%DVa z$>MFgiu+|Al}CqT4U$ij4&MEVH3Mmr_~z!GO58j1&ShgX0rPN1NWxorN6O=rS4W5_ zJY~B))jCyuv3Z(Y`sFtH!<=-reC2%0f30zu-=u!MH-*lL+60C~`>DrPt3*3R?(fF6Tj!R)oeuZTW=nmh z+!sdMuG{lQ%TCElD>tgM=f_K=$x_I7UW*_-R*$2J*xldDHlJ@&UtgFkk)}IF3LUk2 zH79{-LRyFkORL1|U18YEV9Jr^yp7(g**%$^BTPhF8O60t9LX|)E2H*0C(QzfcWEKb zMC6rGT-!w2(gC;X+UuMyg??NyrC3E=8O60tq*({9jN0p&xVDKj6Sy*JuV>=gCelpM zDnR&FU3)zf*EW%60#`=u^(eZPJ>tqJ6OmW2a19D+Ch%5Wd)*1wppa$)SFr5$D3?ON z9TF3Gt1e2}Yg0%wfw$_~>rS`^mDviGy&mOK=(j^+9eArQO7R=4SFr5g?-St_EPFl5 zrO}J!f_wv!D1|*|bONR9T^*UF9Hs2OFEawA zuuqI0E0n^XGdh7%cHfs-tWe7C`!XY(Qo84SAXYwpQTUCD)(=J}_=ye|>F5OZuh@$| zKv1Ok4^xQkx!F>iZ>=4c+3woCsPnBw!j{5bG&2JGUAw(<5-5ee=mUh$N;st)rLa$Y zfM9?q>{GGjG<(i)URi|QhMR=l@7_-bN%NoChhuLLJyzJO*-~b&X2#0yi_BQrAvt4( z1ompSK4*DgndA7u9vNgt*ivSH9!`gqSEvKW5BA6ab@1MZeZ!I!-*}7XUClicK=_%AYmFGHsHnYxNon5^N)5&Z8}# z(K;|(K9~H@&Q!(s*J4OQl&2eS8_;dHm%IOL=x`EYeO5wro9L{Gx{cyCM!)|I*k?1a z^76ZA@eQ<0Ybn!y38y_$c_58BZR)^SIdy2=LcAR~7N=JlFkH5aa$r@e z;@fCV9g1$eeP{kr_dUAt772{IX;o!35$}@p?+yZ`P~U@Pj-`fWZkJ&B*xBwIN6L7r z9W7&>Sf9C@cDe+5ShiR64nuSA;L1v~-MCpq55s0ozfy@S8bOjZk@>rQRS;+^r< z*>JXsjEJ&MBbNRD?diVtb+|F5prPD!0wQ^)#yZQZYubRP$S zQsy4W#^dJ3>2vg`gYCQI9OqZFHuo{~330O4spOmV*78mcj5`u$I-dLaQFpc~u^taO z)PXVKeU>L$(C(xfe6z!k>rVL}qdxE`=|BRd*lLWYt6w!f#5UqDla9-kV?Fgq2TD10 z=$)W6`_!pM;=X;AJTHG+$slmYr@5DNO_N|e?KHHk+@KD&Ez{_Izq&ZAw=`q5ZS`@# zUc19|pcLjC*(CpWd-8XbySy5XI(Uz0zb46wt>dFUs#PDL+yzWbs!Dy-N<`jxjN8@`xXPydCJWF4j|&9O_`d4%ghpT9a0I zY4jSRRYil~{fUK#`qfK4`#4fQ;?z%x?w?~rmB?XCkih+rR(nNrW7eM!-$C|kn@;T4 zEm7u#ZauGFowb}&bjPT`k##0vG9Iv)M1 zTOc>-c$*y7EB5RCX{e8OG@kt_F(=i+W{#R@)IOk%dL`JB%1bvksiWD(SXQ#MB`(v_ z6n@t_iS~C=)z%j$7<5K8=Y6$o7sp9x8)R%dL+j2U$g)^y2T@3%6#uUES%gpPa7v*L z?KgEG!Fy(OxTcQx^JH;%n-=StLCzq+c9*&1H~;pB<5yE#@(QIKM@2Y#VaqhF>ZPfS z`_B))vpJb}X7YYflwvD#gR7(|zW>}La7>Ll*spCm8Dn@#dU*E3#}+95ej=dq(QW2v zje`BQ#jMPT-OYlE?|viY;cn)`QL{ zTDO@O*qbf9KM@I(;@@=)X{RjRxBWWZwcVKOjs>o8_IV(2#WCrCP{gFqcf z(_T)}QJAFnk{*;odom{B3Q)e%z2v9oRWb;)CxbMN8A`4RVB@fBk$iE-kt9ahbL$gj%~M{*cnh6x0yGISrf=lI<}GyB%C^IyHNvI7j+M(@M9f1 z&{7@hqwkO9Mcpe%x;Z)2j#8#YxzcnuIo~usgB%hlh4$!#I7Y;)B)yazTSI%UdqDAR z<|t)aNjpoong2r4TO0)HLz?U%|J%r_i8KRZyyuv2SQqHK_V*+m@quP#AY!Rr) z&7k7@&rKr7muAx$=&wvI{slj>A9s#_QqN?{!nqF8WD;wd7^5`j|e*Vc?o zVq*2Y!DV#I_ziMMpq(P5$x3tb%w+z?;Z*>PJKKP`xIL)&#&iscZnnPnPP{&Yg(Zjt zhD5tKC{}YRMi19EV}%69P>A=4m`X%0B2bF`+8UlotZ3HUGh*V`!E)qq?1LjD90v)} znTQP}opaLHL6l;@wuWdDwds!aMRdRV6ECfC@DZ>$qMyvGD}VE$l88TX%mLU*6nBF9!1 zyIe?Dd`mi7Tts_~LVQHGviCod9@s|?!$K*Hp%6WZ_?o0IMIh!>O;>!oJkr$H(yi>D zQH(Z|<0o1QV`vgf=vMX!T!hMmQXG6@>>gKfO*AsIah5?Q+zkQ zd4oL?D21`5kqq6K{wCd)o;2TI1H(0Qm)|KpdyY?#U~8Dy+6Gj{ zt?XmyR`yeLBl|*fD1|XGiTYILIcgqp&mf0oj(s1NKiQxg9}~BQ%KQZ?b0jc^Y_GHK zt3j1gN;&gN+cu^B!K7n6gIgbvrV#rje0 zX~DnWDeHOIL7)`dVr@=#Ysc>5GrakTxJ|?}MBo!Ap>4mC?Sx<22FvuQ;Au<_3AFKQ zT5rw&L$(C{9fj#Ynt#{1K&P<3-kkh0wLZyIOHhjKMtzqSR3E=E-^yv5NZMI7h2D)2 z^|uDWR$+r=!?Hw+*%E2mm(jAn`}Zm3LpR8wc9cTPRzi$z-QPWxq_a;o36$cH>=I<+ zPa>9)!*JOjd*4bH($uRxA>G%vy&U?C91>{%3TYv3{n|FzjLISxIZQ2jobpLs!p8mB&Sa#2$Hn1f&{XJ+B-Qp@H-aB3?RN{MD z1%Mny+ihm7u2im=_zMx0BM@2#(q#AV{Ov#-tzJpT3Kp(fu~n5BBa+p<@!jbC*zjGo z_`=j)?WOk0Sn~oYT$>W2-MQO=f)sv7)&WvZ9op^-)vHeDf^AzA^Q<9<1lo5oEw`-r z;9M|?q*po!lw!**m-747nnypPbWHwsQM^8#i8|PdN3Rop_17!?rJ0V{Cl~nngd3g8 zHDd+gA}vJK`pd0zschK@wX>~{uezkD%lkf-Ch-rYBaPB9lG6dSaf3jMCrFcZn17DA*OK&Yau{FsYr7~&)6Cs{ z!l?skljxjQ-P0-Id~iQGlyc^k)<+|KBHkkDyyQ>{EvaA(g_v4mpy!>}9toBshXmU) ziHX~&4&@vsk*5Cq@ht8tv?4Q_9NR(}esq&cm^j+7y<*x(>HE}0ch%9)dxnxj0;NnF zDbM7~;vPfczwKE0!POoNNr=+NN{6jLaf_qZwonSc!s*W9FUI=I&|90vqYPMsLW0j) zKaoF8^%=p|7S2^&8DENs5Z7HuIiG|N(7Dv-yyK)v^jurZ)BNAl?o@JYb*AmLjVkTt z2+QisxR1$xMQPd<2G(e3%m4)2kkL|TPsSt`Qtrl5&JG}l1m-BxbUKrW$|OC|L13DY z=JBrmt%f=<-_(cDrgjBWqiLsUi0iAF3HSzEI;NH}j(9>x#Aq*D@(;h%(gtk~< zS}8d3d>~jdp_Hd5In;qtY~vvBzEst^<9I4_*U4I;>nl&YSCGTjhyA+6F>O=joVGNc z&-?HhcYLE*s`p*G0j%)=iEkIp6lJ*(zn@ysr>W;ObO3=N3Gq6`syW3d%^54Ol7;%{ zW*~ac-==rGC^^)QHjB*n{NaWJL!Y0{<-R};!@|^>c8anPQS?kM_ZcSv){L0Y?Mg;1 zA?y30&nd4kR)#)<*jRC(=jMz&bmMIvHxl@EVVcNxLhq-NXH)Lhr`$z?Z9rV3TYD}y zrW@Jd-OPy9Vj`+L2#h<@w6Bk1_2kSv?uO(rEPPYVSg{s8`+)>osmSNwq~3pIw9!gr z#7YzR4kCf?ib8^H7R-t2EE!%cFTr*ag5!#lX zY3Hugfy35LKsv{3=wQoML&x}4>!b`@dRiZ?$|Rl*TTjAtVBGol8Pqf!x~ zaWQuieI)%dIeg+Iw6#C7<281(e-ovHE#qOVkY+n&r@Q#o>-%{}W3gL}!?S8gpcLi; zzvtokD<9@A`?Xaz_G{~lnG?DUkS5#RPtFg@4rM|e$YH*ttvLQ&Tbd)Q5&q-BhqDw9 z#gOB$bht>H#QQ`DlCBkj(BYb~`saSE8aZNx`GYYOqCtWA!Mp9tglZ9iQta1u?o6WQ z7YV_*0u@6e$RUB&<&dUx$V9wK(tRTkx}1@wy&oS1g2^wH^881Rt;77~N>z_+q*DoC zch0mZx2{JZ_*$7#o)YAcKqPQ-YA|*z)vF@i0>QUQdN(MJ0Y?*7#Dn_ zLOIXZ@BA?y&)>s$0=Pjq?6P#)T)R7!ML4y5yUnki^-jFAFsBx@kI5~_J`*loAYi}I_hr@Y= z1Y7m%H_ERHEF~MiMqV{b$`eeb@TWR-V7P1_Zv2>Jm6XJDOJA?*6@P)sgYDd*4wOQD zw7c!AJi#dx{$6q@<rTJ6fvLG<~11zUN+$>zU9&a+pH2jEFR?%8-tt6n?V^ zI&>{Di7eyV1WE=gdCmoB_8eL=v`ERZoi;O7^_$2o+@^o?yOW-d^<*nZb29LR{o2YV z>v-cs`mVjpy#W&V#7St|mNb%S`k(vug`%Fsi?pW*!eYOcGVMV2TXoO^U8bfg1Mgwy+aPA*sm>Mn#4nQ8+ax@eK2^L91?6_vhTtamAB;(%bumCg*bCQ zyFC_#TJ0osJ*L)&bWEV|50OJD_M??eTgTpo4MQ6b?RVcHhjo|zy1ud!&v2Qu}QUF#MZyZ{NBJbpyEOI zF>KMb+CoEp54;a?^h>UZKC}S z&Z3mpqEvquA>}&mrjD4413eFC&*FBI!<1wGU}@64C=m~l^t0r!Jdietog;EsEj4|8 zd9c*jCfxYln^cQdqpf6s>3+nzV9HFr6GeLA!IN zMY#j?My1l*^b0wxea<&Z+f1ePs8WMMA6@)AaWy$4*srB9eI|iVoP@TMn^D{KoDAwf z8grfId|nP9ulmItg7iLiDHH9ZNZTo*e4Ha52}( zM$wvE@tdgxOHc`7kAr;)jv2|e;fjlXZm-xfH%hT3$GeNulq-FbG>L3W)8mTKm|>ff zfWCxnH(pwrrjG955%RpR2gK{sfcU238)Xt%3dP0inuzZprWR?MXBc`RF^zWUc^$j- z@OuDv?2)zU!50$Ol5{I_D8+tlz11Y1yLZ&RqE_)xYjQ}ST~?&Y_SJt!-IYmtC^`Jj zK)b9+QwhS%Hh!fzXSTKVUb20)rFiI{vAI17<8phDV850!t?70oqAy8TbrN8)mx-d) z($s=VY?-^{TdhOgudZ{u$l((V*R-aKUw2p&onLp_qPY<9JEyvDbS@iOK@Pu{&;ly| zZre%SLb+Rpa<-wPJTRxt-0k#cb?@!wCn~4rhpgy|uKG#6chv)MK*OS9=(N6CFPW;NeN1ZfvLA_w()ebRtR0$ z?T5$G)Qu^$Ll53&A!72KNTz#hLdChgf^d;Ab@1p%k9|;ztys^Pkgj~4ra5%v%~LY2 z3dQORiWO={0_!y0s5xX-d@99?*6EGfjs)A;es5ert$mN*^Cr~eti7_%eV3dw9r{)h zjI9v0{vH$>{?O^f3*_(#`!#|3Oah-c3Hx@HrlSUg_^t>p{#v>4#ty1DfVm2ukV>OYa;4E!l^^kWKWh-?x9q_ z8zJR7wMbKs^H(|R<`WuS8Z+BCtI9Lp&2|UXsCT&a$#t-ypS}gHJ3vtiM*}8-D<(L5 z&VFq(ll}UR86gJjo$9VmBaE(&Q3lSQ;)p|tCA+7(+mZBFXipt!egkdWJD6G=@37ytN$xAuEt$XQ$)c5&yE+%0^v87jsjC?G&Y9`Z;WBLy zVQUM{+|~MwoEGnlZWAa+GhU}1GhRFb)FVBd{Sw04vrXVCNxw=Iet)18&Smj9v2gIM zYZXH)$>BEu`?bY)v;{yHFSlDmx;h9PD1OW zdoxML(p!t%iHGyFEB(|`j?Ctf>QDYTp%K0yMl|I_#R>e7khN&2LNKq)g;Wj-#Q z_yET!iD`hR+bGqC!xbNt#lK@ zIx^A$@pYy{w-T%Z+AAc;uccUr*7qQxpP)tK2Vxb@D`)=b9F4AnHGb8-fS3Qzf2;m_ z-n;qu+G_0Y(Y<8&_i#FV`~-<=d&+x%tUcFiIc@suDc6$pXbFjyi>t|QzZCcG9M{(( zN@cC$J^n^M#WMOYLl^3>W5W0U*s-M(1XN4tOpew5+D~jLr#|7MScUH&w8O=eyOf;{ zjuVeXe;_Jx$xu1B`-|R@_lC%tzrQQjKUczgXUIqyo7^z`dqx`W>j?3`Ff2Q-T*~$@ zIkhZ~Zt@7x&SFEymKt5;nI$E>ofnTW^wCc8&%X=ry2n_>?EXkLy;H*bbi45qX(76e zJDhxubny2%5(E46mEM?=-cfreNTli9_QjSdky5_p%plogP)YBf?Iug4sgKIGu~PVp z1@bDkxKHlbQqsG6(Nu{v?J2rlA%SMptXpD?Smhlm-^o$R`*wiNngC7hNbcO>J3x_W z|K>#5_>mXA`FHo1?0vR4sT8ZGlvlU9Op#?r zzUV!%+9%g`m?fWizNEM1xCydk;mPvwv=_bEpJyHZ1<8IcK|W=A;;m`2aNifb-d9Wl zLn2F4dss#Ly>1fe(7b^SL5AN$0^7VoWfGR2~d=_Az zgLtmpY`OBC7rm{{40fbr-lkaVc1QkJL#aJu=gLEGzv%sMy_4ws&gztS(m`kGJt&pC z~u!36#QoqqBVj zo~%5Rhz3+kPzrT4O8-=DDO}P!vN`XkhTg!(|FYXFI*kbJpxrX~m+c7O8ODT`!d8v_ zx>ch4IsfUHjBO$kS}J$h+Op5-7rig^;hRa*9+?||oYF!2Kde?%6DZYw<(qOv*Am_p zzmFoKRhNw^{i!^l^CZ$j464&H zo?D;Wlnx~F=WHXdUn}9gnQOefRl1?9C}O<-4d*YfaeF`W)BY8!H$bV_xQ=qxD!Uoj?b4D6nu zcLHLpFeaELvW2vwMJ2Age9JM$DzRg8dGg5^Z~f;NIMQ)>-iA1O|E&8`F(zv_^^^(U zmh_gsF;&L-JIPo6D&?*58mIhd&)@xI7dJ#T#n6GNMSV0U^ICFvCkLi{VD;9rQ{fnI zsObVnUS%J@*ROY3Ac6UVG~FDLubp*g*iniVrlb9V4zhQi7;ll&^BuXn|h|V!Rb@ zE|kk3PnX>fl=NPFm~S$>xnQ85cEg8gN0rgGC$<~+ zk%p01?YDj;cjhYP{p!GUx$mD3rF(8E@B2;WP|D+5J+{$B#CDDqNG<)amu&J{DR2H| zGabb8byriUmCmqW2K5!g#j+8?J!Mh+>5)ze3{=-}j}wAJsE+)SyMFbGJ4urC71;gezJ5%W|*Bcz;~IP$F#-`(GNJ zaykdysrQf>E2K?gS$s^wAksm1#d$E__f4E4_gpIF-F1APnI^IAYAOGZM3CiEgIL<- zGx_-Xl3u=fame^jWr+eY-o-C*>vL**xA*`Nyn_yPw5OZ+lKw8`eXPNJiTWsazo?Zk zfQT**;$=BQ4$Ttd?K6=qO#v;$%qF)|#!;EK;4+8$lc;9Ni}cp6IXhn>O(zbEy%kSq z?yM0q61N7=mAj~|O{=^>B27CpZ{}|4(O=8XP#_UFveTvb!roV7Q&O`p~~Nq zjzW%faNn-mE2M=8mMfP+El-HnY>jkajv_6@j!v!N_By*i$JU_zlyv!zEamk~B}=&C zp(^Df5-7Fw(amyb));T!h1^m*M;Z2L!q&%9=Vb-2?+}-{lfWl>G{90<0~NeIN-vH; z;1eVs^Sta`*I;P`0-qppZ|lq62Hq7B2z-J>oB9>K?}}9s2z-LXrBfBX4L(^Dfxst7 zywW4q>pQ$Q0)bDExbtu&@8k>XA`tiliGvF(c`F3JB4YciRT9=vy<)45obOr+M@a0y z+9<|*ywImaOv(G7pH?zLw4(6%2eM~`F9u-LW1E`JWGWz{%uwYQQ;hx4t)C%VS4)T_tiyqo>W5eR&O#H0P< zy+0oDM^4kZ9k!GNr>oZ~_HBL4y4mw-P~K zS;VE~Rl;E*QL}c{2V-Rs7#0#3Qn&;y0>eV$hbvXhyz-GayYPF?{%R9n375hDMl=!b zaEmjfL*o6mXAt8`*6>d5x7sQ-q)h^!Akn^*CrZ1E zPms9k_jn&@+c{FAk*pT(Cr}D$vplNps21*vkd8?BSO*d)^?ptHU_Z!!=(1IY`&5)d z+Kkm>uc+|sDbf)MAL~E@r9NEf^&TkC`^YjY^Zeyr4YvkZ$IKGM`g&u}tKk+0>F5O3 zKBURI>c|S7FFRHVaU5}6Vvjgn%J#Dq-HPa0e_UDwKCwp}PNJUg8PECywJgGxqI?UF zMO@1EvyR$yf9*$g8(IWDu}2(E;-Q;4JsWB^w+LHG2z$igQnsIUtZRA4T{76pBJhbl z;&2kXDqe7pSk=}dY$+k^5r<3R@8#CWr= z;!(z}rPC9t5wXJ2*P_&|D>G#7i8SZ%2NOrc_LTF~pFiZ#fl?a>&yqh-e_khs`}4(5 z|C11;F+(ZGm;t3e?=?p@m>c80+JuRQJ(s6!BOS#YI#BAlt8-TyS`P^xI1&*b_U zG2Va6vyLZP{#;Sp<;3zpskQBv$?fA~yi0!J+^xIppr35wguZm@0I9B1R?0#j#CTub z!)5-;#m6hRr`+{9au=m=?u6qZMDm zy^gwzQZv5UBi~*?-%(weXm>0*eig;)RY$B)>g2C61_gtN{f8|@wIn$v!%J$P70F20lk9y9K?m6wzl1U(K-QynI z9?0ki?U-P=E@kKt3ksAEUavaHO1WJ#aD?nq_`C4o{m%A?g;rc)OpSi4WeDn)!6s-f zK0NlJTPk7XWVJp)-TK&4v zRY;Go>~SL9-45eY^oQqnt=zQN>uy+ga`mo-e zfNN}YTXws6Scx+Uy{?8dt?spbCXP^%qXf~DFE1THy z%OqJX%ZuI%1vA#R^*T58Ha+T8_CG|HRdbj`%`8*p$A1*}eqN@ZM4Ik{^oLUJl8#@q znnc4<)8zC^#l6q2?JJQMBKyPb{MyoE25Rym>C3HcWfOHa& zUIbJ~qLRI!C?KK~DI$U(paNDD8|8n`hS~ehCE@w=Jfp+TdFPxdciK5~#%%p{T`T#$ zK_;W|ZpMzRoX;Uu6&Ipn(fQ_|U)Hre9S50=hKCJ*op)sIgSClPSErs{WH#Sl*NUh* z$YeD7=hQLYRzPG0$6cp()9zkk&W2Rg0)tFO!*|=&QE{*dx1lxBc~n?ocKETbRcgUt zlhM%j^_ZQr7Ug)*CDpv6tIUT=q6J?aYMvsrVbu9KBV>EdwR>vvI&(zha4Ve|*8gWqdEs%$PH>mw^d@t$8DTv&eS&$4&=}pC zwhLy)tdvv%pXNBD7{A+*7c=Rb7eM^%{=E2&i-M_O3nT&>& zcmJ~4>V%$4&iS^w4=1SWBCM2Sa;vfAl;(7v=(HeHouV@3j|n$yaUd02G_j2jj}kvu zp5awq9a($NTKx^_%guX7hg(m*mI0|21>Vh3UeXxBRMV4Inu)W+t+91S1F`PAq~Hr+ zLGSovA;45gzppl*{RY+-{X|D!qW^BsG_+u6)NaCoHKA5xembJp{&}7yK(urrPHlM8 zY*Qi9+FEg{$!Lt7dmhdG5PdfsZO%NRimox6Wkpzt!zTrA@O*wY!aBZ5Zeh>teJSTl zAS$>J9Xl^Kn_iEwyu+uM%nx3~AeAqpp{3~hVI1tDf*uz|=aJPY z!s=6Eg2`lcKfWBs#KRa9nTq?Bj(DU~r8vApXCFj4Gz9TWdf@}} z_NI~6o;GsNOa5;3D_i1@jThT>aUXkNVP>54_HV)C$ktsUc`u$hV)l7H!rHiaqIsmv z$L7{g!>#s5$AHJ5@uhNipd58jj+{sLnZ|SB*7tQsnbY?l3Q@^AaZ3AeYjK&8Kve&! zOipjqRR(G|B7Kj!4E8q_VSl6XAl_KtygkS}6;gGERE*$!87*bStA*ep8by|usg^hW zC{(6=1hk1Zw2FRCVMM|tj3rv_Dg;xNo^v!rr6c~T{lEAQ#pCQ*nANzhj&wf|s%fq_ z*hZ~P&qjL({eEz1nTmN!IUKJ;JYGqhR~b%Us9dB;Zc9kD#5LFMkKSW`f4HvoL@NA7 z(fDW>U-u2qhRv%D+gEKA6$#a>#%NgGFRT`S7~^jF;QqrI6LTK_!W?y|uC==>W;H_N zY^Qsf&|5-Wj*PeWhem2f!!ORfyqpD)>M@s8X>WXKUb<4(s`BL!lhKB8+ryQDwjb`9 zvurY2@W!0MXq=B-jSP;vu=CPNZj|^s)PjuG+5`V^N-lAy4fuO3G4$-a_-b!xmvlsE zWl8*?SKIzq_T512gH#*luQiV*Mp{kg!+TDhwdVAxk=B`?XP8OlR+``UiL{pQmb2Gq z%P;a?M0@-PMEY$T%(6uzt@AUdXb8>;>%@`o<-kL;y$7|cA)?+gEANc7>U=O$L!<95 zy%L9-4c=E{M9oqg&FvNHS?N{YFc%QoF#5gq{uX7e&Ilgu7!A*=r78yz(7J15B9DVQ z;@38dJfgL1hLSTC>$Y{o)1?}FVgDHPAix$02fHpeyM7#LCA~G%ys~wf`P8*~R@5t^ z+pcxsdbX^oVHxIUs>}OUn6tl(wC;+WU9H||9_&}oD)HM~lUt2j6TRf_z1gpzC#Il1IHgtk1~d0!JuCKy zg(l~Ox#Mup_!lv&4acm;2tMCqbbdLMMGDUVGM7A8ww^WW>TFY=4en2@6?(y5=BOI0 z>sim>-Cag-POxbFqiV>im=R1Rvw`9VpXB?i#J`ELy*b8qM!de~Q}b9kob{B#`>&X5 zKZISzm?hG8*R?8{dl)4zhmtcwM(4J*qpZ`j7n_VWj2>@Cd%s5Q-j3R3M3F^bm|j@y zMAly%9NQH-zpmhiHE7Gp2$|JXEigJCfw???T|#xmrDe2XEO`0#oYO%34^nZ8%(bet zI*(UJ9`?c(&W24~aBNp&ZNm5yq4gY(^M>)yM@78%0;2O*LuYZ{SIp{1@uD*(jx`NPp>QBJ|?Em-HpS}UHT z1j0HmM}viPv5@@@qXkj0&Wp8Hj1~mzyc`WySR%(+7lxD00(D-jwPLjJV4atvsbZ}a zqXoe_FGo|wS}R5i0_UPWbuQ{?s#t5qXhGl{&X;|(zb$b`ZxEKqaVICCDp=FNx&lTE zf;A0}et>laj1~lI8XUbrSR%)roCGnwVng4R8|R`-<>(L?EeO^$IQjwB72HbG;OGs) z5;^YVBs?DT?0&G>#eCfaQ_1(JG!4%0HxMCBgQGVh zNYmh)74x|(p=bIfx?d^a zyq(Y387(C*>`dP|)78)Sb-jUecF$DIPk8W|zH<_faWHg3&ut@!!p`)aQ+Y-U0^eZh zOy4<)=TrGxo#{KL@_a(iXyH-V3B7a1uAkNiEg%X!p?A*M_0#&$8ZJ0qIj84gi5z#( zA{C#|^Qk=ZF%_=>3P&)NGl$GOxP8gPHLjBu!UHE=5guOFOo z*NtE*UNhW;$Osl3C1)z0+iyY$04ROdcc}CgK&Y;Q2xl(hIfHY$Q4UbcAL>1%U*`Fh z(NyvbLGX;sQaN*?8{y2UoG<6(mWmMt&!WU5SPpJ;Ui&z!np+_pDt%oNs@>XE_EcNY72>M1eS#F!Rjq|`NtwTxb+hB>gwc4@0(CVC_4LAv zR|A}v#MKRHy{_Im+suDg-@gJy`~0OL+`0k5SMZ{bf)J{vfUzzDfz{ABfdR;5l;f;P;Vg z+&xi^QPL~bSSbkSz5$Ou5?3RR8v&o;s4LM{yLqU51);wAn|Ux5^KcM_wK=yl*9cym z`sD$;*q49$1|W#nk&5p}T18q+(tMWa-bkJvjr+1FgXo<(FGh1s>pU32QZX8OgZgdl zT?-HUA{QOjq+&^Qk0)W%BjVJ%;hdLHnPuu(FQ0tP)TL?<9{+(yw2KGl%V>C}@XYhQ zb*R4oAc9z%)m;ofC3(p~zbMLfOt%U<7B|ng5!64s5KJXjVULXsw=!!Ez(^h2E;0ue z-a#!H^I$6Rcyg^$gtfBG5Fj3^Tq@@SAnwCA$OxvA-|vf$u#)@9n@Fn~&+~0KT;G2j zQgOa+sg$J~^afXY`VJ3hE}#sGmR(o3yWz!>uR&?iaLyQ>E1r zw-@Q$bfYdU}dq1)`@5!Bk>F_ermCE2^cu1~uz+Szkp^w@2Uk^xjy1jn88>9&F1eR(h3S4O8t7 zk?PepIUydLuXu48^CN6##=K(cJW6iwpVu-w)-H{}DU4u0GlucY@ee)9LoAnr(cCt8 zG338bwqRzqVOOM;gJoxa@V)b5%Y-8+c_d2CRPK^1I}qq4F8q~uZDWkTEnd}Sgv1q< zZlBod@VKDm8P*LNLaf_*{~d1KH*k=tOT{aF{%R$zyeCOqjVp$6^v`bt(Ti%^Z4tPS zN?dtTVxL99`2#0*!M_YQ4>cMvKf|c{`?rC}#kK9l;2|lBiuvh0+Whiu;87rw5g5T# zI*&y?7YBZOsJ?x-Szjv$du!q{;pF_e#eq0bcR}FZAaUi>iP7*hRVO|$tb7xDDFP$d z_Y$LVa{m0{z^{-h$t4w|bsoP(#s@see;xw&SBWbRP0Y_Qep}Z+?>2lJeO%uMe-n&0 zjPSbgfehq7)m0AWr}J2MpnqN|h@qRdttCEQxr``fmX2S^gAnBq>0A4@3hXIK_=n9=fnAx_Dr-@@2tN zFuA-_B2*m|r_4ptb!m#sNp@l0|4f6-R%@`b^gc6g+K z#~9@QmWu}?m>=vcld=O9BBT6W5txU>RcUpvTn|2;9e4uN!w|%GSFzXO;p^&Lld0IJ z7hVRsf#~Jp;YO&O3}f`e8+`*%hIcDQ+blc#e`;hM&cky{`X4@ToODt^sUwd4mz1sSUKzn{z-X*2pKt1$Qlg2yvv?Dm^I|_NdO0ffYwCLk)c*sD z5u7ihu|mFqC;|jiNnH7N(Gd~bCM6tO7;8r_iq#P8xrNb&u{C>ALIkM)1&Voy=a$}0 zqOG|l7n_WRFTHiuePQ1<@h?Q+`e7e*;vcRBJkY%Hl=R(>QS0+Iq3_m1-(>{%SNUD} zkiy+?@Ti6S=el?>74tKU@7GlK`9VDmfw?o4?zOJ!*dqSo;nn?*ATWZd*ykPYFaTkJ zdQRQye(|5y2c!P#U*~AP?R39tUB?yie*)@JE(G(_d7OB4Y0mTL?PJk*xvsc9xSinV zqv72-a$YjQg=E9z1p^Rb%t&yUvB5bPz7(XcIvdDhO0Uhn%3f!9|O zS6+d2kG+K%yHw&wkVzzFuy$P(g?IS>;;{dEBdl`o?WV_c_nf1`n=eEkrZJ9~B1 z5zicN>%Z3Wkgr>-Lq7hZ7$Nbp_eELX?46Br+=_>yVl@2LMsJGy9JMP}#VnP0)_enA zHp`^U40@+jXv6qu=#jW(K!_hmZUe4eu0z!Br3xNsGHhrxG#-p#emE)mI?e9irh{)0 z0`ri#^0Uoo!`KYOQ=pc>DQ<-FxsAQVfsuC2ZIcruFhb&rN|$OB5I&(s;1oAP`7_P` zYSp@gTZ+-*Kl5=|Z2h!T_8&9qq}vS#+!5%9z$uK7xWnTxdWnr*a^D*U1as@ z{rWcVjhR!cUFIh>qH4h~YHr`QMf}j?#*-$M_*?p@QnWRq&Z1!3IEW8+HOpxPM2-u= zk}w*5x9d>fp4S`NRc1D{nLAUle<#D3+hwTl91wRDfKa}oBqyf@{WVQr%*T!GF$kP5 zdx6qG!hw^$0op5^Pz*}Wv z{8tbdA#p_|erp%3in8L~krreGr??TyPcBB$@tyLF9kKrQ2#jEVxVop?#y}J;0Krsp zqUY@BpZ@;Ggvar|Vc6mP^F)=xzH#MF-0$#zmH#SYpOKyNe)}la-xqv1FE&m zj|u(2;|u~LB(A8KA71DBwTkCcj90jCsnssGAom6ACYmM`h~Ro-v|+S+IS_iAgQ>)i zSA4^0tM)I8OdWA=In(zk`qXa-oFZ}MgG}#NOaJ&MVZhoLdoBVa*t-~`aenplKM89= zJ;H@xsTd8LqSOB*Oab+A1g2ttWIE!u$cA3*h=MOKa>==EI48sSbmXL*HW(Qe%g8|e zNBohg{ke{)@J8F5c0kN^A-FFw8s0BDHc2>k6sHDXLEA@dz+Te0On4jcz9tESL7fd0 zBRF418^)0OO%jFy@eUA7C2_Uq)DdNGD-tL>JH~E>mlqkK`Bft{&N5E@mA3?%zBADH zv9^yjeBxWA&aiOn+LZnn83tZ!oP8SY0gH7F!BnjI!yI+`ue`UwyxcsL_Y;gh z?-dQb+s6p;WpP({g!T6WLrtB>wiiwZdUvhuU(%(v#zW95U)|$L>laT4hJt#X3&B+4 z@ucmC;nw=HsVMpE-->LpQF2((={$&vCB(^HyOr6p>iYrxit`e$8cq5~SQqw=Fm=Sg zeNP8cA<<_d73J%eN_la?9hT#*?Rl$D`0iSB!p8`SD=P8BGRuQ^g*u1@M767je2=8o zwPz!6EwXPB)_-A-^UZe&<)I;awg4@e((vkt9seHkl})Q_&vNnL(lS58X!`ddUlb6J zAuyH1m6sD8@zMt^{A0am0@(cHc7yj1V6ww>OHkKKOH- z$!N4knIgWgpdqV(zX)nDcvi7?;8n33k30` zk~t{SdMbKi(D#bczUhdFKbK~Ihu&Tc-v^(5u!M3FavFZ<2aS`rYwEZCHu!#&ym`-5 z;{71!pGd2w^_r<8-rdmJKlQ&8zB&J%@Uc|v>xF&GV3&&8{qv36eRrXDxki~vuiZz$ zV+(kkLtq5+V>I5>tN6XI*rZzi_O80(`e6z6T`K3xCFdG3jC%vWdhm9XJriRIOC_Ec zzMUCqJ%7imK~FlWonT*bsjgS-cdr7$Jj7GPnuB;btHR`l&o_GO`Tz9P z^Rp!EjY7Os^r%tK>d|KkTJZLN-wqV{Aj*zJUX&mJ5-!N*ly~{HTb=3hqkyDr-f1B_d z^?16SaiWwj4}qy9t|Za@MwJ5Mc~A%32=Xo^2sN(c_mVY`$|v6k$<9=|R7s0l#ftsz zA(Wg`#3#poyd`v@!+4W>Pd<;bK*$?Bj9?y&hW+k<9(Lo+n*yB>IE8(HaIb_%p#Qz; zQFeXY6Pb#8jQ)MFyl#G~=R-f(x2}D3TWR|*1kQ`4(t9E!SQ0lveJhv^ZX01w?Rh+} zX0PLUTt5<5KF>r#$Gg}ZAg%+^$c11kE>q#P%LtYb{$xCs|4Gkz-*N=;M|J%DaBI@M z6jS_B{dhgxO8Rk-%dgs;y%(}qpbRn2Z{1pBEQn>Zg8nL-sJ@{Z8G%_|cGa3)367LtPOonmSlW%*z z0RrBQGz3#|nP3-w?PdS<)35p-`0iDoc>YS>9$}r%7;Ul_Fs?UPi{y>;&z(Iz;RpgF z#2d}=-H}%F9g{RP{ICF#1?m6-Q?dUV<_8-Vv|wkn-Zp7PYBlyK#x<>%gAtrBqYY!$ zE8VszpB!8cM(cIe_G+TN@Qw3HVlD9>oOKu}*2G$R~;WMw~2v)A0yF;Jp< z#{lOgaYZE_R+|11VMX07Uh5daDQ<-Fe+K`%tIB(nmo%;9lrOgvY_?~Qjgx*=TKX0B z5|)s8!?reQlzno4Qr?3HlJYo3;z|ytc7fxzo0+6q9t9QEo|&ziDinrlUN5v8?#Pmd6Bd_<^?y`6RznC{`GM3u zm^Dng;LFCya|t65^Kg$o3N5RpLkW+7#~=hoaK0=dTJQ3qgji6IMc@=SLRrlk#gn2L(^TWz=%b~pIfS3pbr??SLd$fOUc(y7z zBg7++ngR536#rzBuhCNt?E0M=*qp)$_Tz{BNA!h2$S zHU3LVcoczC1g&OE=BLlKj9?y&#*B0AW&0bfMXO;g%IzU>)f2hibcB}TLMVT>uy1cy zA?OcTZh$hvEtS(A74t^grQVpH&O+VQL^h+3dFT?nRPez0yEeJX+% zI$;<7QC+*&Yo-04O)l+c1XGF6%mdWbLGc@d&)q<8`8}`W(N5AEBeYM1Lxud5fIY~la! z+?haw@6QC7ioHm3O&i9o5Dl{GSqJ;$<#+O*37ewCX4xyT_7Q)UT)RT05X>8Q6icQ1 z?>bW^&;&ueQy#$!%~j6KG}%+7&Vv!0FQawDy#I#z(^lT;n}xts;sJBMFUpF3@ePfi zVRSz5V?q)%mQ$cnWQ`JQ5#@K~2h=dim-)&UeW{NBmEY@V2&U5ga;_@HXVl3kYi)nrP|!-= z?yomJ;?)svBl}rP#`Pwz56!(d+N%DXc-dT4{Y|g3HDD^vS4TW>-3Tm%7VZz1CXT6C zD`yzBuNi^&L48^f#9iX*9V8u5vD@%C_{Fu~!r0D|aLG9*!^r#NyFhB?+V)%oPT@C^ zm>;CN^j+XtP&d6>M=%dYLnpkVY8>>a!58fLtL4#-zcj-redn<)%G&~`Fhab=&RkZ{ z`r~!krQVtF^On)z(Hkwu2&Uq16QgM79`=Tdn|xOhIA4jYv6j)|Ep+}Cc%Zf6iPq04 z;(2yCda9$ z1q5?vskq1B44_yGJ7>_@K=r|A18yqynz@eXIB}TW>0HIYd<5psetkKo!g(-4cqo3b zx%=(Az-9DyA9_1;XDYq7Cqb%VkZ7SxDy}Q$2itSZ=a(>-w{p$ra?Z5pKs_t>_L;7C z+}i_jXS?Zn4G@@0;>wRKuLf{G3a6Y^zI!#`L12WO^(im3jK&Ce;FHZe(c9(zAg?m` zbck~@3|VC~!U|)mYn8z(3r6GnXj3ex4gVf}m-AvOIbr)HKFT`w;2ZePS6{4_{THPA z#f4xhDN_>O)cfsjv0G}fGY#+7bo5O_;CxvsK`Xz-g(H}X`GwA212{vzsdkklT+b#()L6r%X{ot_$`TyYN zLHRNocN&&n@MUfZv(F-MY5BD@z2s*OJ)MoY+}7ZV9j)&N@zvTT@x?|W@a~6qMY5+-UohU0SvWb6 zme|0*5P{1fan%O8FYip8Cswi=_$Rv%OeLouHQ$Z0y4*P*-^Xa2u>20{DnN0H8{v5W zUbu6TFZHPg{`m-U%Awksy>ZL$NvI^4?Z^h5@ z6jX9Kzf|7`=Y;*o_anEc^Abr>{fFD1(fH1*9?pBIM6A8sB^7&CWeKr{D}6ZcP9WMK zFn7+E(Xgz!{cv6pP`5{5Dv7K1&=C*6o#rogprh|u1UZdTGmFI448UKoVK9Qfb4Kfk zoqLLfym&A|zHK$1Ga9!!`rqSGvl>&$xsRFwbVO^MKwf?Oysz;)7^&$Sbbk~3OIG)=GIxr(_~#&+f|=ZR`OXS89o+@9!Bvl=6~9E`?$cS)8VQ}TRZ0RmG= zT&+uV#P%T<0=4tP?7j$`B5^gVv1eVK2P51(6xuN6*XZJ@tmY4@T}E?%!)vgRDi#u@ zBX9~!!o3n2ULfK@-N%Js9*j1O_j7l~hE`;BPRTun`y2M>h0HiS%P>0sJn`b`R|BQ7 zlHn9aXzgTN#F^#yK-2+(Q``vEnsP4c_zIJMy%zb5CI6Sdv727DwjJH@ki=U@ri6aS z%WpSUeUhROF+InaEtd?i_PmDI1nVS*;zASRks<$52#Hr4J1q1Ty2Ec%$L6e<&5GAlJVrx)5BXFnQ(sg4{sn+^l=Zq*x(CbjHvTyTl2mZsn%^j z;hqHD;892Fil}Pl z^fi+&4YZxa?MpHH2iQyYjKKyW#dlKPr$KT5S~oLJ7!30`aXF4c;nw0q3?syxWN?|VvY-8 zzJJn#_71)i$5gq))6Bb*Qmx({=bJH=lFj5fsaC_m@>*K_FST;_pyc;JDn>9BwLwCS-^tgd--o4oU5t7B#7h0|0&r!XR>`WSO@@l@*#+*idJ z*?(!9)$5MjGnk6ehS8z#zRg(W2k+N%3v#RNZ9gkC+QD;+xz6)35Vhnxr&gVm5z#4_Yx&J5|yiSql_e}-8Z2V@j=&$U_#&9$Grl5QnO%c~@AgqA{x^Ow`D zP3@N#fY4G1ab);#tG%_V0ECu8hzCoIu%0p27J$%F2=U z1(ym96LL&^gDdb2x)F*hZ)QvLu^MSsrEEcrELSf$`rw5}4Z&2s!yYkzub*c5?^^=I zl>2IX)LhFvnCkMeBy-loX;zD01u^^GEN?%2QN!em!k=1^G1b0bpEN7CO|zbkm44N{ z-C@seAn@j$hG43gB2Sw`I;L5DUl2rU=Vm#NpyZ2O4dPr_!u@Mhar@_P=wE z08!3`V5(MQpE3J)PP1xG7sUQg6TFKdRcn`2Ott#EKIQ|Drdead1u^i#$Q@5W#v=l=8F*n2d z_E2^K2rY#WtA5C^e%+E&076S4#BWVUThBb1TL3~!A;gl`Mxz`qgj9*4r4T~;n|duR zwA&6Mo@+S9$%_!>Mvu8!svttkix65)c98VOf3&;^@$`W)dR=7zAR+X9NW4s)v7t8j z{~;=O9TysL)|VZ3V=TGRuQDWi1dYdEE60UKHAV;PN~k4hh=m~kU$E%;Z;*wARe zeXMZA_uI#YMjS>Lj^N(MXuLH#p^-o4g{Hypgx4j`iX$x1i9>$l;kZbD*RRYVf>WFo zhZ`|v)V=;z`&$PQ4wYdzD-OJ_MC%g6DE{JI{+}OtJc!^FXT{-0JoZOPe?;p}L4-qP z7|x0VZ(P#41eV1;uK0T8bq^vq#aVH<5fdAq^Br2-Gl+1g48vJ*geCIt!FLoxZ-6>f z@P`&!i{Pa{TFK~dfI3u$;mkf^iO%n@kFNdRZKd(*+x1w#w2hk2kBlE?mbjW~onDt| z@_HV&oWC^mW?|-R>6$rt?n_<$iur!EG;73sS(ltN-}bfyBFlwfD&_|dH1lsOaJ9rc z2#Kq8kYW5ipkYo|tN>nftpJ#+$Gq2RwKV3~Jvr@xnBzh)RZNdrv|4(5N|n$(FRm-5 zN`G)Jt(Jsp*@7j$q>8D*Hh0>9XZ^55p6B%!RyoC)+uaDfAn9AYb7+uYh;iIr#N%F8-ce>6DGIKe=pUc;<-I6k$;C) zG2VG$)x82x@1XIlZ5Ut0?B4pojW=02#hJsy68U%BY|NaKH{pg<4wYdzb35J^rZq#b zpvFW;63!Q| znlTS~>o=g@`gLlQS4&}u{JW9C@1;7kS6Cv?eH_={OLY+Nbs3rmQCBpJ2EW>X`f432 z!*J%outfgd$auebZeYxfx0juk3QKeljK&9*vELJE7eAUm4?63FutZ*EaNNkid)uKG zR-H9NSfYd0*DKhyVB8&qQBJ;K2LWHOp_N5gBL9vR(x`iFHP9<79LK%uw)OdEb*RrC=Mb`yQF3u(MLIPMoYf=fBEYyM;{fI$Z@63GK`nc-9~x? zUC*Vo0HR4zdOX+&@1OoDCcV>sVO#k}r>=A9N5gF`Gj9G8$j(gAEIo3W$(Y zgvKl5%@%po%}LhRE&pq)Sz_w|YkSL~rtni}oawKMeMh~=!idMl1kChz23SQl4lx;x zmBib3=Qb)14`#(=f9{lH{EThpwiN@cuHOwd8I3ctoa=;netX>LiQvInOV(6!j}a=RwOmjlE2OEM{^HzFjq0_#hjoxt??>KU9@0T_PA(lJ zAxgipI;4YSv`Ys`h?qy#hIEjOcIhAqapd~?kPed3E*&HxqBd^`=^z=6bDwu!@u<^U zt`qJ{5?|jf*(&+=Xsr5Pv*vhepqJq6$L~gjs2bX4w_gTYZ@`-0FmjDG?}oINB$Yu_ zETObtaP$f4ycRUrW^Q^Q*@{}4VUDi&q50eW$yVc-4D)LLZRXN{2U>5>6CN#oZjvij zda%6rGu6e6kIbcYlC6H*N14m3erT>Jm0}$!Cp)ihjf#1HMNfoe{@(w5Vz#f8Y;`(5 zQlnxtY`pr-%`Oifhk@WaVF|fRI1!#+K3-`|m`anKY5{M!AHKK1aYc5!mN2FJoXw&E`FmJaq-ig=7NY6YvkY4O-ARpN5NWs*5vc| z*gfsDEy4pHVl*B{-u%>@TrS!AXZlE!`QdKgU$I`iJY*jPqVcx9=80;_R!rO|^Q*r< zGkexfvGyIFZZg_1;+|h0D=&aUC}1S;Go_anteYb zn2O7UHA7l<$V(biF%QYrXr6&ca;V(KXke zKe@~NdGY|u?-^wB42N^-3#s0*sH>i?d9c;4PtAKL4X~oC4l)@HAB|&bde;FV@5-=L zOZtCqj+i~bs`CY2WFs`*&g{~H4gH-a)rLjim{(JhtvOxBn2gqH7yodilDJc&Jg&4I zKPhA(xA3pEsNWWx~e9%OC$2Y#T!HVQ+C+s3kHB&{UtPW|ab;MAz zHDFyM*MeShwl!d>qK3Q~vNg~VnPh9g2$qD=@cTjfbC$i;?$sgvIiq!LI8!b7YDGwA zt|RhD9GV#ODqtsyukyvTNX zNAB0?S9hRaG2){gyUg@;Db|FVI@&O@&!okd0gtc1uhVIw!>%l&05oemT_LK8>pE*IIk1wt+XR*3WyD z1w^HoBW9l$Qmh}Zy^i}a4uW&SEz=4of)-mc56bAHMx0YvQ!6_ey_ekI@$#0lC`VBl zO9-J+Q6GcH#mU+iq%`h1aKSi(i2zp!;t4T(1y~Pk3dxMqlZISsL`)3Wo zwaaK}!O&cbxIP;=S_h|m`E6dXZi2N7Jbws=brX)JAuN&Otg{H7mlU9zVC@2+d=_uN?fsPBW%MfnU9vWOacVe+PyGL=oCo?lBkmbPF1Lk;?bsK+COx0}JYSOV>nt8YPan#itE~%L6V)=C;osP7ht}AkM z6Jd!QcUrdD#~)|sp&U`Ja&W#p+U4^oSpUHq4CW`8JyTxwwRnG!EpbO{!5R!k3l-}> z9BoEeBFCMa1c6g$pE`ABDo3-yXhE?4!_iu>2IE%x4@a93mdJ4@C*e`B{v&@>Q~D1F zEeO_sINFS`M2^dsrfen((|@oAgVDkRB@gL8oRfGym1ndd3Ofrg>|ES1SpVT@Em(tb zEB%L~X$VW?xRaAeg)y-J-303+7%hmx^dHXox2_A(bu5magQ=LG@L>IiqqSgd2G_J8 z3e$f$8Vp7Y0$)@>eNm3qf;AYo(tkJ_4Ay2aT6h$u&2V%Rx@H3>IfZC59DRhY+0eBa zj;0|jk>d_pq+)FbYcM$Ow5A{m)@HBTN@j9q*9!$mSpu!PM<;<41l8UK#t}UDgQ#m#Px8lK6&fIq^1XJ;xSh!S7#WQE& z2&Qu8zFSGfRLk}RIvV~0c!Us=m&%vXh4bKic^1710iooN zx$_L5kL~(*?&PJ9)Xu!?9vK+nQ1QHbD}*z@>LZ*k6;tu-%sSy9A|qIEl$@z}7QG1} z0HE~w)uGbo+K^O1gfr^1?3~k$l7m|QSnJI*8KbGLG6cbMA4}!TK5m3FdvU&;lUpiA z@I2`7BObwWaGUdr!&yJv3gJ-cD}Ye#-o%4f9L`#TdB`2UDvgt^ypjEbcWRYBoA3D{ zTW40wa3h-MCR>Al!^yUG8&ELsV#gx4ofsi^Q5m7)@Za9U>uqv8%DzA>2UPh)rh=tnDwZ$^$WT0(%FRRZgV!OHw%La zOfFN9hk-X`Y#U|alCva&P-w##`mo2J;k_1Egup3sYwz*wfmW@)V_iJHyKv3}O`i>o zpP%z}BOD(8UiH|;8(j5$e*aY;BP6bR3EwitEeJ?80TM~zeEBY+Uh*R6i{z-&LFOU( zcIh+F8uU+w$vHuranR#`_NA)<30ewn1}DB31m`R9Nf!rNXRD3J$#%@q`5t)-;!76~jf(i;1QNCT1Zs9F0!zYoJGnItD$rz(XJ^|$0T}g~bABn3G$Bi(I!g{;PR}kt8hPR$K z)`-LK!gFATv{T!%QP{^bF?>6d@{B*>lop9Vt9-eQt=qhG(P|Mch; z9-MaBIQ7!ngV9{mIuGW~QZX8#H?v$G*~Z>le`Hv!!qju07cgyP3& zk;<7l86g&1j8O4>gqk@SA@NW-U~8nL%}1#3gAroGq^OvmzGe`F;?7I%qSTG+^*-Od@i}wZT8B)}0w|1U+7; z8ytKOR(gBkh`9^XtfX1_{xTFAr>N=Y0{>fH%P+yrL-{Px{bkH31i@6|FXPL|G%Mjx zx#Pa;;(^WT4lnayD*0VWh}Y5*H~L0binb+i3g5_;-<@)#m>YeYKt0lhkQ=!l<)vA< z%NLrA#=3;c!IHSkp}d9|#%&$41JRLDehG|_xT4ZMY*cwXJ1`2=Pa}vYjpMD-tRYwD znoOm8*vPzrh;#99BUDbX#rJRY<)GBxAaH5Vh{iX{=B922^x zS-TgAU(P#$7z%0$oZ?0(uOaZiTX%ilpU=elB`|`0eXtJ@SXmtFlT#n_`3IQKxpuis z+!t`F-DrZhArP=>&=6cEMjOWPmHRy7fOsAVrjlFOJrAc@c3=3Kq#lEJU%!~_YnRZ# zeg%P37{PbAZ;ep*!6hf>Hu;nBo8BD*sd~Dk;(B9#hVew$Jg+(nXDYer-TG{rb?RHZ zI!$GQEzW?ZzRks(*vAn#g>RGVWZshabFT&FBP9pvzmP&eYfJe>OSkcCjOxaoWkB= z#B)nc+@pWvDM8eUSf6(S`4@i{uTE25ZXU`TjbW?+j~2+kzl#TZone0Pr@y+oZycz* zAuxBQ(*4PF8C%3(BfPr*O5N%jf~j=>B^$;U@&6Ch^$^6%iHQ+;^{=`o7507tsm2xY z{{ZU8T?m#?=kfP{N!if+1TD$ACvv@U%fkBM)ulOj56OmgrdGS+LBM}L&6-Hr~usU)tvALxiznwJS2 zYaL@tUYU@Bf~;RYt8>JN#m0;E#wHkJ^+;oj=+uld86Kv4u< ztBIeY6!;Ljug#3$8cun((h*E0apl=a=TW!ehrzc@(TYJ|NJl7~FMz;#F%?T_82x})0%{3v#I-4@*0dTk zO-93l4pOy+L=q%LQL%(N;+f-Z{SUV~2_ei9RUeUC2{4!N=Nw6=1-x$C%D?2 zXBlpPjBrJIc(0=z;#J<=OH|L7JMKrP!Y@v^_)mPL?xjHEwl!@DoWeKZ`G&k<42dqE zoeds%1H{iIXW2O?*a-J|#J>07*8vHf;zp>O{)RCbb(M#D%0yr)cU>uqR>SatM;qi{ z6@d{FS5#akyk)xMVebW$icH26FciWu87T9_(sf6A3`5U`!MlcUX8^#Xow$A|} zfvMPLTSpwHy&9K;>y2}Q2O8R|F_qYCtG$|z;Jq3n(>9 zpPsYEMB+*>5@Zpy?v|LFO698~YC`Ya3p(e;2z=fyaphrLzeg{57p6B*k_Zo_r!b7B zq^V{el5a?#&FAn&MM(8JB$D7pD6OT2HcD=G^v(w5#Z+uVXo zR1#OZbX}J!{tJ}8mJzyNgR!Keck@vBa`|)~%-zjH@rEZG(g`zyCFFX;o|Cltj9@BV zs%X*)bH3t}LFt4UEq$GI^z^Ms59cq+>FumVW*%Ilx(^7}A~S-iSVH)+h@9waHNSyB z4naI29DOv|+SDvr^FX5e7FixK(boplPrDFICB8*o_as|w5BJAD>M9T(PsW(g8fHJ?4wvJMvE7Q(Vrz-&EtlEN5Zi-zES-f`&S`|pOV!P$(DVjU(m~m zl0^4-aw~+AME7`7Z{NW!p|fz}A-*M2HYZy>Hs^a)QT(t@OgI%fv*dEH?DG4FpOY=) ziD4*5hnL3WD6gknTIRuM!`OGvm4ug;$M|O=aLHMA-49ERnpYB1K|REUU@E!ee($}> zRxy7+NL4@VNH$wS+!MjUY^%L^Gj`iQXx!ml>kZ5R^%Dgk zR68@;Fdk3I3zVr}&;P@H^@4Zx8)T$dcjUr$jBfhtUR7!j$O~)-qAG$=DFjoAPnJOo zQmpoqC!=3I_flTqG^k75U(e4eZiM53=4|`d0?CaU_$7$HmG{@DSpBQNrg>}8{jj6~ z(FfGi5ts*ihvA$IV?cv?fi=^c_$6=(=PQ0#Udn-|n1SN`q9)lI@La?*fcPR%C&jw0 z0aJ;;1Z8WWBYxbq%7a@kb`Ke;sTNs6xl^AWooWp%EuLHU@BU`X2q5|b!Bi}}j%fKp zPfsr(ptxQhJNgBE)^u5#Y<)jQMxW6y*H3r?quYze^%0IoHAds~+^U~& z2?zU>=9_I_BclHv~>$1be^1+9&(= zz`PDIehDm<#FZqvZ;==B4#pisT~$O~F+#k~^nW+msKMFdpb|-8cLD>i%p5PT_oY&qj0BT=VrVeXsqgtFE|baJ}LC zSb5DiABe38OeJyU(@94Zz1rTt^6veCFA*3a9+Q6Z4YbO19ToJ@q|k=(?WBtyya^EW z563;xjZi(`FiL^Pa^(L30#iv``Hf-;VHckHxP8aLFMM?nn1{qw57+%=Sbv<|@(`q2 zC1WCeokI0_v4K|gwdp4F29NXU{(HVK0?`PZB7URp+c40&962_KP!i(SJrLDEErF@r zo~9hcmD?A_se2R5gQen}WMv5tswju}JmVDc^}$lb--{l)WCHr`*wP!(sq4PIpkC`jFcnJ(Jp(92Ckp=3}2u#Jk+jK-SEIGnq#c{diA)k14Q?`SQ z;GA@eM%MPQ_D%O%$CjC_D{>>0-pDYT!3MHEY#>V`Fb~dG_gdE#7LC8dny~`{mxIwd zq8&=^Maf@4U2xZ3mf3;>vbVN8C;}EG!k5mUDu(4>l~DV3G1A0;jNtCguk( zw6I}WeZz)@5zK?phQU^P+Q>jBEc|WimS>zI9x0W58h@L*bsLX;jNqIMqZiqcF_rka zQ?_F|kKf2Dj8hoFeN0~ga0(;DHW9yeXBIK6A}1ql0Ap&2-YaFR4j?a9iN%~4z}~Ebt2cI8$Ix` zG%KvO+)%g`50$T=HAG0q;;!9Lt8q;mPt1HS&%gquY2rf2)rt0ub27?`Ar}qLEQ#{ zQ``vU0o5=T1MvW;B`^=(b1)jG&KElP-mKc#-i^S!28k=5vfN{!iHo?Fur@QsK7hao z_7=-%?7WI~^fg5OFSvRlqjer1gkMWIi2TUbc;^7`K`nvvmALYa%V?Z(?yeRX_G=Sc0wbiysGh-SoTTom7U&D=83iEJc?qNQ=UQ0S z_~l+Tf1NxUh(9v^YGH+q+U<5@+*Mzyh6Y0Y9^@fbNVZt;Xk!VecoYrAgOI`yDqr@P zmd_(p4wBbh4mA=9!m+a860-L-i7S3LBUFkTp_fC|mAigajTX*BIKDIB4NLwne+@P- zw6-1H@Q}n`ACMXPJyebiNnym$#czgQFLcmxJ;S%C6Bb5vUHXt&uKr?c&ufE&<9WZP zt1RbLE1^_%#ylKJ;N^&<#pr-GhiBm681JMwRBnFyTS890T=K9)ryL^B5b(gg27kh` z(dNnli>=YuMwzXCdB%Ka=VI%hQRB^+EsuwO7kb?%Am`=ORal}EAH29rw%loe*LRJ_ zM=f7AcbCev`YxTM@qSN!Po{NuuPG*@;pKlobB}rjgAwV2 z2Ajhk&9o+dHq~S_Y~d2udDTe~BR)Dg+$=vR)B3H~43p8&W~`2me;4Ht?@5e^sheTu z&Cj&fESYICI=`+&Ukh)A8e&7uX`x;saaHU2h%bPU`yb4MshA&pi?%D51K-s4Dby8H zjXyinthj2i6+Q@FhlWfu?n~l2$HtrnwnJT8vd~^NJi>+0^Omrbs zpM`uGBBRq2p$ViO?zDORt#5f%$pxXPxW7u=Dfy6D7qit%G;V~VDt&*OP?=yuUnDA9 zy~n}`rs}==$xxXLqti1naq6Z8Bbe%92b1dRx4p|fN>j~LzC+E;YSpr=@hcaYI%5CI zt(%1hUQ+TiV#DaxX8*`6>*Mr=CZn;^-_tJ+_hf9`m(dUvOFm~l-YC<$TxyERXlTiv zD3gQRhBj^;`k5*wu8+C=iA*c9$29Zf&RxugR+hEfEAO9IIaM?MMIeR)!Bm&8^)nZZ z$h794m|;Hp%X8*2Gs_xQTM!*~p7J&WV!eyU1D`%>j%ko(89yy_Nj357&K?_xdM?D} z=*P|9y;;`vv5QPbqbFAF6MXpuT6e7+34J=5J0VH?T8m9a8%Cug<8zmwx3@%HvFsb( z8)*KKnQ7JgW2WiNe$l)flVv^JTy`uCwkO47p0#nGOyj}z!~C!o{iWM|J z_SpaB{@gu~>K+$@OUr0jM<+LkZx5;FLaN?7Qq8=rnbzC;W}B^M^*6)O6B`a*=&Gya zy-8ax10hyn{C!-+2sgiOmi0>8g|3m|+uOHzufbpV58%Ow%k9^i$xmll-{QU0Rpqyu z<(Fhy7ylbCY*5=kX$YoT@Z-|Z*9AK=Z@u`Zz+*aiaPQ-q=C*--V&nd~Gr^-HcrexSlr*#0 zu}rJ-iaA=_7)JV(^0~Eu_!x-2|E8FK-NsDcVzgnLSsfN%q$J8w1U-@FC9a?J zGNa9jA7omc@@Atw-kE<}3I zIib;q(XcN$@N)K1Ah6HTc+@PmAT;_g8Y_S&+r^)UR99Sx?=YsTv6j(>QS_KImM|5U zDIsNXXk39uIT}kig%K-TWYI|7r2LL-l-fq=wQrEWyWX#FH4h)nwEQ!r1rz@HCA$?6 zVkg62EmJKoyUm>6G|L*CDWj-+1>nr|&fG3kdyc*tdL3L}Q*(-14G3ai2;|%g%L;IUvAAfx2b^k;1otAmRMmm3cLfvA2COQ>#KdPHs{rg#M?caWgV?5?^Lhe z`=ECPzK=xoc4-flFH;>X*TTH(r7Wvbwjd^yy6TMt!ge8;Dm}Bc**hi6^53@vh$qWT z&FPJvnBkI&sVbFfYYrHiWgT27h{ksmj9{wLogX)|M`l?qeicO0?9uV@_-Z!<-?VVb z!Bl7ZKVkkoCd=v^yA+5Iv){{2FA-{ zdW<0cTDHR4mc69_1g9|K+e<5~Zx3Y`fZ!BH%x=EY`gKcA0SHcE#5ZrOw4Qk~w*UmE zFk;I0D^U&?LJDYb3L_-0-kS)0=Rri<*H(q{V#L+st8SJmh~T^!!8z$XI4?%DXs}wZ zs|+xZ5cU~|S{FLyVswFs4B^2Drm8S;y|u5Q z*vZ_g&At_h7ej4m9(y^qnsmp1$x`!uyhMqbG{Ym~4=CoWX{ z4i=|4s~0z-+~|A#Rrj?H@^Gk7Z=uyoSfUdb9&P&Gfg ze49$W7(_T!hT*JH!V>v+!&sV_>-+J>Tk#H+VK}o7-t!8reXh*&yf?RWoZap^v{?94 zz2suCp`kq|?@M6W-=?8=3Fbk$;l>Dq=Dvv$X=a#O7I3Pv#1`1ConzN*-LNc_yA*w>cOYnQ1~Cd{Mt)y6At=k~!a zbtHDFT)RxQ{LTfmzUrGdDLw%_*0^{u)w=TwX?<03-5)vk<9)UFgYT<3y@aVYv|CK; zt7rdwI0w668*h_oQZdzuQA=rkl{;mew>esHnX3ggs^!aRebsBmv)=ANyy`+2MAiKC zD(Z=|)}PC{59Rp4RSu@&(-x^u!{ArOoUe%2ZDEPLQVYI`6nbThQ=HYA8?m$dGrqw) zhuXr=p@JoQXx)Z)GicRk7-h;8X9CY*=60TU_4<1r`{fGCv ziQWp33i}+tlu3=$Of~nzJ>~@Lqs)GS_&qAlt4?bf!BnRv?WM2w@fBloK8I9QTv9RB z+8@6#_m9uAet%YY$REG2#%oSz%@~%*zvKOZ*mD6j4?0vhuL-Rt!xH&-oc;V+(pIyx zv(gJobmE4=YepjjD*#)q0Gw5yj$m|fJ`bq*+>wOy4NKH`Xz!~zeTi54VTt^^k-_h) zIV-QQL|(UXJUEtw-dA%F@L?2M!-XaC@4@jZkW?`>_|-a8u;&i#Ai@&)cOwI9hJac# zI4u>H=pY!4CeOec&hGk^8RX%taq*5Kt)n?^Wblh@&U!a2(Lw8LZS3mN+a*XJa}b8% ztctOQqqVkSV4Y~Ib)wVe=x-sZpi8AaLy(7G@o@4*pAPMc!V;ahlw8iiFkcX#0Cy9t9pN<~Pgf1|E}KJh-${ zZ>mg&ak$hY-YMwmPY1_#hsP=3`jGyd>&-AOE@>J+1gqEnH{O<2wR`fP^=6mxnbzc| zUpE;I3+(b`bIW7xbNI#^xeCFuGdjP`@omWW5$x^UUmZ&Y?#F^r77)u@WQMFB7Oa^Q zDy?4f5p2&ub#m(WWg&Y8&Iy`0vS;8LWrWV7{M<^pHBs`NC^?r_PU!3HTWs|yF~QVJ z-Y9F0r#0GRG1`Nvl3w`0?9nmP+TCWF3n3-HQCGq7itESO6|=QS%VwX%LQ5U=>UiY; zDEksPo2&Q#BbB{HmXdO#uPqTX<_?*Aedg0f*@mJhw5UWRQ4tf#QdzSkOADnE+U8DW z?#;}INS3mcq$tc#X%mJ1@8>gfKIeTtX1soXuh)FPr+c3Fd7iU;&hnh|JkQFt{`fic z<96q{g8A->`Qx4K?N>YD`Sab*JH}vK4L))rwgdb2bFgoZ-L%R1`o8(@fy+lbQ#Nn) zsbru>u2ceM%J*+#jxOvSuKDicY7X9h`^{H=6)>b9)P=#Ou_FUox9 z8yl~8I@Fu*-tjQ}u+U7TRJ)ExV&4K$F@O*WM}+3PjV~JJFd8;LX>C*RekE~sU@nPu z+33uvJKz1R*D#0CO5HYKTet-f?*|Z>?`-zxL`K6?NYB=%mWL>r-Z02V)W1-%yGd&d!9Eb?EA*msc4|Eiku z5qQAUf0SpOq(9#F-$vQ{;o5G0>oxyepC5s$l(K1ZzAPE#C(` zJK~Fa1%1wQt<2h!5)0hXC!TlgdAClp3Mu|uXRKZaDlK&T+&llk5(s?kMl z^(gA`V4xnderO39U4+0a?M29A9$Z^SW8Z$x_V8gybp%pzj;w0Bwl z2$|LN9F?(rw^=!PepL6oP(6&daGnQ#M#!wDUp+7y@5mlc38w(D0ySrZ%(eQ}1EZzp z{>q88J&W>gNlRPQRyaYHaMQqd1^iN0FnrTs?hQieoaP${?*v)GO#|N*Fj{!-~>q_QrQ(ZvW~lWO#$p>$5m7iDy>C2LYy7s_bi zftvf8RHNBqEf=E&@jGUpzr2~j=u2!JqM4P1-WyjXYNJZiOxYSpqc>z7;^{PyM%Ncg z=d>wFq{25?f_9KKf{YeKX&OkQfwHxcwwBXq8kvgu36Ck&n#GT|F1iIUdPCMHa^DD| zG!3NDAu?JJ_y&Cqq|qC)4)JsvNTWk!eIlcUM``**qaCz$i{8C+Lf<wFq+)#{>kv6@dQ%Xr1K@Iv4w3l@g7t|^W!65{Kt8&1a%?cZsNsS2 zoWCeJd6|2I8k5frBdS9p7FeB`>XK)-I+qSy=&sr(_q8iWwoSpVQsN0)>p4@&$;;e- zR4P9$HWE@j5Rl5EqTPU0+ui~2s&7_gduFf^OvUTDX}2H`rZOw?>3A>|uji%nU@G$_ z;B-8gir4nic`y~P?WH4_%B;wzlZvT$JujUHQ<*!@)A3*`vm&1k!Bo7Smo61knLE$Z z@n9-m+e_!cRJ@*-jtElm&8W6;^G187d?>O*<~7{hV3tr1A0gazxr{zVDj&h+^4fk1 z0z%0@=FY2(y?)qVttrc14b0je{Aw9tsCaEZ9m1^PJc|YqOvO6@GanZtGQEnU=1j#a z^(hDe0A;V?hRR;g{kHNDW@Ta7xul>}jNmoO@S~c0Ja~OIRQ6hHOJ!C_TPjmhP%1{0 zye5)VXfAtP@vg?~QBH?2RQ4{zug57ocvoZgE6hV~+#VY`-_05Ipu@Iy_DviiGJ}Y7 zvgfsZfODoN`*NVy->Eh6+K*Mq6JP97jr^I)nV z56#a*sBzGK^u?W*Q0ba;xm-V`j_z9?yB7PBS5Ob85?jLR;RWtx_m6dKM5Aj{qE*8u z^cxIfhz=P(+tP9RmKR>vb(2Bcz17=8M-(o%7|q2`F2(GxK< zM)m(Y&fLYqu?lQ0U*0j%lS$jFOJG zmyPA2t?HrA84={6B~*%IS}=mkWwc!n&XKgXmbGiX2k}2%8}w8B<%fQXGeRsi&-`?O zTfN7#4x^QNhGJTov@>WHU47T*Q->~p{9z^KgWOPy4 zdL%MJUO+HHr;8AJ9b|;0{ZWFK5n9^PdC02=O~w4|J%g01xr@gFy=Rc$%}C@u15>e& z0~;Z7>D8Heuq0d(?u2Qr!$ftb>EG4$iZT-E%T$S{^TK zUlu(31QFWuL4G&hNKS{yeq*7VF{8-ChDIYIG-78Cf>bjCQn4iBFJt?lg>KqkV(%=` zHU;q<5S$Z4X#X6Dd=ibPT#bF=Qk*Eb9*maXwS-C?zHWJ>eznGl1rl?Ta@h7&emC_v zc>KcH8t@2#2bU|huU~w$(9K@-qSw-f#}LG|T#E9?26!-n`C(7pDJy;+;(uP z`03i(J~?I}&ge=Yvg$XAitmlvG8ej!o|tWU?y!Aw#BR!pX93YUz=O->lJI)x+U1eC zsKX4j7t798zji%-LR`zEh-taM>{U_m*3oPdJe$m&V|fR$y?8K!xwGtC68zjD66P^P zsNCN>+2X;TLd1*5u;&)Ktv8ClzRROab2AQ zU^TUQSr}_=Vu{Qp)E{EKTxZQfH*x=g0JmMy3**>SFy@U*Ow5Zr5wR%+g(ts?8rYLRGCu7y<(w)EyY;eO@bR*}0v{UPFWGIuVQ z(MlC`Xcegi#G>*o5=k z3&H*X7>#{N&vUZ$%NmwMEGI{=Ug)mvG|yq)wts4FT}D_Xsf(|x5-$U(76hbX1WO42 z)ag~?4+C)r5KJX$?aRPMNW}D1#6EZ6KbjG2&&g<|-bm`3Xxn&cWK5%_5#EtWTKk7% z4?CqJn2Psv#lG&JTNSTU!j4Su4S9El9rD``M*AHY6JJy?CeHi8e(>+~zq1y)SG1q% z?Gv>xESy-DJs9n_e@y(fePiNGC28#u%I5JHJlB`qx-zHyS1WTEA)bZ?)LrOKz7!Uw zgogLM7WZe}hm-IqoP=2_t}T~@nW5|8@Y8tR)(5ZKI7d7X9WJ}j-I6lZVSb3638@xu zTbc6*{ONNJ`#-X!dJ5k0w+^llSqty@j9^bnj8^LFogJgY;2-|ic3UGnb{Qe*z8x02 zyEcgTq|@=xRE);k67uEEQi=DZMR<)_Wyo~L=27LWyTjTiAh#&D6}KUDWPj~UZiKlc zj=6++FoOBvb@a}miGSL6jhu%+<{@eA+mO-lVh%+6o4Q6W>DV>GIYET+H1*V$QHd$> zNol!>NokCbw5GD98VAH+Ao>BpIYET+O(iFK{nnXVkf@4n`eFuY794g$gTULYK?F(Cl+9BbZ8jo|$tkpHdmY zRPwu)5UqRt@Q6gw381xD}uo#A*92w9}|S zTQS;39HSFG*PIdD>ey4$d5{s@3XDc9Iyw(Bm3Ujw=Rq66=RwY4j}G#?@yhin&V!3^ z9()m}J4P@MMkCHB&Vz#Z4+y4`wD!bhBR<86{uoa51vt@jJ-DC6m!3Z9!`^@poD)Q7 z&!P7DoDuA!kG=BYd=4GJQJirOKnK7GrV_7wMh9?YaqUQNoN*T6#KJjj!6CL8<{Y&Q z=cp;*@sel_sH`9lbB+q*9Mudw9Gr|8A!$v;{FHL&w9T!{b>ee*AX7!8$2J%^^u#ZJEi zcKS>uR>u04-A3?j2Iojx--g?_8H?y{o68kTG<_S+B`NhQ?$0OVZek+t&p9WE(ERYA z7`Mn1Kl>;07H*Qc9+K8R;@NV#=&ixWpZ*j1DS%)qme#%#7Rz*f@5~6>V*M_K-HCICIAXoNAvWL5C>R#-Yq0mdjVa=93EqdJj1arBg+I-A zpMQ0vg@&(a9?Vyi#x!UgW0%qJbT z+h?nPcu>w66`eg}O4`=vr=&50ty&qaR1F}`o;fA$z7h~?<}Glq$)D&j8k(XMH~J-d zZ%j&IgnNx8;g(kFU(}<;-U*R`s0&NQ2rdaWir?+YdJFRv?$$gn1-j-;Ww%ww6X}WJ zvkK$&XBNh-7DXRu{}ndkdLV`Y5fucDB|$U7cquq{_0YsUwY$Y{yttc<09sRtHRxZ> z7r2dTj>RZBABg*a2m!%4!jBQ!D+1!gRc;!qj#08%MhW#fqoqG|-ze1}J2|Fp8W_QK zVzg2XGe#%w+>((~dvit(=SW&hV%xsTN~pEYj1VdaQ4w?M6fT|npO=wehBd3a#=#WlR0Nw+Ebv;Ee90I9z_-FEZOuaOU5>WO#n|O~FoH``Dr?fmG2OZ>m00lTxt7sN)qi7a&cmp~ zyHb0qhorUawnv9Qmu$^>1c-M62&Q6w*k66IHRo1PUknuI1QDh`>U6G?q-)Lyv0m0I zfL)KXKb;u)s(aJK1pILhBiOr#QWso#FurtsC^~gr$dXF!uuThM~%y1|@y7%3}`1E(Msu4nJt5+OG;~g0g>w%b20z&s7qm`<(d}uUzf46w8 zecj^RitIziM%<1bY>gg#Akc%-H-0~p_Y>@|=I>3>I{@it-D_fPuXh8?8}F|mRUb%I zvjnO1ip&zi8+-qj;qn+IyD>_*Eao8yjmDhVd31EcTU~R;z1cN~bHt;AUI%R+g2(~l zOCXpk=mSEpyG84uUTc|$cqY(1xFn_ab{QRQa6BU|@^3~O=LlM_mdwvyYZ<{j7>)NO zCkG@}U=Mc)dpPb7{syJpwAU$|)&3ihs0r$8fno$xaZ6+6+}1R#_dc8xl*;r+wVY9j z>Dak$!VZ=Z%!ARmKL_GJAo}8ubAkx%3kxSNyr3J5Q|e;8pyOSJr1c9rJ{9ABkY3O+ zg8jZQ8u1O1`zA&-TpC%~U}=Q+C6d;+$hKdcxwv=!1^3SNaPQ0rrs7=Z8@HQ%x6OMs@hzk8wrvF8Z8H^nqp=ZuQ*H5}eIl0xe`ItU&N<=>N8g4s zKl`Sdsn~mtjgZLi&2URycSwZ@BiIiQqm?SeP4z9fsosd2YN67%h~j%kzh_}|QFQsz zcxbeZ;I}xer({jD?R6*{Z*j)rEzU8##bE?f*&c!F;VsS(yv51H%NmxQso0-T(OaBm zc#BgbfM9-%R%##aYd7M)b{y_&nFp6^*L*15*K+HM*CBmJ%OxrG0B&Eu#{J{j_+u*e zQ^X#d?UyyId*ynFS7`mp#(r62aVIJZZN02v9$YT#dIK+Os9g3>Z6o-;mZjoWV17lB z-_vkkd%fJ(5)Z3=@VkD=Q*^`Y0#PY|U>+7)S#Kt2{^0phJb&r8CiaUI{?3_-zsWkEayt;=OJu0QBS3YW!H{1p`K zGW078rV>wF`elWU;8zxmkhK2}fY7*o<+mSH+h9F3TB)A&W`eo1R6Jq=?-__Yf3-G( z>%rv)>!H1wL952EEQkktEn|Lodydt4V5ccLJp!w<=zt=Z;azy8>4BJfoq-q%#B3m# zO48c@6|e0@n!8qeC&cgBGa=3h(ZijA_(8wboa1RDH5zxRueHjmjXm{x?5TOrz?vH_ z34S24XXuALL+8Mrf%gfFMr^?LWwZW+b`a4pJsp{@EmMj1t8=%7?l)~_LML;}#_F-& zK#VWeCu#&!ah+hvx@KtNP`PfAEoXO&aJej%ptb+A(h*F>{NPIudeL2I*-Af7h%(`2-rSk%>B4p2;Du0kHyS%x5MQYZEgnIH=BL#39&WT3 z5cN+K#FRmlaB^`5=_wve*wj*9mdSdesgef_2S5 z7$sbm=(1{mzR-Q{!+8Oz9((0rBn9PH0w3nV20hfmsE4nE$Z)sOM*tY{O%Opx{{;E z5BEQ#F;j=O<;;RqJx~uWi@iXxgm@eE-`1QBpiThA2rie=V(YvuXAThCfnX|0>;AA2 z*S<3}n%L4UvUqd1h-ixR$|7mK0`M0MuU~}V@0`&#V$J%pDSB07gnZk2y<#+U;T>v( z^{U2HqHQ^&cy+cBO`z*r`)*-m^*flU=^Nzlj_ahfb}$0%pjC55+lW>ps;20&0zBrHz0v78HsAdOv5OfE@5T@Omr@OgF9V1MHz5w|qBF;Udaq@blVfGo|@l$|DzqNgwe_qOW6Nux^Xr(s3 zSs^usnl}s7ywyW@JD)u@VT%X!J1*xP8`*5JWZ z8I5~5FSO5dm;5?DfDpOX0fBijDmBz!YSKe@It%LMxhF@DbLxb<`o9;|+>4EaK9T=2 z^72<3N;h9{`!(f1*bW}&26!y{Y@qYOJ$dfHmnS;R53g}A*pM<4h%Ny{wVNJs4o}N- zFTCejhtY_YGAJ|aGaxPrAo?AB)VX&_o;%_5=Nv{W^}&0y!y|wg5I`*JJIXn^InO6e+#!Pc6 z56yRXKaJD2Rr9KIzJE_61h2m}dv=DE!OYKH-y5Pgo-g zNq0GsXY<^@D?A$*?QME?$?_u{5mlYo{m!&^^4x~KCkGH!hrJq;=tKXa=1lcN<{)Rn z*Lm*z@1_P26)!&+gD>R7Z$L0rQuV>k`B*=i4u3I#_^3hUR_9~o+*-VH>e?P^wAk7G zb-r8WM?{lrmT)F4&vU0m#QVjiduN0Zi#~A!`kZ;pz4~G2hksCyKc_p{BZoRa^~rbp zc9T=;Q2YJLG7TCUDTSBjtz)sxi^M?-3axyaW-N=nEI*e9ohZ7BB zhD+e)(n83HSur)=t0-1SnbSnAaTf@ROR?h)tB3-jF-o1PEUqgt=-vGehL zY=cxxHRVst3@!8B^B2zwAi7+fnOYHuU4j1K_Tj#P2bz1Pcsd!(?@_6t-c9^~Z}4f< z{GpC7I!U$1I1R7Hj^n7<#J%$BoLDaQyO(1<=T_wQu@M)JzdLq6YCZ}zXC6$&Xr=zS zYkAg{sKOvFzn&t8k#L znCgbHeVnO3=DU|p5yXKr&rSUi-}xEE`*z*BO!eB{`<%ab;bieYL7e|oB>PtIcrL($ zsSaH+$SI3?zT-A&tKe5_MuX{pq55ORIzPU*J+3%MODKrF|E=?5h}#IxVZ??`>-}i< zHiB~)k@?bk_wWa?66(P@j9B;EdbiIl*(D%2hY^pupQ9cDgj7&*4kIM3BO>_q@DQ!{ zo3a>DkhqyiT+;i#+2HrV{|#~Yf{p%+Gj7cn z#dAsVxXMJ4@@MhbbjL=2R%5hR522Pn3-QeBU%FEteaq`LM%xI^VZ_nbHJ{B?q<7;X2*J;OKoYZ0SML}UsNMljXiu}!D0g91>;|JvlQsZ7Oa zTdEGtH~Sh7Mwf`l6dsIVs*`&*yPKL#F<+EW%b(SQn;L)N&jvik>>lLt`pIKo_%jZp zOGohNV>DjTJ$+R)>E2e}j-U4>X2%gqH))Zb+9uGr>8#I3} z0-x)CT6pC4zaV=5ja@th=a?Nw5b?qv<)d@j_3#je3h~7Jy+|nCq=m;Fy^lxk$+^Qr zaE{q=1QFw#?~81G^KK7es4$=Vdy!B&|BllZ#rZK*Sh@YZ2%>rTXp!opYqBF_c0B0u zFjPvJ)hCp0eqVgcg6#f?MK=<$=mf1t$@X!NJGW=yJowfEhj+`EnWul2l8x0_{QEG1 z*S?3AJmRFR&Uc@9L3T2WKi!=APMMa8mgQ{(Q!zh8MW6auR#ogWwgz??ED7(mp)bjK zDoZqXqL*Q*n5yyeInE0k^4+)U$m$&5J|OEnAfV5%5KOh`yam=S9DGo7tx45w#Z=Zw zPXw`|D49OsRX>!@>%1LBiF3@_9z-BYOk{p>JbFU~Yb$@%$0(sagBOV=q9f55Doif^ zsvk->X{pD&kN!z3dqGh=dd@Lxdk_&@Fe~lpjz!Vs4Hd8Lp>+NouS}CZYjbsRJo?}c zKu1TAR@XZKeCMg3`B9WO$E@L@bpG9rM{lU0HT3rhp>&fju6d5GIp>%)JcuZ1l*qYX zpXe5yyVFk#2c>>_Vq^Tu3jGt3=5^bwyPi zx=BO#Pph*XmEEkIHiFT1+$AFkmm5mAcvvx!OpEevDU{B?+fmug>J>`owU5(wOeBNA z+joB*L|f4+YDZ-^RCt@^@4`aq{JR~M-Sku_-5?l^ZqDQ=N@kycXlb+;;k3%cTAR>o zt=Th#(hb_)ulOE;ym4g^N|~KSD4l;-3TtgbueGMn;rGy&DwNK@qZKiO*2hdLW7m`m z&*1*fGL&x8Qge=?q-RlyBw^BwmU5xN@#kHwffR&JCy};Z==Hy%ote?lhtfH%(U{%d zsFvgq%X&bKpj-T3v7IfBq=L|%OAzY-DSrhejl zI4Re?eeZCG(byN(n;d%zEqkZ8Mj0Nh&dGH;!aqYf;E3= zjq~3Vx$gNlfd`?B`l93;6TT(kz7dR`MY$DueU-Fn-7i|KOSv0TjfGT<;1-qi@_o5( zxp$t#4q!#-%FXovpcUqn9(C%xbyL<}Y&H=1G~( z$p=4i8hw!GzCB0uyGP$&n2I}=gp7EmI+U5`y!lzK`{1h4&i0HvXVxcq?yyd>(?9n1 z3t6IXKZZW|zq47iB6- zcB|&k+*d0#1_WZ0S@l?SQLeLeWuCkJyeWa2?`i*cN>&L7t}UaLN^;7kz5zr8Jh43_`}-6`XeWO729qudUj)PkAew!$!MR~|u6t$6(az4JpF5R4 z%X7cjhI$Yh)@LhQXD3jP2BDAxlC$&N9-qlb?6P`BDl8fkuxyMn_q}I)<`kl>J~(q~pdKfF zo0P3@!x9*pUb8YRm8u#6Sn6_l)_NjBtSI#v*=DOV{JsD`LQIqpxxPMH1hSp`OWxs55_MemI zPELL*fM~n)su=Di61bm;GF8%+?atHJ<+-Dljd!G^CAh(SVbgQykGKD6n5FM)8NpOs zC#4?g-#IlKb9*z)?M%fyq@*gN^4xv(p8*dnwcgoA61=KOn%6;d^BcZlO!gtH2LF2J zL9-eZ?q2J3#@z0t409IIJP+%Et7555@d6?n{w$ft4Qtmq_l?YT>mlYnqw%8g-wU## zcTGV5Y9SUq_=QswYt$thhdGSKxnO>WEPeCCJRZ8~OK0iCT=$bzSUCxeT};wBssEs@ zenx*VV#`_EoRxEP-8c3=<}g~R-BW7C5W_uzSnd{L(d%D3Bi_k%r+qxqVYE_-<(Gt) zq0dtTJeJMd<@ES1*PY&TjKgT9mNefQn*<)c0z7*C@|{0Q7;W_+{=@W#q)o5!+_j^y zK}&OAYa{gjiuqx_YI;T(FJ2Pzo`F|RUNd;z6{-B4Wi+Yv8fRaHe0TVdGn_@sS38Yn z<-4yhndhaKotf|cJ#_{~;(^^irszFfUjABVc9neh={nC_2rdcp)otJTI)J2%bgnP7y3bOk_`;m#!ZXtHBcb)?J z_Kr3hHw}|Eq{yq6+rVQHQAK)ha#ohlcXw{KTR|!J-i6_cKsmir?h9iG5QYMl03ry;$rn@Q@uD z^I!=Xt<;&bpZeec5OPz^2(AaCrKS8C4rzO|8*QYKRuHA>ELrQwUz8wNXK6H$p>$5O zF4H?lm7ud^ts|p_2kR`21~Qb+Y1U;bRkF^KwT_Gy9;~x8nnu<-GFlL$5m zjA8U}9Gje}jJ}M~!UHw;wQ5Gc!@8dTi&o9nqy@io#Ju-ole12S)2yGd^>9Xe#+tO# z>EVpVE0oS@QT_2Zgv~{P`!x`-vYt^`KL@JI= zZnS5tNn^AiI5xS_p0Os4(SpEt9?z)ua6;0ir&z1TXrU@it7dd$wua4H4NBCi8NHaT zVe|Jso>WHT6-wu{L5ozZRbx#Wr%lfaqGYWaYtopXAXuwr^lki2F#5=^OTt637k#*d zwU+hNW|t9MYk7S&YajCqBAAL-PQCk(O4#c;Q<=5zbUc`fSI*LTFqOH-I~@ebRJA)ZkkTVgQ?8gcRB=9 znYHh9h#-|-IZvrarVL`)i zWv}OcJx<}l`v;&=anGh)FA6wYT}WELlo#239+^SJzZc|rQNS532&LY`DK!b_ zT1JR<1tWACFMbiz`;!I7`)|ziqM37@ill;8I{}TYR}WotmP8O54cjHe8NCbf zycZy@H|L05K$#h^l|dA6o5#7e27Q2gi3IK^qFineVR+n)h@O`;IT0C$Sfh-Pw7yZY z?O1MtR5K*XIHclo*;>Jts@;Lf;X5GJgn(31Zn*)t?Ln+?E(xbD#Poh0@x1HwKVk7; zD+uOS)PprK5;uYm*MluoxK2f}OlP9z-2ye|+OmX2@wRiq$0H-)0i1IfVO#L@8a^yM z0TSJTnlnP`@!D5;UKDV<<`UDpBpzI@q%U2T=S2jE9sOI6mdL}RB7UcDT`d*2bW!x| zCWw6cHX@+1>|#A~>{y-`1>8ooJ@3}=o2YpjYR-}b5k5b4;dSAdf-ZbNbm5E;3#T5? zs!6PH3+=@w*HUqgq_yRXEmdZAw=nEQ6IbBgfXfwolix1O_hNc;NjL?=KioL@hpP+! zaGVoF`24(~sOQAsNYrzbj7t6e3&6aZE+Z!CB*X{A3!h_w_%ar^S{4BJa5GH7Av!jhx5Fc-i)xV&%SH> zXsiqBF$?oMm&IsqX`2Tlf;=>D+)Z3qDRu&VUI-psu2`J)tCjCX=(c&xLKN^MMCaa* zNZ^cM*=>8ZPk?yw#ch#Y0R&Sqzf=31dyV_t^dPOyO6=0~SYjJBNo%yn1G;1HUuF=Y z`7yePRF7Roh_w?Vbh-$kS58JqT1z4~DO%bhY4q3?DqR-a;K}b?b2Hk5h?3C4!|Y^O zcDCALez;4$q<^dsv(ItdfADDFUK4Asc5Z%gwC0)j=Z0O3-B&TLm`dytt48v@Xw5d_ zKSV@WjJf1$L_}a7lGb*2*4$;)L+oP)zn|~LUS?YvNozZCyXHZ}qO(?ek(3z?4`zsq z+!8U7*C94?5TVPpZNsMm5dq@!0D`H+Hau^AesS#ORuoB@c`%jyt|f%8dqfAGikM5w z5T}ZB*ycxm*LG~!Spv}oh<5`BvHAJ9#%eE;GNa+q0riNY9$%pzEJ?5)+Cm;4nh>LE zBqB~##vdajt*LAq_Z!-0#pi*#YNJL`v1Pyh+k7vQGE><$?hCqP#a{*D`~Z(2LYIU$ z!flpEMxy3_)@YpI+OoBiU2}<|vCa#A5-z+rCT zFdA`}HWVQ$0KrsjOJ*Z(NnP>Y7}NvrysUa~OLIv|{q^tNS@VEs4+K+*E!h)|S9_6^ zZN$X8T17rU%;J|3<(PBWUX3Nhi3M?y`xM7TW(4zKG-CVoZxy*8h$p~#%QIkK|HU;h_lruFcNw6iARUK3i69%dP_X7^ATt3cl>br7so}GcwTM5;}7s)M39H}IRT#>i0Ab^;)P8|OfN=AT2nDU_>);uJF*3cbAVv( zOl5nGXg#(}bQ=7mE{2~}MlhA_ZDGy0GSNQp-ufX>VnO^k=5UFN%v82zaeu@`?gr|g z0tn{EXjo2GKcmEv$gRLV3%^m_Dx|)N@4Sud;i#v?M)6pa)m~(48^N)*7{O98TB+-| zOiWvfF%`nt;>sq%A$;fDgN)#kaC?Q=%Y~3C3x8Y|BUnPE%5<(0Uw=bV zLgGj>m87-Rr;Yd!Ub8yM||8DZ|oZ397eFUyi%7T z_VV3`z5FF&FLOP_(!Xlod@ouv*U3gO75fIT5utO|gm>bMQwir<=D`wjNlNvHN9zX> zMR~!tl{uWlJ_nc|UNi!+8`O7}faozQ--`&%Xxvi5qxD_zXk8wElB22EOM#7$NXpNE zx@rlC4sYap@s=4~st_RjYr zP{Yr}_@vlytek`JMKOY@xPDmAF-m^!&^59Zv5|x2YX2pSR%$3kYv!_;O8h8Hg|`if z4h^rs@Qo``lq=$osU)p=u;&G|75VsO9$c>7R&t*B19J)fqMXABu|)f2ZGLei<%x)- zd>m1fN8^vFSP~oYFT9wa1uy2i;KiJKkXIS*O}rY|*e*66QuW4azzCL5tmsys%rB0l z+@$Uw@uTn^eFOeDhi&cH1`n25jnB{0Z??HymYqw&>P$6fs$k8vw+*~yfLGvAh@!j_ z@t3(QMoZctZU$lu5K~G(XezD~&dn4_ncIr_@eGcge*Hh<=fTJJV6+wIaQpB)uhgxG zy<7u`76AnFU^Kocj9rQ6eGqZIdF*mq@#w+M@|Je-XGS!Swtk|y=Ue2FmFQbF%ktJ@ z`(a@nfY?4VLZ2FKZzQKf=#wd1QNkbcsBIri$INguz7OUhx`hkj{b0(v*`93uwDb!A~&~-&qiA%1=@<+hb2_%&R*@}?~QC8 zeHkdu2_m$|8>Kc4*^|-@-ymvlA;bdohPtcW@wd+l)O^uh?ISm#)}2v%t~n#P6=46- zw|(RyAYK82so2WYM#y|slKzS{P~3(>uguL4>8XPHE3M1&5Izqs%i;lh9IctM5lkg% zO(n02b=nikdr>2)AVN#RXpcvx)~az?LZvlnwv~`zPNE0#%GB#Y&7ILa7KGmDsVyq9 zK9b*c{|mxs?ij(cvxGu#cm%1q=DJQwEvh#@a&%79Xxq6>qhbM8_sd*(9eL2QAG0mP zHUm)z#I-<(mC&`{<+>|c4?YEN2XQQUewXM%O>}?-;0f_w%Hn-}*vfGFotNajy|GC5t zNX0zFCaP-HJh%7z!z_OAC)2G{3~!JcLX6?6wtM^XT!kj}|3GN4}b5A(+axq6;-Sp7tydHSlNgAX_`J z&A9i|T(@jgB6=V4qBIb5!7%`s+1leAmEO^!M{lPv^O+14CqKKWhNIVJQ5 z^Al^Xf{J3e%CgCN}Zj(BYsDj%M*Rn=$FcUN&SD zf(UK91{=jm9kR3qDDz-Rc6c&IEN8zABlJqZ&r#&x+O)Q?V4ms z#ddJEg`0EjtKrAcRu7`BxE^BrIQ9HIxB8-Co|UO-tM2btiXX{HirxY~jNo#`Ca(IH zT=)I=A4Kb}dUI%sSTC2sewPtU#r$BM@Lr|(kKi#FJeVrTLtE`Bb=A?o<5_oH8m-pn zQVYS>dA8l%U4Q-^PX_e`0R&Tt-QC_}x$aZT9!5QCuB{Xcqpc8w(BdIhluMiBxeLlY z?%CJsc7vz%i<`%ifp|B7;Bpy_SLA>F9q#~%?(cnRl&OMJXK3Q^F|G zmc{lgI_=SFkrfvwCAQT{O7K`>%UOF|Eg4cR@*=2P*G@`sPf1!^7BdnL8_+sW2u;*&7<*%J9BErlcQ(jkLMD$CYF-ASI52KWSQZf z{H-+Tp-Y1HqsM=d zI{Rx!EAGQB0OfGGwngr&H%>Y-ZBYY{oTI}7ekr+hik+0N@IS!62hivL6^kMUv7+Cv1Ma~N4uo;2)8X+d!!%Ex*bwY3`zxS*-u*K zxwYRM;V^GlNEeQXzWIfU&)cXhsl?v-*U5SAkp^QugqBdLWk4(e;u#>ADrg~X5KAu# z$HZ^#g^-H5i|kq|E=hKlN_~f!p*&^=&Jk-#mgjw{|%{r1rJ7uwdBKfabMTxS+CE}`z_C{)*ibkr7(6IV(hlS z*p+g14v%Ad?0x~n*Pw0^Krj_csMI%CToTjwQOrXuOFQ0}@BaGpWN#KVU(}tb`2(o= z*b-{4CA4|8*gqnAji{J?ZWuP(3D|B& zx!1&c_2xZ!ZqGX=c;ia<6LfuFzu@^iYgRl;bPgl9Z507l{nsX2WUjOJFyiZmBfIdA&ubH#ht|Cjt;8SBDWmlf1_dHSnN z884h8X|1!8w6-fN8duOCC#IFaLu=Al`{^MvuK?m>^a+ED9ShLMMB<;5k(#A)8yA9JLu?Mb9Z0>($ zg3D!p3G%z~w($7p(<9#l@l^>3eO6<%Qd#GBi)_BIc_LA>d4kVm>|w!1u+EaDVo4-z z><8!18W__)Ah;ES=xOjB9jZG=RyER#2B~zp^1FrbHATVJ^?Qw5THUne&Ya70lB3TA z_M$RN=x>7erMLxXb!X04APxe--#Jss@5al``jgcn)v*_SJg^sKD&ChO7RTCJ@k+np z1=6q3$LY3((Fy-DB(phMZjzjt4pxbL2e6FeKRzXUD`-WEEQO#z1jS5TW_O z766F7cU_$572v@u8KX}>uCxw-#~5axGft*0TbPuXvmhzK^DBGq_`f6cOwDMelHZ%2 zDz>jf0&_dhwLH(mkJskLX^(%KoLGlH&S3<55JAkQ-i_1l-i7FO0R&UoQaL>vr_~4b zWT3cQNo#K;jE4W){95q>jF1i(DU9He!DyvMuCEn;`EZLw3;Z#H`=8N8v*>8dZt`}J zzcoqw-yr;q@HdG28_bFC0~P$vHI>-y``@GYq3fC2LV$|rL_h6`RT@H>gk2BaR#xi<_~|;?QptZk)8KnQW>~oa`}~+; z{Tmeczxz^U1`!!I|Lps!Ht3dj|L+65e(+vEFrwGXZJmO>``z`3GA6%Y_uFrN%oq7@ z`B%h?r81Jh`jDiBPf75QGmhX)j-d+jQ<;z|Du047cLVqTKBN??av3?nuv+SXhg3YbWMu(lE#P;@c-EL z%mQ~jVj?jb?UDXg7~Yc;@S<$-$lE>ADZH}4{S7gZ7>!kJN#oS_fDo%j=8<%HrZcNc zf%^vH6*0P~t@PQM5zE$0^IMeBMF`l#C14k4@eqU_SBx$~z{6$&o;IUL`poysmGrzr z1zseYAfoiq&g0eeV3U<^hqY9~QB(2gle8W^7*jJ2Wg*f{0=4%>A|o_a#Vgx8Ki_e{ z{U2g`DfMBQMp^panRzf(za_Ug<6b-9zK+;lIMDY=YIzWCbv(d>sSb5@oIlqTxU&!~ z59g>2i&79xCjqb4QKpJ)y~g=tbAdY!5%O%r=GRufFFgJQ4@N9|vYm7N?gDo;BIGez zPDKO#SVD}bR{l<>@qq*G5kzWYH1u#cSBb3zkH(OSsghdW=R8rRz-@p?O{;J1>6HJs z!2Jx7><~-nhdQbKffxw{Qyn=u(0Qa~f%^g?HQn^uiM9Ezl2e8y^LLwec6W(IU8;%@M1fKv-&$54;HxBdT|t+uF6Oi z9oeJ+4{jgkhh5*-b+S>1L>xT0twvne+u47-!2JY~{ur%P_=}5Ealhi7=o!H+%4nR5 z-k;<9ZsU6N+jOrp@UH^*B1HOQv{IYD+MKQLiWtGQWi%r94sM#-8B)zg&HLp)?93Wk z;J$;HQ8&zZ&^h^Cf!hqRwivBci{I}_`4R}si`F-I2=mpDZwuT<5Zh_)s;SPgjRo%a zh-!vc#XBEK`5cIg1BfH{Epk3@S?Heg<4az2`NHl6?xC~9Yu%Q==VZxBfWIiuGF+}b z!@W}dX%F#d0P#rUSxyvEr6blBqoF;Yw$6*^bwox9&1#Hb-ne%TH%#pgsivSFJeIiK z?6w--{K0H_v4yuwR_iVrGSvBY8fH<%+Txa0YQ(eWXCwYj0&#ev8~z#MY+hO5-h=pK zOvPy2L0;1^wOsj@iQQ!|5^2rg+K#C5q|@i&0xy!CQiJYnocuWIF%b3O97d$|eA1~l zyTG04Md#YGwO5LsuehI?iqRM)=jUg^QqR-%@x1%``70gW&k~P*`P2e8I$a`wr5*b< z3(?pT3s7@LFqOoLAM#s)JAA=BAoQL`67pYWWie}s{Jv=F&weBkdyV28y_N`K$rnGL z8kHn7n8S$H=l#p^Lo0%iEslT5&;BLjabmmi8d~Dqh36(TdF)4 z>xYq;8IX#ps#oaf+|azx{nCr))x2`a2&SrdYZqrqN?~z4uZ-uPOijfb`emNKa8nPa z`tiY=oz1Nai{p9aW__4F0x!u2cphR6f~js84POnB!s2*d^=dqudIos>6yU*Bw}g8+ z1EYoR#a=wGKfXK~+lrc>AE-G~^(xcbd7^EhTgQv%_4)C%SbHF#O||-isaE#A*V)^u z&>ic=^SXE7sIZ=WxCbq&HwQY0lMCHWCC2kI9x;O0)bp?( z!N*2$4kJE#>#!e7%0_SwBL*BlY{#e)#+mXzoWqE$4!@lmIm<(E4kPYa_**IQyf}vu zlGa`X{d$N{s)75vUlt>_J^%ZuQh5k2ixFIs&4bHgL}=gdc3WkFf&BU3xumzG{NeY( z{|)i&pg;XNYDm35ONr;jlC1pbPk$a{v{#Qz2qAx#RF9TD=Ew75w2j~#M*LLvuTo}e z&S6Bu8~<`oSq(%e)pIJ!a$ommeWqfx-5(iW{^hS;j4lz8DLfd#R6kyD_1Uq`~|5)qlggAq)1F!FaP@w}`)fBFx9HsCR4_aGydZvDfbaaIz# zbOet+M&q`6+*Q%#_Z7wS;+>4yqlD5;T57vFr9t%U9Zo{hoMUz`K?KM1GE`_cfA<3K zX?|MDg$K;&!#8#DcyNx{qXZFW{8c{s-VH_ZybP66W{(m|H)-JkznqZ<@uGNMoMZMV zK?M88HB^|-{oP9_oqzZIi$-*uA43J+{_X`{(tNZ?#lCS36>bRp-AgFl{C<2+%7?Qn zwoG*TFWFiL50!n)X;-z-{T}hWc;}4QZS9+dU&6iw?*}b}qP1^){h?01%LA=P-uGdscx?}*^Ez*P9_Adgwg(aL@tZcO zIG&fGLQF+})eoif?}%TM@>!eK#qqohRndsiehy{y=pBHE@Z))Lj#HNDL&&yCL zW!Cmkx=BkPvgcvWF>5#@HqW^^+!3Q>PGBVR8L;1LxlXxih3+S77GSk&{oi{fo;jJS zY38TSJnZx@XfCnn)`efnYL4CO2Z52uRO9aY%z5@|oF>{zG}`dq@!_hF3cFcws4#2zdl7g_r+tv;_dKCvHJW`wDBYx$;vI(_$;|8- zYy_h{Pw8HNDL$;_$sLIcHNFO#EKKw5hP7dh0+az(dhC_>;Mva2Vizcp>*C~aav_! zmyyuB46|Dbr5m)pQ}b?RL=Wf9a0Y>>5B{DCI{@0L**=#|pDSe~!95P`IBlQHrd;Uz z{XI4Otov!Hxo8IEPe#7}qQo1fNi$lgivAVtFwG5CD4o;hE=;J+Et$C)llvBZW5V|o zN_BqrJi5)W5&CX|(MmnkJUdIjhZET~chNd(9(=!3RD%+wI%9>8V6;-LE{c{c6%f8u zgjUMQ=-le16Um8dD@*GdsbocdeZv7a(RY%QMri1FQ|G=f>-le9e;9-+Y>E?pO>WK5@0P@sIuPLlYizWY*3;^ME^T@d(UtNk@0Y@-Qb}9^fJKuDff$ zdm?L?!)T>u&kMIo`7b%KqM}_7E|(=NYF!>zJTlCEB##)u=qO`cLrHV%>a4IOX~Tt3>FvU#gdCF|AEng`s4RYn2v*th+|dr}Zl~Xaj8^KacUs5bKPZ8B+g8of#(n31=Zv;`5dUGMl61*maGiGtw#0Yt-I4_n zdL6X*sScOD)bdv#u!cl=zT#UM`CZ@2;cHquAw}V|);bXMa+@Il)w!aTv)im#- zwklbx#(FV+QzRJHsu>+wD4o-+ZS(fKe#9xJGWs$`3lG++868U~&q z!)W0FO_8tnFwN?P&0qp23d|+ubZ2Wk z?3121u1Y*Rmv)Y}^(98j!5WX#=}U}eB9zW)Qqv>dGQV6-4O;*`;Hu*QSYf?$1#(Ri@-gVBO0 zP5WVV8MY?GyJIO)`(gAKwkE_sO(cY$(M*KWIc?BF#Sy1iLP>ME zM&rT!go;n8Ol8(S)|WiGa&l}izNq1W^_;&b*&!P(XN}3{hIM2IS!;ETT};c-1#6vXS;w z1XJ-!t?PFxf~m}kd^#RX#p`+LJebPdf1Qp8Q}NnfIuEAewY_u%Q}NnfI)bTqJue-> zROW`}bTwxxUfWCO!Bo7SmyTd6b3=1FshEn__R@JU6|d)|BZ5?XkF%r0IqCk`%|r@+ zybAOBT0%X1gmBm8^4fliR6c@NeO}v7K|m<^$J}|9vDXj#J2z$7tASbDgEIpo3>B~K zr$d-EoM+J>f~j~1VCLguM5b49)SRh!r9K5A0HEwO+)&x;x!+bE!mKPTJC_uciV?g< z8GclAj|Z=>hRR-RZK=!(X-j2F3QEO@lGj9%3e9DYE8f+Z{mSVOhRWV$`1Lr22k&ak zeua6+{Xj;O16~{~wqvmG4G57LMC>1Qz>9;$Xb6IT6^nHlj1Y?&M(8x6XZHz5??mjV z8y6mr3qt3J6~ryCAMoNw**tn9;?zD+uR_!*reeDZ8zE9fvHHJ`^_-=WG~Z_E77_ZA z>mhd@nu_b>0WviYrV8@V{5*sj2kl2+ynz%dU2`s%>j#U&eamClVqfwK>cLcEb@9tD z2fR2~HbNq2RYiQN!xE>8S~Q3-Rw*qJ2P=d~P!+B`5n&#Z)^`QA?b3BXYzB3s0D`F` z{l}UEUW6-X&mq;xl#}shtxm>S61E=U-o)MFwYOzGjG8|js5zI*B^AYXT7UnE_`isr z#W`%J!TgGPusimNQ^1F7&i7p03h*I>7**e=9*?xbA4|eDx2?8by!qoSyhlyoU22pO zBGrtm3cNU2EG^DZi*rguNVyd&7I?9*7+n;ds}dq}T^W#yC1LdG+e%Bt?S`8F)H>P| zF|bA<78Y|C>ydpo7kF{7Y{XB9yA(wPrM`&0#N1gbu~)k`rNE1D1xwc3%f|B1R`t+U zj0p1362cR7&!H)Ef#?uGaJh`O>%lpa)|Nxm(>&F>KV*HEJ+Zd+pTD3(6eZ8BvusN|iR+b-9Aj z-~6dOn2LEA#98R`v(V?w5Iu`)9_({%;aC*asu7}QwcCp5S+HbWc~gNGIZM!*%C=Fw z?a9R{jnKL|f!1X-x3tZJr3&)Uyp?(vQLXwTqSZBsg~jEvB(@#os+&8+%3&Sc4V^wC zgeqfLffsAbmguYeD!svP=s>7F;YL^h#@L#BK$T$-;x?5AJ8N=1N&u zP#kM(f9SrS6?GS#fYh^ z;+qh2f)Q+2#%QIcAb!+v#EyC%F{H%W<@WPnIhQ%pvMjZ2M!Btm2>s$yem9n^r$bEZ zTj<5}VoMvPUWZhJAk~b3R4j?urLJgDSR9?}!(zm9KyXeFq3vLm>O%3nxE_p_-?fDB z#CF~CNPR@+T7WoJoWnLhc0CRrzc982Gh7HW9G5FLKd%od^x}DON!Tqp%Ogus{@4Hy zMle4hI%UPrL!7Ov8#c0d2tQq0+xB$^B7m&~BCCF*sMxqBr51YeyqLp-aMzJ0h9QMIC0Ky;yd(cCzd76XJOtJ@cyQe#G<=tJw1CgGJ5(oQ^<*5f_OOED59GHM{fr*lP6o zWr03t3Ash!YjX3lFy`IF5}8Y=Kg4S5=%_+3o|lcNwQf?RKcaK>LaZ&$VaqUXX}q7^ zJSp;0o2H3*0R;14G~TIgEJFMX1XHmsnT_bX>xZn?sK?uZdT_gON$|fr>-<%PI-My~b$V6Fu50vhJ)Fi6)59#bvRD8`nvx^AG{-uHpz_ zjNoz^jW}@~T19FBv8a5D1XD>`Th7^t=#hzO2j(UxhR;LXC8{mk#4#G)#j+-*9RhVS zqBSu>Y~p^JQ0T>vVzg4_U#lIt9dWi6Bi$qK_b+*TK5A zEd*297Sa`;EE64BzjkyIVup#0b3d#T5&?{<*!EeeUWfqJ0MWlL4Io%TM&tWf@kT5g zW49^BF4vs| zaj+P{c94v=qjRw&VlmR=i9#6}EjZ9l=z* zpTk=9C)*bjabPQq^giuO|-8_@{S zxw;`b*K3H*#Z;2ko)>JyV~B%QcI(QV@?WjYVTAYqxb~X@FAf%?;j^a2{aN?nR5VH= zThqRlYs)2JX6QON{4`!V^}$Og&Jo`LpM6~5#ie3?O5G2s79%p(ABgG2IqaFhmP%r6 zZ5>=8vKFzn7{NXc7!6Oih}1L;k(z!*q$VD_jF5EevkJXPU5N8@8XlU8(RgP-F{)T9 z@oVq|{IE#uE1O4^v+fRW#LRFZW(ICiZYyp>=u0Tp7V}^P^TUfq#Pj+GQQywPAM=p3 z_6WpiSONjj9?`!p>DV>GIYET+0Uq&1Z-)i@vq10j)=I46iOUL)kR zrr#%WD>7Pa;&$Q|`R6s_17a$oT5Wl-a(o@)d2tRSByD(X`?h2B7er)RgotcBN_fq% z`@CP3x#0%*K0d+s!3dU6tmxjkt*|(r*WK4#5K0{w*k4gR zFK#QVTv~nb!UEB`+F)+48kpM|Vb`3`oML6z_d>KgVr{X-p`?vJnbRTk8IaLRE$GoM zo{NZ6+Yxap$U~RQeqZ1(JnM&)oAI5CpHxPOZb54!AJn;nLM{y3@ zLCWv?ryFVx>UxOI#W_KQ;W4TCc0bA( zBg8s08D|`cwZ-*=4!KYJ$SuWD#yE!&+zL{2rLL5CoL^A7z2wj{b zY5iLH6dvMtP+~7z2!7wpXiq3}yF_}ipjJ(5*gS-P6DRf1Ix9=7M!GB;!BmpgXx4rS zg0#*gl_0bvjJ6S6mZY_|PSVz!ZC}^N2q~8lI(>Q`x?Cwq_og6>E-*mv; zlli2>mPl-&q|`+@qoT8COiA1N{FF3Cuw4M75m5(-vu938yRQVqOWg|GYw{;LjD~GS ziW~hBy*DPMFv7jYl5k5a^)KqtV()~=K-7h$Vg#3jclzJ$$$AU3!qu4Dg9zQCc3X8k zk)9Ynt1w=FW?|fFQS^bfaI+ED12GJUs32%837QecqUzk$LlgJZ?iRoC;%+vA?5@Nj z;){XJHqotQW5bTdXL?%a}*Q+snp4(G5Ww*7~E>H4(I2%*xF*is3PYT$82fCrb$XyE|c zZPZ-s4Y(|(65i_*1@5t7Q_;FB^F9tA1L7C#`uMvO`;Q!0yVa^Z(6i>~(+4(+N@60_ zE!>(`vwU*&>$8(BgrK#JqHQ0Q-1T7OJspL){AG2OZ>mAo+3^B|*PIr+xcoQF||ccu1J4@qm;ZOgDfmu$^>1c-M62&Q6w z*vWjcHRo1PUknuI1QDh`>U6G?q-)Lyv98f`iCvGgKb;u)s(aJK1pILhBiKq%sSB<= z7+<i+-nqP4+o&z4u@W+TC4`cP)^eQ*8&MAyM*0wOh2+MK~Z-jMA zLpzwdDeECfCF^+*p{ZCx*vZVA6d#4rJ~rJR?L5MH)`oAx7bnH515pkL?mMBhN4b3Zd0@8{P}jQj-ZgSS8`8cUMa>j$GTGaO2f?tQl~KK&i6IE0Ye z>eYbJN?i@adLU+$fY3e2Xr(GG9~w>G-z{EiU$;27BHNi*u+D^3eIQlM5~R|rB}=GOz5Xr3qi9xVS)=qi$UMYyPV?ZBl-k>6 zbhN?ojI_wV8EKp&XuXm#KjC23K}IkSM&r)w1sxeF^6Tr850dEoW3>I(C(tu)Aag^I$Y|`at{#L|^=IP7tAO z>fz%oJ}P>w+??V9Oa#gGNmUuN78)q(x+mfQ>wI+us)v&+90~1<^wxT$>|ZOx$FRIeqG>Z zSDGI1*xMP1p+L+Af~h2}EkL>5iuUcT_D+c3vu8q_5wcg`32%|V)tuvfOF<)_>>^@r1x-F_r9n8&xiJziBfQ zyS`gCR*&@tVtnywLbnf7ah+h%wq|JJP`PfAEoXO&aJej%ptS{X=?JD`euy}QQ_(K8 zY^5J3#F@LKwS6u3jZ*28zwy1Ea#?cP8q^E7?9N>8B>ex{x}Mi2q9FW0t(962Jt&1D zd-7Cr3JEINr7j+d)~Xa6FkZ!rw}SYCf)&&p3LdmnTNDNVfC?rE-F+8pibX`B5v|x< zv#}tE9;~f?gPrz!i-*7?JKvj`eKT)ncHeueV}ldShQpw`-i&8W;_yf#lpmh?6vLnd z#DTSD%v93Wok1)izR!s|YHT9$kKIdrmdeo_HI?pUM*QgQs;pr=5{#gO0M#n&YPB17 zMLHqt1CNv7F_7j#{YnO)Xq;vDsO3*rJX?G2S?-I^jbfdQxmoJTf5n=jU$7GHOS0u3 zH=YOt2q6)a1Mt~Rx zg0G|zTl?s}mu25CW`faSNQK--M~e5g%l|4N&bXQ@`!!Hc;m17E2<^$3&&bF=f%^Ly z7a?esgwgn_ZeOGF5o6wob!2?6m>-WOymq-!orLY4fR!+nv~}fxi+KIGg%#wU9fXze z6=~~!KG#YX&(#aBc4Y1LommIL=aDfQYdM2@p%c`{+gt=o#b~@~=&u*DK>P$BrjoY4 z4R;Y^Q;q!pG?(f{{eyc#&Dp-3)4MBI)P8vU8I76Rt49-%st7%}FWv{q65@H~pT+16 zs4Y;8;C>k`PdMw*BoH5fU@B?rc({n8Gb4e0Kjc4sH{?q$s*y$78UfgYjbQ})Iip=f zZDCtZqZ%W`w>4fd8rAl|-n>RNrjmSaX(Kwjh+b3^^Uo50ZU!?o`5^lp>twY4@KJv_ znlsu(9K70<)483wOEsY}iqY6l+r5-OkDTH9My9WKm-S#Y-s+zU?fR}Ho^Ma$G?m6E z7jgSaD=tNzy*2Auz9MZ!@IF|4<@j`HFSI4`#S{02r4TQ?oI^c5}>zh&nf~UNfZM?5dlS#B3W+9L2{BD&6fci!KzvuD&^4o?pZB9fB@mt~KFp*Dj^8Z^c zctY;@ptnu@@1xI_vY0rn{v28A2hhZ0a7LFQgywR~69LvpX=T*Vskz6BUXQeegXEB# z={D8>+=()7LBJ+c1|qIrnj$-IOt)#-sLAs19ph~>5k20VE}I=6XcKZF+C*SI zP!p_^i9jx_M{ESv1GPf5DFf?q>dx8nxA6mQx>|RJ9B^&CO(uf5ZkC>46C&!?oF{7~ zr`r@8fm~_B7sv-!rrTs9P^NUis*a+n6@aYTklV3*I z6dQqD7oL-HN|Bj1nF!?a9ePp@{dlHL{|>QiPy@NC|16sj9a~1}V(sMBTgTXhXcK{4 ziOHR0)wbho+VavXa!2YMn@j|9UA@^w&N?x{re}X?C;L{MW0Q$M8ISF3Bnw=eWz)aY z>S{tWd2uS~KjIrx^W|Kx0&Oa#^g+Xv;D z2yCnV_1ek}&(E<5(XkQOqKGyTSdR{SJIeAWX4{15*a)mGqD=(0PyNYVW%b^(Y(lh& z;D4#7_Cle)^4leIZHhfpBLcZNUGmI)n@kzVmG;UYIr-W&n_{nnSdR{y-<2!(PO<6B zzuu9rx0qp*DI?>Zp0f0!X*MAjqD=(KKm@j%iNJbXpnb`^jc3_}b;<_;t&36DHeKslar(Y{O)mVk3|%ZSi#3>&#@EVk3}i^#?O#a*-)EnTRiwX3Aj| zrr3lU#zy>b&n!7Taf(fdz>-WEh)DZlmi*Y7ViTfG#7m22%a#qM*o0h&&Ii$Cj%?L< zicN?%5zBzWr(flE++QCLWf7{c|GkRO($V;LbtB(H>vR?g;V%1%Ww;X!`jTIpOSz zCo^KN)%x4h$R`n)xHG2N0eB!0mQAP>oAfn1g1*{Fh z%qR%VK}6373tQhF3PwR-4kB**q=S%&uFdUgSb0`8 z28bV;KWeEJ61fmS%&F0sD0wYNsZHfr_5G};*!)y93cdTRY9dMVScNQK?r@6MDCRgYY#p%2+wIo3Y=q_#Lhm0u7XB{8 zGXENH@Mu>Xp}B<6tB=RR-=pst1gvBus`Qla&zNTu*E>1_x$xaEHUi%Paqk;JRGT^9 z+oANkc0IK2xLSHFTpxwlHD;u@-nw2k0(115=tgX9^{RLBw!St(a|xl>M305P3o*WQ z4e#~318jum5<;(u9!saWCT-ULl_u^QXd^I3uZeC%%Q@51Mm0{i5t<9vM305P3-N5q zj>qd>h#=f6XJobX*k>PV<+Ksm3W4jOUi;h#(ke%3rMZOAYof=}>D-#cNi+m<)d*~rr5~MdQ)~os4SRiuyz7YtHpNCD*VHd|$+RvD zY>JIQu2ZAFbnQjyIS;Al9M^5V?s_czU5FjE&W48O&Lx_QMv1c`(<}*3uaitem*+iomCD-W67}s9rfu1fuOZF{I{1t%(r2T#u#G+*)^+sS&!G`$XxH zfx9J-g};kH`Btxn=Hx!LdiC;Hxb|V%o*6=FX3z*B^h)Nj@OL3-_6e!kM{^0G*Flek zzl#9PyCF62>Ynmg8i8o4c%Vc|)$q!rk#$h-6Fe5~GB7OyONZ4AH|-QzYxSPNV`;Rx zUlHN}oeh@0C2~Hf5hUH&S$HhtcR_1ySgp0X&xO#Mcr5%~h_C4kvSh9dU9J#%C*!en znrr?SAubY88g{Rd8HZ;h{@p+7kNU5k8T1(~pLwb?VLWw+U}7QE zNdux|Bk+6>&sbw4#*0bv%8qVrTHM8`%fJ2hS2-l&&NiT$U_-Di5) z6dQqDC@(gmdF|P<%Dug8LV2+fpPiT|E9~iI6QW}y`e$dz4I6vggy`6aW%n$R6V?x~ z3DL0;Yi?UEr@S)QCPc?ZbZE0e4p=tACPc?Z@T%74YPwB`j*ZA3wMF(G+1Dms;ifL? zZ&PdpuTg&md)b8O*a$2aHFOYBzGuL181Q*yjC!K*6cJBD=l!@)cHJAW$&q1y$$<#u zieJ4*K0kVpO|cQkwWiIda!k@tn_?r7E6w_^)0?po$aSXET)F4)c$-WF<{*Mw;rk)u zZ88ztnl6+rcZ{E zgKavJz1?{pO|1~Y`?k3y2ik;a6Oq+uuRK>N-6rHhw246J#it#0TJ%W6EslmJBBRbG zxqQwDn~)39u@T6XHtGvUo{7ME9BH%PX&}6Kz5+Je4yMn1hJeXLl$Abw{)*1JCQ2OPzqnMqq8- zXw`0}4198UmPo%b#irQLInN)jZkk~eBCsS=1|pD)pI{{~He%AQo$~Qp=h(zE#-RDL zZHkTH=kC;$IW{3WHlo1nJ+ex*IW{3WHiG9X)e4A?jlkL>+C*Ru&!Vcfv1RZRrJkbZ z%TGG9bL=My5zM7djuCBYg$SMv)X6cT84-JC(6(N!ZA3%6veiq8Wh}VQoqESeVu&F(SI{6|JX;W(3+^wZ#;zr-)_*+FrFKl}3hh zQi*6rM7O=7^%T*JK-;Ufn4>W`RKPI?BTSRm2rh-j{PEGwtBa+wxS(~_#KnaG9mSO!`-wQUk@me@Cp zh-Kx}c1lDuf}TOg%BgLWXs682%Bk&?XtP8#%ZO#O)D}q7eknvOo29lon)XZ6W~uFo zv@iWvHcPZqVp{hm*CV>k677^Ij}d6I)V4`{CJ@aC{X}Ule4-ZrxJUl_+5(%HKRN=r z_-!KA*@TvXT;<0clHtSiY>F)dx%h1&))|YIVQ|6zfn}I`2HYp;(_URtUWCB~I{>vO z%8S5#f?mV(lYv~g1BfjHx%6poelm~?cLT9yAQ$ciVk3}Cui^P=g*1F>Zw7w!OJBalm9{D;zUT{2f3^EuaLnd^t%4Y+3pL})JD4djQw{ezy5-3a8u zJwqNuz^*ve9Jz39&x2q9q0CiZbD28;r>$&+UW;(ez>;$7LEQX@CvLdDA{uHQU<9tn zsFhw5-3VNf5urd88085#l^??}>s~e0_v`?Bx3r(GjhFq?6YhM5q;~)yXKITB;U_AfnX+xv)+) zBA{d-ms^ICmsf_WIhM=yP<0aG!;S?ruhH4|FH{fYVt=ehdX13}zB$CMxkfzwREqCR z>dnx0A}q^|(7t5%-b@Lvc;tq6HxbIkAR;UT0 z^KUDq6}FoY3E$WCHGT7s5EDvgFR{pXhbIUd9Ce#dH;6xIrcNQ2z`V2%VXhH2Y>etAi^9( zm|pP}{z?koO|84v)w;}8^0`T}K)q2CTUv-oB%>3R&xA6tTto}euUg~q7qbp~nJ@p17GX$Bj^5Jqa=3btzh3Cz-N%t>V^$YtNS^)t@OU#jZn`WqJ?Oibu{$1 zs1#=69L0s>mDBo$jrDeJ4aW^NzzU8#K#RJho zc<+kHXz7xHXcMubdrs)lYH_|gMEq@j*)5agZEHqJyj6p5rfBuyWZ87|XnqC>@i=ia zAp(7$5iP_)LewN~CM*lN%-b&}HTfVksb+N_6W?;tFMf*bdfx{Uxy;)y5P>q>2vw30 zzGbPQXUfzHw;{sz;U`Mneu6Dcd$@d_TJ@wNI)52z#xW7A`@$uL>#mpf9=<2hcZdjcjBn%UneSYDifns>#})sncTU`~;Hu9a`OEu# zC>M4a=RzPC-|17c>W8vYxpC4&_)FcJd5q@761UJy4Wk-cfxrD2c+Yg%qd5Cu&FQ*w z_3>N;a$zf&h&O-zE=W7@Fzv*Rwqif}>u*h$ms*dPCc>yWv^#skm-u+P{Ac=jiD&S%4M6|1q2Cui=wo6}=yGG{ z%2I);5QEI(un5Xh>d77f1zwXd8RE-p@rx~THT-Vpzpd%E7XMVNNG@T zzD)al8nsnnLOehSCd_do)J-w0m3}_39*E}Om4-rmV=ec-StcROggNNx&A;nVmQ^~IjRE_>W)6{W1WI=!R7pa78Mid&pI4K7$B9q|-a5ng$<#Z(K-S$eoyL`? z+R~hJ#La{V)CAG?uyW>I><`osTf{^l7kl^b>a|FQ-<&N?#M!S#d*4i}9%jNEybl0d zT8R7*N(Q21YlW?V8kz{?;u{1mUR)$keLGwJI|TLtqJ?rI5U#k-04)}aOM7s#nCc_cQjvNY#c8ih=Vz|qIQtDBUBsJi$*Z+j}A2@%MJ z@`M=m>3!br#La}#k;}XbY3+~#zO$9?vk4K%W!{BUacBYGdO|Sq%|EMe%#)v9nkJFU zybsSTD7QLrvJT_+YC=a(k265=%1cfvt-Oqa?AOwEB`XI^xm_5vZXMg9vee zxS5cP)9U696LGZ4Z=vjGD}uCUaWGvvb!C)n=>>aI8wfn1zcw~LuFI`4hoclL004ih5yrm3zTHnB@-pFb zjtDG?R-A`dWR|7(kFV(c1GVD2)b356B}=|DNt%e$Pw!18?l7Tz*rM20*oH#<-eXy2 zIg+v4B?A#CPl(0GdWKgue9p^+GB~|B_ZC`23$ch0JBa%l5$3oN>PA|cul5fNuL_S& z+fRfDPAe|+Mma{DCT=D~AeUJWoT;%Dk&Ex3`}5#Dd2t)Ru}pZVf4FhC+d@p3g9wxt zyU%g8#Qry*5BDfhBa!c!=um5+e7SPuR%qd@I9LzV&_tB3`derv&Ff5&>;#XSLa&l{qs~RHA zn&X?W+t#Q2SkUfgYJ~{2w(#$h-&!O`w4P1l>ZxH>Q^>b3{0BXQ$c5{biJ0HCNhY6C z^Y;eG#p!A_7D!LU>C!~xR}ZBLBUGJ)*tmU?H%zN06WU(zHbh)S>1})GB=0QZ?(SM^ z5p5zGlx*RBr|hF)Cgj396H%TJH*as@tw-E%xe#c%K{TzLceU^)6E_obaa!HiXd)UG zyP0;^yu>gQBJlP_M020((^q$YsGSA>j=oE=5sC|Ylis6lU6>fneuWk&v}Fl#YoVKI z6{!wPn1gpeVx0(4W5J-xBrvDesNO+8&U zB9Ob&5970j9&Y9n#TfN&V920y@flQUI+dZ*rDb%owAgjJgeOYu-QS^OK(1chmFAM2 z+Y2SpDV#ks;CU2spkW4N4Kw4XF( zO!@tf&<|}N@G&8R)2dv)bD;5=!E)%bcc~tTz#KP1-ErXbpzTMGG7ya#3h^?X8`P&W zgCru$lz?K2L-Q^wmeS5Gx6Bh`4`nAy5-U(--%I*hAb*$i-=O z2Y`wA{d!a1k$d)qm=M8!?RTypDND9WmxvbPt~VNFs!@XN<3_076yg_>ag@scg$TJg zt?n5xWqkHie_#1MB9uUc2==@$m`rzhR~;e|O&(i>C`a5($mRCcSMq3$qB9mgUt!{Q zS7#~s?!>?AI|nYl@Q?TVv-kO!5P@8Hn*iAWauMeU!8P=>oFq#(9A%GiCC@~hA_Q~0 z5y-_QsoM+8v5PWzPW=0?59Kcx#~Jd3c=%j@-_2VJc$v5rJAXD_ww*uRZc%lY0k)fo zz}7{yiOBe+sjp|PeO@NyLf!dyCC{t}BG^y3UYDt|?yF<$k*L~`zM05JhT=kb^flGo zX5p{*@9{F>*yXgE4fr;aa;JyO`xf`7xnxnLgk<&YAR>^9wN=lGDFeCOGSq!3LhOkf z5FUT-&om}Pa9VMhH#%G|F)LYpGl2+q-)OD4ebxGbTwE?bQEDu>`UBbo5m-{}dZ6xz zcFBN|$o}pfJ5Q9av>7R}|4jszg$NuyLiG6Lx$yQ+cZHac3w`QM8Hm6fH$r>fUwL#u zcx3om4ijpHHAghv!x#gBT%1;|Ks)^uzwhUipS;CVfAaFJGtFKaEJsa$NAj&9nS+MP z=0En5cpHrnnHyT$-wvLmwH6WVi+;Gx2svfp00S+=x6hsT-q_^vmD=nv5Lm8xFU~i) zh}&HVAp$w$<3oAqG`7j8tB{5czg~M=O`{lZ!SAat|>9eg=kEDexCZA3Hup!$G#EbJk`87 z)tU*lLIjpXr>`}i&g3th{^8bzwn8qmbz9%C!dX*)3NaadK)t4JXvzlxqmR*uPZe7*tw7*K&Vvj5S zE*xu)@>qsK$JPopF%fai)5AaSYo5b|If&qUCbU+Fz#KOs#vMYSFQRe0nX@S7AcE!H zGIWOgv)2TAYWYvCANW>;`ykwLmum`C6UXatzj~yZ>3jW$~ zCe1$OY38|=2odhtN8RZ~U!+_;l-9mzlCLQdA~>zMP@WJC{y3EO5^*zOjvJxA*b`#I z^wFV1R`u{EB78njcf2D>h(lJdWctQ3Oy61>S|I{UqFHpy{ABgiqE_t5s^(fm3-My` zP|j~u2PQ;tTIr5x`pP2fP|n}P{R0u^xDl$|gs5}AXC~K!zurLv-{PU>b5pA#TSs|6 zYgs+KoCtFefww9N@mRITkHDO^oJKd@spPc5yss_sHpvxU4JW{X~IjzD5vm6fO?Q_p@9Eon zwnc~uTajqgcT@A`MwL(oe zt?oZFwK{oWi#>LE9mKLw29`uKT+?)4pC!-bFkz1SW+Nppe}r255N)m>D1&e4L4-;R z@m;fYU;DqCr!jF^Du>Z(C5tTs5hw%ELR`N2cKFxRBfU)6ANUM%yXk%66+--vxc_h= zkPBN{h*$P3cJ}R<=d5FX-Y!W=h3?=o&_Tqs=X zjq*0(x{Y^w@$V{vj}ZSM#2thvM1-rp8=8S#@Nkc-&Gb`wDlSY`&35RiqUS& zX@zD4oC2U$lvbMLk5FYX7b5gI4!*JATMDP08?%i2EOXyKLiGL-LIl4}+^n=fUZ!t@ z5pC8S_oBG}#FAq7IrcN6O&M4ga`CSJY=gP->&K_k&a!rVo6K)%CsTyK9Dw&TfRI;YirWM=DDe98(c|3NH^ z-x}*RUMTCznYIl;^^FjJxAgl$H-8KEga4q7rn$^rztx0HLxNej2IZQaW zb6U-bJkP5qn9jUt4|m+E?qkAwa9Z_&d5hp<n)JlP&~u@gA;Y`H$o)rDo6*b%PLF ziJJ*?+z54Rp%9PLuJ6AstNWOEucmqm?*ZiBm4-rmPdm#E#J!LRWw;TlB=W%`L=JH? zA%fFN6GXd4B8(qIV{e)WNX>Dlzj2b;j{@wlEO@gz!(aM z7UJG#52pmxPpwuKPOB9FpI{*nfzLUjO$4?sBKX->_C!R}eP`dbNmi>Ga`Em}tpFw>_dUu> z?@?at1AOkVPPD2;-n?uCmW5~&f%QNHzZI!93enV78?6+zsv!dFfoLIWHTQ=D#eNDs zON3mURx788DA4zOsBVrY%!E0dR;wD`0wzQSk};ghXF`NqhC-X89czea9B=gg(dT?9 zjWlAy9Ml9yB}XxeAnLdfC5zrlfmha1}(ET99{MVr>l2c>-_E@0-S@0Jymu( zG0HT0)T*CXsT}|!>OW-3hhJN5?fjs(Z9Sbk?YMnIE%T_qh%A($HSt*ZJJp`{qU2F- z-|4M$G?!bReq&*2x?HTe$I|s+F81xajlceEKUfa#mT3*SnJyE1w393M1g-1o!(@fj z=bYb}pFHVEqpO7CG?t~?%46yD_0&15*c-68OUAMtU1Z6lSytx-qYQa;t7g5)nPkro z)3a`9Rl3o8^4iEX);({Gk%$)J?Q$PRMRd53}+;wJnl2S zd&L|?a9U}|{IQ=89O1gpSNACsIi*bakR@AQ zUT;mMDE306z8_3EOCmi6Rw_hwrh#ok0TYB@SZS?Cclv(PhgVdHFzz4Z_+ z#Pf*_v&xWG`$#K9RB6;${`yw7#qr`1O&;q9D>`1Lh!CS+mo?s5WBpEX?@ujlE>}*- zwm5D)?UuUlPEj`qAXkN~H|2n$Yc0NcX63O~G9xG3;<)!h6v&#Ctj=qZ>r|kdeCd<5 z7W<~w>Cs*`U6XBb+WtIMYH9;OH7|3hdi|2vi9)z?S-mu$z*@$Gu&42)FRjTUq6zW zpXOL>U!qg}k?G0gIS?*S^+2v3Uk#GQ=B=|{%9$d~8SYFYVf$_K=@KH43+G4HgT^mu z#kVK%Og3P4mQ{7tNXcpSOk6lM$@yJ~P31n#r0)#uSrkV`^Lp>e6ARZ_?8}1fMzhbU zE1BF@v<~`^Yy6e>0&UloklX6y6S;ki3MJkV|L_N z>;+7x;Vae!@27g~aA}2HzO}7o?L9eG*@29h^V7B9!-QDxLLgWDA+O552XZX7pU_;@ zEGVHOTdE&4WRoO&f4kF$T9C6}%n+VK7MD0yS&A8z#IKcnG z97MD&_I2K9<~9Oz5K*qlF()p%qZNw~n1cvTt4OgXqT-2foU#z{O{J50wX$VkS%|=r zOc_`fA_~9tt=U!q62Rm_J4N#+`9y97M-P;OIlN8DkyyC3?rbiixG7&NAE@42#lzsxyaMp z*^78Aowj8_L>Eb3 z=IAxijleiRnu}~(&T8qg@OLxLkLJQP(PQEIXvX=$9K9yG5g6x3bK#okvG8{@&X0TL zRJZYSKdF_|MmTYPFh{R_9t(draf8MwyYu_4;8V5N*#4PMjaD36|@z3>iiY z9^F2;XYg3~yBTR*&*5YRhFu1x%@{lyK^8Y>HSk#YyBTR*bJ0zr&R)c0;qPXoaotlC zU&}!tng)pxXCNWBj~KkSzkRC{%*%`b0UrFKBw=$ zoPC1F!r#rPJ-S?u6|VMI91%gKx#k$PN6n%f9YLoN%}-S1FS6UvCk-A8)B4PUx!GD9 zInB@~7;NQMCnjhere}OuQ`_S(Vna3&$|8(tic;UGj$_MU-Ia8-Y@<~@QiEt()jzew z(F)N*Jlb`3qCWZ2G8myO=7<*Ju`e=$9(udmX1@<=M8+T4^1stpTkD_hD_gZ)<)F1z zdZKOC}p!xS;($sAUYButDDc_G*ixrlUOia{1!#nPkh9F4BqQvSQ75oP-v8uk>-mMfB%*1guhWqk~ zefP_+7G_%AC`NqFxBFzVifb*7jV?s5+bXV7;|k0AuHX)Nvin+Vb>Rsvt=^s0CQE6B z2-FJELhL_MI7Q8gs1>J|G+k@4FEyg+cH;^SQ==hJcSH-ZW?8)~y6wb{V}iA<@Z>@H z^!`kX{oD~PM0T~cj$aXSE&KVfJTxF^jip%kCW3oiX@y*g`;W+GuVh*8FB>7bq@&-h zv1U*ld3u{TonW68(HXiA5y%x=26CYcE~#^!H5PkGvrhVERh+3gjcctwiQhQ&Wb*ga zR&)kptRMBa@03Sst){(6FNt%o5RcrM0v1TUetgvRD><#fYRj{ymqaw(w={NSCf}yZ zr!uHjk46XNPaRiV8#nfrh^A`%^-?X>x`@d5P`xmqPQ-zi@Stg}A0OazuB#O$ee*M632K7+5Rt&iT9os&vLGa|Zm5bb-2 zwq<~IP}}-E7N*f^XvW({E^YTiG|NEiptk9ueJ?-jptk9ueGkzrgX|?vylrjML;D`0 z8KEtAx~K4KfxL4f+7q=M&SPO(%Z??(C>Lf_=8t9TLu(z%V+6+Z))qRn)*+e^XzSCK zJCB8FT@oWOrZ;kFD;}a5fibT9`^in z##%XJBREHui|E)0T#<3z&4VDN_z!D{D}Xt-o6or}%bcn8y6b*w5uv$o-OUeyD>cq= zrdG&>>uw%Iz^*ve9Jz26&4XY7p>P$(xz=239j$DHp7l+HUa50yPTc%wuKKus#MTPe zLCs~ZgQiw`^)j{6CAqai1g?WxUVc4@I}W{<$Pb~p%pHJJ54){!T;YyG?rs{VJz=T~=ct;aCRh^LuPEyDWr{dGhX`}n>mqIJ8jGW6qdc-~7i*A0-c(`U zV_~^&glcKB=~FE1w3){#cACrBP*IFEP8s$ZLa2M_2=*#}FA%jMSst4AI z&TFaW#jD@&&T`cpz0XiXx)F$?XCI`<+3kri2N9-c*uwbYnMX*g+iC0~g8faf9;!EE z%fNCuz3fl2&{HIBitLbjdsG<)7swN047KhFYT4n`!l(&)!I-Uk4@J)|N0GBbM2KMT zk^AGTVRHx)2e2FTPiJoyF0t8LhZXuSTwWwEjRewzMe&wQ|c)vgy|BoFmD~ zw-3u@Z^^xj*I69d+LU2Y^z7EN4|{J?^lU_+?xwHhNJ6|y+`qUG$c6Iq_Br+%_POps zT%FnD=-}sB)*s|8i{3}*QOPps#%X%~=qm`H8=<}y;qOaSNzCu;G6HVIsUOx@E04H+ zM!o;pyP9UVFwJs?4EAukKeWbLP%VN`Xd%KSUhw@neQRg|5zA0H=>NmNYrj-hOVtDQ zWiG6TN^?Ev`y|qe>=}kuh(--f1m+-u)2dE3BEYK+KZkCFI(_5cRhl9q40|u}7vko) z8CY{S`nk_@ERL2aMEZi3;r0)0@|Ge(E-aU|QhsHTdc@L7>COnPRq3s_q%0w=9-~o$ z&jF5C_DXBhc)b;g;<~T(1}8!{ah(Ib()f&RQR z$0|ipL(ww`J%+gEu@O64hAfT&ifHN^ySEo9hqv)roiMt-kXPA6=8wW$jDK_UsB1CXnahd+LZ&D1< zLBzeAh`n&ts75aI&K2TKiUB&9xQDxBxDl!(y8H8q<=zog>uW?R z|2dBcwL(oyAKtBm@JB%)7klJ>^1mF5qlpSpzjU`yD?*ea!W=h3`3zHhwMt8K?td-G z_Z3B6Ljx<}53ERGs#A}UaP&Mzr8XL*X~i8<)0XVxR0qK4i{+{Ii7 zl!0jS!X_EzhXg@j1J3d``$g?{4YsV++edE*xWYXJ%+r+BbxFgb>KZ zY2{gJBF5D#8CpcqJg*QT0)0jiO}F)APRk^lOqlIHs5^VtoZA|*I5H`gL~#^vwoak1 z8Evn4>_J3eNp#XjTFoSlZfR(0g$UG8h}Q@)i4eCE0=YP?{9R4NCloP#C`C)JPK0|O z+#%sUNQlmaSVi2$#%~NE7pK*&5hkJ`#WI~waZK-gc&Qh6WSmy_$>5CzWQQZfK0@5@ zLLe9J=OWQIe>*og^a#c0M6LM#fxwpa7RTpA4NU}caa!GLU?M8j_`?2*l~4Ro25N#O zMdDfxr)w=MYj`G7va zw&L3s7H?Q@am-OOGASaE3$>*^14Y7ofudlpCc<(#t!^DcG~KE~h$o1<8xiKX5$d)L zik5b0aIh1-M-8MtN9l;>J0SWr$+kFxs2Ob&xj3!LHE)fW?im_XU(lfpEZ1yZexFeL zcFaKpdjno-pJPR$ZH}U7o8M5R&ACL#g_@X%Ce-Ie>mTw?CBi<(RR;T?JnzTECvTz| z?jh=PM4*Q3KiKH;kQIrx*?=NQZlws4KN4XMdKsekp%8r%{u2z6jG80^%SGL>Bq177 zw9Q*6(q=Ld=C~2ci;_Gip{5kxIDN}WZ8eYD>DI;+XB1W$Y#*HGg;+v}j|g#&2oWd)(e#c=abLX~MyC}Z!m*1r z$I(MK9FVcm5v~k z#f!^QIjlR{eN;MsL^L!b%zCJ@Se~*#aa!A!7{eAX{*s(?6vC~Q9*HD_`y;?eY0~~uI<}ex&Ds3Yidm_qUbhJ9D#xD5S@s7I}v+^J+m&;T2Z%q9)v21?)N$OmVI{{f3bxKl*b-pbJqr~CzJcoo+0h) zhTgZkKI&UX#D0PoyJTA7BVBASLshP+RelKN)o6Mg#_c_j`8$nv`ufa}!G3UqMg^_5 zt0P{GN*=v=rJZtom#`kFJO4iQM$i(^_oI5W*f=G~Gd10NX0$HKK(r7?JlE42WF-2g z6JgC!che8jQ|)?Md*V)WA&`qbcn-asX%+Hue6{5G>uCYvev}Av+z91&NnUiLHw4#G zJszQYpjL=xKeiKZ2d$^}aNMPeSD$m%b3`B)wt_u&^-S#=SIRq+TUYtP=8sT)zzBt= z?{)8*9g-cZg*y^qyQB9Z|E_#6$wMJ{G;~XWO5u1S>{~>eh}Q`rh`T`)geuq6>aSd_ zn!B{Z_CXElrou9_LpM593pXGHa&cODfSQQ+Sec-wbL%XsSaP8$*LLa?V+@{IM?KhwucMAN++Z~c&bfMn4A$C!QC zXRuF;Osm{&U2G4~=C22>bAx#frixr=V{oX|O^?3$!-CnC&21gDihuMkry7WRIMwLOFgwc@nW#Poygm~+5s zD@3rrYo8H8t4u~OS6j7QT_UuO%Ae$tf#tI2G8T==Mw70~BQ-7s{j4 zrO!)*CQ|vkiIB@JL-}?_;%$FL(YXu0`hbBzA5Qkp+!`0OA`!Zu>0Kt1ZYZ#C3q=|1 z2m9l1L5rh+n_4ALej!{FGmdAUwSSlPuBJHHh07-TFo)C1|D0t!`_4M6?G2u(5rH{w zg!1~PUB>hOc>mzY+2_cn4`p#$ahV>{eJGmw-_%Y=U2TQ^jO`}GPoLiOmMwmNxHS=S zaa#F?n}~upn)=33T=Mfoh+xm_-gC1oj(?75Ax@qtqW6g~s@(`Rj%oHG8DpvZ{Y1#c zY2|-z%HTNFYbZwbbs`+QoL1wN{WcE>;@;&#AQx&#{n7XDO!n31n_Ex@d;0e1y58bA?kJCJ z$W-$NRO^FLYOZuQWyDkD_gjh-@Op``=GZ>in?fWIq9AcUK!p8)XcMu0Xi40CraHW5Q%2~E=6QtLWBrTD=w^`iNG8;Lfs${8SToljWQ68txk4uiW?rF zDC0j9Vf!#bm5Z$qi8MZxxYrY5xo(8&8@d;VT6Z+H>=`1If#qVWlONgAt0^0(2kB0I zW5vO>1ow>OhCwsg!5rs3dlPl89u!Y8zfq=DyefWRMSZ^KP z#yjL$8~df)L5RaVmjD+&Q6{3my~!zRB;qrOwMDf3mTK56l}!e1J)Bm)Yv#KF?!w*g z2I@8%AyT$a@?PouNca;X+|lD*4g9;l(IG$UptA3wrI2E>9vqykZmviB6{nSDn-K~f z8N2*N3V-{7Ify`Ow2cU;FJ&a2&EOGnO|G(I0ySfm5UnEPXAhU`wk&> z!-jgV#kVkidsXsiJ>S5dkJWNFJKP|kTvX6$l5g`Ka;Xt(6x>#EreVo>JSp@Fwn(4%w zfL3&)-(R0&c`EW3TiAncE>$l7ZXg`nlDl=CUc;6atw*-YsZKlO6RsUHKac9U!`(97 zs?wuf&PC#WoKQIXAQ%6xcFX+KmJ7XfsaE(?BAhcgt!@gz8$^Wo=h;Hx^X{CCUbXBZkIq!JBE< z=O%`qnU@&GIUH|m`S%F58X%em$+Ava9JPmi8n8cbKF76$PRaTuq+L3m6#ke9a}a^I z#?X4cBq41KaUXXfkjvDn+WQG)%WgYr~KrVReq=iSQq`TzX4_}YVh6_>1gx2n$XPCWw5g@`A1)$o7YkYf}fdgH`P ztJTgD5v>+HA-n!i$-g}}E?HcmI{u^2mU7~sF>gc`%Fvqd4Le#}y77WW$tCI&l)?U^ zipwp}j7z4=Mcq9XwgUNK(H`y?$w*r`NY3n5**}atE$aXBs$5y2ivK#rB}>d~=KRij z*k6nA&5`!b49n7O<*{^n;Wvw%y${OR^z_^E z@5<+|SMhUPGDHi}^@9VMDt;6q`uFNB{qa@(9G48y^qu9oR$0+%zUkxu*?mA&|8EqR z4AHbk6<(9XXI^}ZAj(Lr6p-6CRrN2SxMYZ?cZ1i<2iFsVqEr}&4sFKBcSIF`L5fSp z>Ath7_#<)2R*rt#v3{Tooo1wUHymDHNHzA7s)hQE4J0cWU@kbiUUMDL1BXP;r-!?PlD9I?r zvnc&31afuw^ac4zmn!~9T(UNAugU65qrFS+6QvNyb>TTFhpve8PqE{Y{kpevGVQ>_ z)LuU1@*R3o4&EH+=eT4hqT+=G_T8U+mmVUP4Qe10PR98;E*YX}UlKU(^f@Bx>}({P zUySn?yf{lDnj$Gb(;oP?KeA zd&tzUs`?Ws%2;ynE&15BIRDdjl(EA-ehISI7L61i%D~#9JRv4Mnd#W{vF82jwUuv` zi}!~q${3=BC^BYuP}$oNf$f86y62_T8}==N{O*kP=&-k=e6K{jpQDT+n!bTNU81(~ zT|xxb7SZHC`0UoyKMK)Jp43(u@AQ-{o{aOCrf6C9CwG<8PsI5-_7$QjR^#lH6!NFG zqg3J(b%AEM!2ja>9G9%@k?C?)gE)UZJ1$vj+En{)TD$?JHzpD z{26utqIZU#0G*yo5AqF|lsaJQkJcd^$v z9{r#~@&4X(Intkd{m|=#Ml;tLS<@fb&oK_&2+UElHgjFfsp;>Yz>%8V2+Tpm9ll5X zE1G6RL0}FdZf<_m-_-v}6a?lVqFs|({#V7~C|M1SDG zvM315LB#c2lKd0Su84xb97OD!o8+$<`kWA3AFq?Tlt#&8uC*4|K2Be39`8SDM>3mO z@}FQO8i|oR$#p*?*WNqo$(|kJ{gFs!BWfK;ZAda^yJR3&|G5ohx%cAzkw|9$eZEqP zvgz{(P`V>m@t2y(^<(4xPuh{pn(yeE^$KbA0gZM=wCrZdXdK29@r@gk;H z$knPqTRCk(y#GEslG*DQx@5H@8ADw%kZa9r9c8;I@%}+}Br{olU}}GAUAn>5sJX$l zxQiSxI^G{Wl9?V2x)rz$Th&eWKk?G)DB}uqRNpY-@uSK94Z+MP2+Tpm?R`@G-yRA^ zL0}Fd-n%!|e z*Kx{1ME!R78MfAOg8wuP^higA5RFZ2boIxb*#O6w*@HMHN8Fd_9f!x#Y38EaDSa!CN*jSWddJ~LG!ESFTe-J^jnG`wZq8oBW9c-@ zXh?SzzuTypjldke<8UJi{$9+ts{V^MLUReBcN`u|r&-2|R@c2%Lv3sX=I9-V8?mF- zS?|!r?QMkSqCKRu7x7s5yLnf!=AthboV|$0!r#qEW}1uQ89RFskEMUl@*G?9Lb2rV z!~*83cA-#TS^cSa|F;y$40p2h2D!J~nhHf|hg`@+2w3}uR_Q5EwTt%;up^lrxcPHx zQCc}qx>io)LV4V_rsYnJU3?S5_ninLbH;pczubFLb!~Bd^;o!~lGS<4NN>G$z3l$L z9KDLV5j|SJ>K(nUuZ_@Lbcd0%zIrVD-MkN3a|xkWQIDn5T$2a0|4O@Y=RjKq=IB+_ zjW|7fTH5Hw={7=h;VSB}@OSe*WcOOD?o#AdS8J_}aPC9K9KBl74SxoLw8~LhX)c;E zoE6z)>2z+*b5zYSN3WKMc;)^pnQW7>hSRwa&i55|AuXIX%xk%(M10$b%F+98*Dl;5^XHF`VibCQwl+9x8{u-A9U)gQ+D?|OpYqY9SVoti;= zQNL?1id<8_*dT#D=$aQMem#};NY<0mkOXwMV-1Q71*Ww@d z$V;>0{lC7(GWuOR7i>ZGnCz+tas^iIgL-g(U}QPmP3YZ#$HL#u$a0#CMxwJ%pnK?H zk7C}^tk-alrPK753VQ|-pt(J)=61cFn+Qakk>#`|Sgyx1WQaLsYJ@J=9bi9Edi3EQ z&ST;4W@I_Nrji{Wc72$(XP=OoeKdls-p=Yww^PE}E(FbRAvMEkF1q{2*&BK+{N0Q! zr+dm{X#}FF%Z#cSq}Th0yyekEPRGbG$oR%_V$Rq|=Dz=OgkL+5PFW zB9Db>bdXu6`*g4N8!2!KU?P-FAJG)S@npqJwg~gNGoEwd*#w?) z#MX+}SEUuAX(v(Qe*3N${;~#TFhZSqAUYyLc{?GZ!`#meghJCuuhYb~I`ds&h{ztb zMb;@++24C)U&*s}iG<4jX^Z;P4EM*rVzqyydH04(E1q{N9;@WP7VISvO{V~B_pV8% zz0XQ}@1t86%jLDCES>4KxtdNglK!fa#J5IJT@0;|i|eGVoIihe&ADL%>w#R@3X!&= zvq2}a9F8S@R)}MaF?54R_;W&Gxy?JSmnG^{_3vspPI7-}t%|*P&0abA)RL`^^J-S{ zcPctuA{y%?#L>S?2g&y%yw@cI(J0TTN0|SGy6g7@JWG9S*4$*eqbU3>$vBd|-D&p= zXO75DuUGLepEE)pX}CpZ@2u=^dUP=L$M3aoU9*|u@IFLwc(WSqm7-l`|G7%(a{RtS z4i~S9W13d>&nY>O5cgdynbm=MFhHZEO8;GQVe88N(kCh28Oi|JG#jt%l{%KRdWp0` z1eS~FNX@AZ;UXjh^+m3XI-8t2MG)k}63!tRn1hJ4Q8e0fd(%W9SMg~_9WE2`N5xxe zKSz6p$+Ty{wmQ;gzths#Ze*eVxPOq(AlX+2Yl~bco3+wjzc|`C9eo_%U7*&Qr%9{% z_D)7WgZmP8$#aJ*`JaE6{2<{87UI~Om4o!%N0`3=|Myr z{XzfGHRp8n=dozt?^n_6<3wuGs~4<;Adl=L3uXt&Mja+wwGU?iJk2m?p9%-Z+41gf zQd_yX)H#=l;F{305vFIvha;Sym3Fts`O|Mqk>+TB#Cp)VN0^_%2R@DSzq)CLLvcz@MubL_FJ z(A-jV_Xo8VpXwumpIX%lh>jp=u8PdN&{kMmL{rqzwOewt%6&f6o+@qF31{`eaV*52n+l}fPlz2ZMDyh*^$dq-@zuw>gY2VNHuw2J8R|3? z(d6&kZ|Ew0Herr%b^68{Dm1s1Gs98Ze4=nwQ?eti8{I~RRxbSBhcRd)(-tp!OL5NY z5Y33_HZrtwA(|0rBhwZykA-Qpozc5(R2vyuxe(1V&_<>$TWIA%G$YVPrY&1&d#@f<$ z5wwvYnh_XlOWQ@zMuKQYpoK;E6xvG=%?OOOrL7!j{V;7O_K8VUdx^GqAev?5?#S%- zC~eE|SeVvmMxea}Z6ui1y~zlU1EFGVVY%8yg7O%F_7ddMYad!zDn3{%RnmI?v1>iY zC#u|-Lvr+6@&3@^d9+)4ZNP@$Zd%XzyKr7>Rk_H;`(16{9krpd5uZ~b3@+NesE_Ejup)xo~w>vhyO4ORul_$v`e#ony;DE`2tYpA6)}l{&Tz{nIH9v%#ORdg% zv_@fb2TuZb6u7>x9gSKJu@Ie zbKy#z9|G5QJy*IB$b~C)9z?*dIMp1vaNW&=U;v?T-NxD3T%Db^vJrZnFcErf&#eb> z^B=XlV~NML57AKb03&ea)Vd?ujnL~ImWw61wL(Pn6%J(B^}s&IJ&N9aQiT3i(OR2GYb_$!cLfnD zP0^qzs#QUX%*D~U7@=~|d%%>z@w~Dr23Gwjh^M<%@pC*cL`Sqz(L7KqPOGyCKFv@q z5eA9A^6D0vYXDmoW)shVTCTn|+z`U3m>Svwv*TfC5qzw+;Mvx=Xi zbD4;~6glf@ikfwh2y@&B?aguhMoM@W#iuGnajH-Tr`1`J=_PavMYUQ;h`U?}p!W{NJ*f^-F zpW}I^i9H9#}!J3=?QCB~gVWOh!)~|iUu{EB0&|Uh)^gUwK6@!F0N{m+MD{L8_gw%aLZ7=Da4Rh zdZx@I#Ir60ms@3QRX@k`LNv85)#F~O#|R?K;k5F{G3#-Vf() z7uG|iBQ;kiKg^|Sh-lQ%L|_ggIIZerBOKpwH$t76^Y1EcM(4tsyU~Rvl5d|KoeS^T zMlLLuwbBUIi@eEg|8qp3?wr*!p_#eQDXTg3S} zI+uySSrlb(T6r~^v*=ceQ1QdqjiEG(Qh^Bc`9d^(l|>P!_EEH{vJ`cSyqSbOD%t4Q*@Zw&5G_Ren!{6TP@mJC07ieHhS(xPO#XOp%1}bQP6*^;Kd%oS zNbqw!FB4IhqH}Ge$XtR5bI>ykTUvD{3AidBP2tM)Ffu-#BYAuP}IRQ2T< za$XL1n*2!ULjR-SVvqRNI*XW^B0D%LtqO7fMJPnDPh5vu z34V?rg=q3Wr|4WBinBF~2-^oeui2-%9{Kj^2w*E{?8ed9MFfsl{$2TrQ)FwB@gSA| zp-TpGp*$hZ{r5hvkGRJXp>*UjJ-zRx=v+lA-{*UV2ocC-dKNDxgrB&lQ9Lj9Anru7 zK1TpUF4MEP6~zqOO5Eu#1j<7+-P$s8LDuUu+K13+$F{=$zR^g!?N_t2I3 zLEOipAXK@C7Gfwx=c-AOxw;UcbiP60_J`yB96u~J0=e+c0Tb~m#df+$vAkMSY$u#c z5P>BL@oT~g`$h-;(h0TVdj_V`9Vs09%0yrUFzf?tU2H>Y-QVs^Aq!5JEIEb@M4-G# z1hB3Y%WF0f%HXuR-v`l=2w*LVn+bE=2z5&Z?b<0OQU!{K#DoY=D=t&3^xP;){Rx3N zZiKqsf!_^;I&t@5D;>~mZ* zu>a``>;i35)twyfQKCj7d(r)KDBd55=hYdab74JDLlfbv^joMbMYL+eQLUgX^v*M8 zJK>{PTY}JVCUGjX;k>kS#=ht*cs;Wh;5~O~&S@QqR-+K{xOj>j&1! znDfQO%D;vB)7;*YYK}RGFl(Mg)(_8&t)aOt>j(QN7QR2h&#|`95+cM1inVno#o01{|Aes>(W<3z0(h-ERUvXI~hjmvLDE?ig^G8HOGeX-kuq;livBYU@DPoL@ zwS@?`R{GtUWN?237>Vu9Y3vWx(#)r0xVaIEi~UVhoos+(9mE_){~u*v0%vpe{{M-x zwW(AnnaD2Nn7I?py`4D-Ng_&UktBuL64}Nck?iZt*!L`D=8iJ=(k7%rmP$fG@okS5 z^nX8hocnp7j~TDue_pRs$NfC-^E}Tv=X1{UIm`1f!sH`}=(%v_A!t*h^1I1j9$_S5 zv>@CSdX`G^MiMU~;s-Laa>v|-%Cv#}Zt^w{MlhA+&A3vahgltD1WUsFY#v@J*29%* za=Z?7ivQ8S7)kC#9ek&n9ycph$<3f2{;Q_j|Ipua2W!U8Q@P9Vo(E(NnGkXgePWmD zdfKAF78>{W9y#UT^L|XA)CVyZg70A2x3+2lu^WiW9t2a#jgjA8sHXq7LLyEre|b+F zzKFv3GO~Cum7LQzIw2lpr}fd8VU`|5#X4b^?81wI|Bk-uZ~EP*s zf4i%F{=K`~=kpWE8%gZDGBP&Y4l4uWN#ypK(NL&NPqd{H9#^6K#vUGAE~DM?3Vb5B zgiN_iB@)KYuc`m;FA<0Xtro;jgyxR}!EdVEmAM)B{3KG7pnQwXwC?^iDbreTaOYSjq^8nv|hOGWA(!Pgb{o{(sAn6J{2nUNlc*G zju=ZS;b$bVZ|ddd@5wPU4ofBHkj*U0{2XUS-BY28D8HSD2bare$2pv{K37Ht+=UGA z6Se@u9)!?c0#Jk8%&^Q5z8vW{eLV1a&REAKZeHH#dD5jrEfE$N4v*%}aaQ zoZFev*m?bUoL>R)Iuh5Sy8kfa91Yyt^6Wa|73|Q46e;*P*k=vPOF0pwCA_LTGk(kPRpVQ2V z1#^^{2bqW588ke&B>3BWcv#?$f7>UvLE1@hA|JFk15yL74l_ygpOFqbP*=24u7#?C*1?GGBByg1-&K zvNYcVk~jBV?Qesl_{J&!-K(K^Bu0qcZf?{t8fPYUf3#sMw65&2`qfY({J$Sle?pa8M6_AKcZ8T>p(WyOrfBt%_ z#C&P7H9g#*zhbVt&!@s1Uv&FR5K43D-2D95+wOh4+B)ap=Ki z$@kfyFL7Ex&53+%8#HF;M|EKQg&#t{BC&>yki2`N>T(D}WlPl!-)bYL|L8x3#8O$B z<2%~i6L*}3b#IK^)sMtVp_YkLLG?2>^;4$432lvlk#gI@o34TsEonp4R)O5x#L6WN%aDAkysDOn>Mh04rJl` z<7QniFy4b;DzTO-zg5eFe~HH78Ayy^|8R`P zIhoe`5`O}9f(OA;G1_t7dthJU@1U-W#8i?u=KyTPn+a3n3)5V_cg*|2B{|LwCx+%2 z`%0de{=@ChXuRhY_T|4&60wRrQnA-MmJll}W%lJ?2SjTm=Fa6Z8c`~*-j{y`sM{ei zmE=u(*ofxq(*wnKKjMEHNvu~hvq;{|0Q?3!4kP$IXS9vjx}$j113x3=-8S|x_{Om`XS9vDXV8uD=P}pH z*v{PL`@@XqjCP#+w!ZYszz$`cle0AX!QxqAYyxrQ1{R8r*|OY zRLiPK(Y(+^cYZSDUj`xhiX&f%`gnHvt!tRFK|K6Pgd;{=Qtmgyi->B)JY3o3hNG)r z;d;m|N<+mui5zxRE4N(SyouITOp==y$iL77d*}fvE3HPd9)}wWu_@iv`2ETHJfd1v z_H~Hh>ckdK{u!F&d)}MYUqYnx}`VR*h5jpNMM3Xh=m- ztr$`3`wTUIP^x|tQLPy5IE&9V@R{9LMx=Z?T*Ze|byvh=VYK7ynOHA=IcnCA?*hR`$0cle)~Wr`Pe6u^?munVUg8uI3)fTAW*}XAPg1rbDW!ppYZj;c=ZYpr|+Jk zN+c%hgR3&s;+i<`d>}FYPtst1yFsXK+o)0sKBIH4Ctn+JAPoTh%Sm#mvN zwGmtr5T85|Ukp4DZ_7dqnlN2Gdsnhvv2&Qk&vB-8ubEdFJU)iztRYLn5@Pf@*dS=u zMH#_Vk}uaSRX@0TGW3|zpmXpfc*yO{+vmPy+143n)04Hj+vbq!Bn;$mm3o^XjF6G z^Hm*Z#vejm$vU<0GgLe#O5Thycx%6Dkb`JCVTl0dMHs3xYaUP!%}CbO`pZsSugUYR zxJ!lyQ?;-Cn98V@qF4Mp7KpD)&kR0`_89Kr!BqSACaG?{()5f-B(r*Ve;kP!_7@OL zRqN3fs>|RsJsOeBY{Y@3_eLX)Gh$Bh2UPKyY5GG%GGjEnG!J(pnK7bjo3^UXk~DoK zBAGGTaaJVM3(D#T`mZIGGyWO1ZF!15i%4ciR<~DI=cVZ@Mu`u)>(gHIy$?iHX%C{R z9DH6S|DK|2A(GkJue+*IpQh;@kw|8mBAGGOk&J%o37x7l5y>nerHA@zN1A>m63Hx| zBAKxsED7s`wT%3`a&R*&azcous`X=Mm4k90MkF&vW1sWvE%C-ME%Srj-Ym|CxMvYxdaQZRkDpR{cbeXUNM?*i z3sNLAMsWQw8hb1gX9scTE{wZ(0oJ2^`{z~L_tW$ph-AiSc&DsVCSe1llDqnhU~L)g zIB%9Jk=GnN7J>)YPwdUfDtShlu8&A&jCPz)EC2B)iA+o$QChz16)UtRVG>p?2HE5lTi z{&-2(H>R)&aDN{ks)}FOk4hvdZ%K#9)eg2 ztnKMK{nR5#>H494;)ni*f1POU9)V@id2*WWk7#mSqm0J6@Gp7=Ta}CpXBD$~>>vG# zDtSIlA4Oyb8*ymHT^n%HFx(COim8~NjhNdz!*@Rr^MT-c-}2BDwRj=Mj{|c(JZgT_ zH$EQ-@#M*fuEVFPiR;t!jqT};r*pS&IR7mGU!yYsG4ZTkC_XiOU$VLhSw{G*o!T^b)JL&~zA5aP!( zgQBr{Y#!DVLhK(rI2!-QMp#b>ac{{X`jS{e5vi;vgm`cIkRsw0Sx*R2uIA8aL?v4) z>j@$D<`0cVZL$&86GE))H0)w*r~iIWvf{)UONK2A$$wNPU4K| zN_#NEP~}g#Up;t7y1qGA5JSt>3?c$X7?Ch61XJ~lc|iSEH(dwn$mlbnPL=rY&=ZTJ z?=lajI`dU~HMM!VZV`zSH|_0gUmqY4snS9))#tx-Qn$2C*N-=r2o=p9+7}OBuVMIn zwGd1dd&T2wz$5AU`R4?Y*11XW0q8N$qX$!|jonn+N7MCfkvMTXxBL~{4@6lHf~i`L zd`k80oUSWJ;>7LRp6JVjRINNxG1c<#da0&u()GwloVb)zLxbkt1lJW)*<-u+t#x~Y z+nQ2RsnCcE^%%$_70GulRG0{1dsTgfbl|FDxbPbWwg(5P92P2rO zdY!RF#EG-oJauGrG~hmFw;&@o|=>I2)Q!VQe5Edo!sWXae*uw5$0ac7^HB=`5Rm9CDH^+c?oTeCCI zWcIf~hu14cqQu|}&q6Sjd}6M`Yk41 zC+?6o|8~~_-)E?+TRnBfRIzWbQVmO_>qdV{3^(~(G-tlQ{l!RsZhvJ9DJF?+qKGHx zXrw=W;@Ux81iVfAGcQK^bE(j}Q41+1iGR1fvAa}k2gM|@O@tE)@BEWk{HB6PfBeL? zgS-fi^ygBs9Tbzqzhh@VVcWe;P8Y-m^jb^CFRQeZv6is99lLE)(~Agx;@UegN&Gw3 z8nR~OkGU9s(4|6r&Ztd=I0VtWv>-IkH=6Sk*WO`7?+^dW5#Q8U@3Y1dt7p^?t835` zuRA6^QT~kI56PRJ=n&Q7>%aCOvIcz?T35PvWHw-{$hB%>lK6M*z&1P{G9$H1g}u0_ zy%Uqfzhhtc_fp|m7v~Sx7Kur6^SBj7wvUtXUhkX2&tIIQT)V_ZFgh~hgv^ZNO2XyB zvnunDNCKgA7vtTzZNRo@OcMWY$GdZFgP0_?EI1!|2cz-sTm<|>M6C?e68gZbnrv+n@Tm++~W9*8r5iW5t8m()E#U!x}$9X&6ookWB zB)MqY8bo9VjJy5Of2Ak72>4Hl+HEmO{5$N=VU@zhl5yMIaa>7alKA(?jdh6`7UAKR zi#rujyB*O=qIuCA)X^wo(f0$p$Znp|B74DKffeiSBF7|g-t0lcqulW;Y3Ik@oi_V8 zvYTmk-w_`tx4_DfT}$52=6z1y|HM3)P%GFMH9NqAcz4Acs=~&8`p5DGJELZo6j9&) zy^#Ab5Hmc8#P_DCjw}1=)pZ9bM#BfhyX_NJA!<)kMD1aznzx*$`Yi0H?_Be;Vl+nl z`;O;o@L1&G(Z1abb!28g{mfM_D@MbgQS0gt)c)!H^v+|5uSRHmb@h6{Hw994^yrb0 z@uAv0xu1S~QGdl~oT1;c!&euGcRYy2DO*(=^u!z6^jC~77)5yW;qk*gIl|-0zr*MI zj)KRP;Bo(u`D*U&6g~E_iHi5Om)yHVok>7@yq1CX_^rdDAnp!^C!ls2!BnGfTBdGm zgZ95`I1q`wz&U!E!9v!eZA!kzYj#xbQ_gkc@Y){dlaubz}STpIf924u8rKWol_=iaxaNHRw^j{v8R2V1<{070!s36_=KyFUv^K? z>0gcWAf#u>94LKjPQA@)UDIS;Ye9w@UhX~h>)pw^VQhvv{roz$x>}kp{p(CRdcqd>LKN)E0z#zZ6zk>UIRV8 z_UOS9+EU#zBt92k!I9HDr%4jAUc3FAqNn>`_ej;PL2;japUceI^Y4#TJDfs!>>y5v zRCrGx%ioi}}-cPL){sc1D{77!iBR=PI*GvaY&*nA&uBm&$cg_2b)6)6V}^ z)Qa{irkXKux0mH2-58XyUG^ZE`_W?C)SWd`b@0kJl&wd_Q1yhvkSY?H zl(ZFeo!Y7Tiksh1jCP#<3rge_hVW``(1;2&h?9s`t`=6Jc!piU7Krc2BwPLyV*%2`Tz`bs4E)w!>D4{AKWbF8RV|(N^0#ZaL@6IsHMTIs;$>f|X218-Q{Nq|7>!#Hafb`F zUf0a~Mx9Sh)-$?{P>hyZjn*#mu2hm|yTo0;ZaaEh^c1TXVZNi7A8fd~)#4M;+gGB^ z7eBsAt;bl~c=qdR(y^85x#oC3etJW-+_hANV^a0fZIjUE|CaweSN1vQ0+I6J8g+7G ziatGMf`#Cc9B0tU*CVIyWrVX3KfbAc9-6AFy*tH1!?(zSb8&L&{)7j?v)Uv=JI?2C zzLR6l;xG@M`xuRp;niin+2|#>HDb-@Jonj%%-vOU&B;tY0Ws(9Me4_1sro{hDT>ce zIL?T~?8rAK#>D_rZLhRM#SKZ-9p9Xyei^b@HONiVbzYM5aL?^b%sB$7t^$IoPETH{ z`bpZb9#WOQkE@O@gL#*?w`{0!Um z^XM_F+poUAz(ejRGJ<7iG)}#I+sn5Mi2EQFOLF9urK-w;G`;bYxgMzwZGOSGv=Btg z&MVZn<Ve0m zs>N$RRP#5b=@rZ8Me-|Wr|Q4om=^!(hf zfROiG#&&~fzvLTLBbzfOA2=4T&Y-dMoaA$J*&AhnOjmpdlbH3%R8=YUkir!Yu(+~ zm?X~gK5k@{vylB--f?BL@ZkMgclR|WiSxXV8;ML>$bK#FxH4LJ@P4hk%gQ^hj1~m% z*Sfo`yyMDfL2%4scbAoSTp2A0j#=#Pc_EH_^z0a;1yT4e8Sis3S`Zww*xlRWeJ(}| zf_KT>Juk#N`~Tb}<9#kh3lH8UbN9A*pNr9g;9W9z&nqU0^KMCkD14W!;GHnLWG-3| zyi4Zpc_Es0^n4Mc1yT4e8Sis3S`fTT=I*}mP7I?3QP`fBz0c)+&x_hIare3G-4n;* zT{3rXi}$%Mw@c>kN5v#@-YrS=z&lvT4i@iPFB=(M(y%Xo|hA|cM6CS)v z=I(9ro)@=`AUI~RySK&rT#Ob3$1HaDws@b5(SpEx9^EB#_qn*$7%hmR_PpF3EPMaU zj#=#PRPnBrz5iwJdAa*hF-e?vOA@Jg&x`lDIPbQmATF*Uhb7PDy8B$rj}Vw^d2fr~ z3Em~Uy~Xf||J_$THUqy=Jnu@W_FT*Jt2_5GKQDr*c;+^2rreHIWN&8LtYfIcX_sS&w&)eBZ`o+Ou39Mng^H5v*;xV z2qk~aoyT)~q_*F4w=8?yb!SoUd(H@#if7TwA>7%L$15*_sd%n+$H$9^jELgUoT+$D zyaXWtpzPVwrLt$vXkA4R?ikLpb4gyQ7{RlF%a1gV@Zg!urLt!=TPk-hvZZoM@=C>s z!e<|n3e{!zE4Ck8%iwYdm&%^cqk3GzgY5^`GGHEZ^R|4$2UB@@7=G|uizvr`Aj)ysLiC8KkrIh=%sLfFW%fU$T%$QlA_#-Vt|j8| zW*`RdJS2W1cWoceO^L)4ws~*_VlLNP8q~S4npgnJesrj3ziV8=gWNIWW*9)j>JzGVSA=1w#!vv4wGRnIaH-5m74QhxIGO6YhwboruJe@QrM{b|s#0Kg1GV zio^)H`>aN#M&b!G8oK}#9hl|yB1|n{&61)6GeSIf{D)OFi4JU`oz0M{FC>!0Pb6>d zxZ6_ArC7gQuH3J#{{?md5FMCHf}aV*6MhY`geCD4FCxk>(iPuEFO+-R-kxa2C>dkS z`!4|Z#t4r-k~brc7vVTX^>$OPAj}&KziNo@{5j$~OXAvu4$VGBfLBeZ_LLO>zB(FZ!Hx+OpV0)wRuQ9 z;lB{;R}v#wcH7sBLV_{_OJ<{>_0;&YPq%+38Re&~ruZjD)J}(q!FXw|L-h{@wNd zdMIBKQ+auq^Qq|V&)=WueA+l5i4l@FRC2=frwYjtkN=L-xpgoq6(hudO_v49`oxHr zlr2?_Z{Cag(qlcCifbCtHvj!7=L=|#Ge4G8mL$Z_)`O|MdYE&zjpCg@74wUXK9RLC z?z}4D(wSAl^2AVa4`=^Qj??A$!=aTxNMZz2 z**up1d^q$J%D)$h%ay$GY{h8AO6idqicxjL|J+yCLa=8mM#GCo_smc`AZjCV|B<}$ zX2od7$$Ta=R2kG&kyt85+lcP<5<<;yXcWE!iK*D@6!Sy0vy z{%5Dg1|(Ss@(*U8DlSLS<=HA?>qWfZ9JwX!Vs!ak-Q4K%OeMbUQm#phM3=V_Y*RB2 z$s12omJJWpqpKeo;uoXd-)WIp@$4gmeP>8r6^)oVH$4*hozd9!K^*t15W`)Pmxu8W zX8ZAaqX-03i65^oYTzDZBv$;#6#1QbFqQmnB*b~2q_zIxh-NN{pRgAX`Q7*cDTw2~ z4%9WqbZAk2vnbK>aw9 zc=$TlGF=ZiKT|Q4?fEO~A|lSi!;3H_;f7gYtv?8@zd>Sc*?)2L@h*qe+W_EUqa$1j9|Yn@ceZd zgz+IGCAl7GI#J|zmWu0*`8iHUiu}%0;vJ^d_vw+y?>3@GzkB=}5S9HP59cB&({Qn)8_y>ZLp zbPz>;XDabJ6!{J=WfKjUk>Wek(f&I#z&ux$VF841Vm()#Fh;E2V|=T`v!;= zU#8A#|1rdhXKP9F#`Cc46}Tt_Q?acTu@f`Q>LA;Le70JCH`a0l5nWI8B8+9pXxx2; z=6#{HE<|%<8*(ia#G{v3^b(Vv?ZNi&(U=Oqj1rH&XarM9-gtGi5gd=6d2s!3Nw}MV zSn((KZORWJ@e}p~$^0Cr4-oS~Ey;_xFd;1xwVu(AGaAw5;}Kb2k~}d~>^ag#NId!{ zKpiLqaq?(dBpyAZv8InmmY2j&ya?mZ)N#h6 zuJTb&Sx8Lftt;a{6*DJzv_|vfNF9daM)Gi~K2cvO63VpX5`mH4HCEmVkdW`hL=$Eh~K9*Y4OxHW%*O&b* zN#1zxwGbIzM5kc7Zj(~r@749`^uPmCL;rTWJFpkYi!kNd{$A5sPYu<4;_kqH4}z)0 z-|G|bjXQpu+~K{i)znZ75R&+b7h(L%;=A^%9-+^%%OHsn>_^u25L@o+D}AzqjM>xD zgVCa&sRhR=zh!eyLyUe=*RNwe9$;h9f=X_b63{R-0vvgn8)&Kf?5(IByY-PwBwu_ z@>qTaP%9)Zi>c&y_d9CFsUH$&?~M&?L1F|`*>~dZ{-u08?#V>_A#+`c2eZ-Nr0WkB z&hzxE?)M}XieQ}>jWy&KLs9P+OeG%7Za9&ytNc7q*@!Q0RsND6H4NAD^ed*~K86*q zvdaG}sJA0AmE?`THXAYJo4*tHuZ#_^MPdYdVq-Ks1^)DRVqH-G?Ln{}jCP!2HR}6t zn<`xUD%@S79>;CNXvfJOIxdL&aAEkBwOUX-v6-(s8_}}ZKZ)HSktBX1dE-USmg>`5 zjS_GDvT;}vBiKJ2qv0>3cB90bKz*AB!BR0A^LgDyiMN1S5>v59IUBL*$l3gA*u8qf zvxCLER^sdB98RQNn9vt*ZOVm)ksDQVKZVg4GOd7^59*N~1XGDWnHsMo>($q%$>_7OSUf!Zhv7}f;vv38 zP9IFxe>EK#@nsU-m2;;3dPPnch#DRQm&<6!Y4Xt0=xuVQ5)UL-S5MKWb`DWC;`H-J zL$jg7D$#@F_3B}~>fqb?V5{)9<%j%7kr*L)LnU5rUi>LppX(s!0Cu0->mQVMS6C9a z6#HSZ_H>+4-+Y&N4|aVdd3SzH{n&`vf9>@rrr#B=fW#U~-uQQ6evb3OKYRT(K>dnG zDn{Fg7v60V$n_lyjYMK9Z|xd>j`J`O`Jmp1#8m7Nh9yK~>*|97C-pUCy?SThI1(e6%J$q+ z>AEZYZBhOV&t4Jl4{=Gj57+BJ&PV9GG3dK84w~ICrsBOR+}`+eLF68!co^p=jF5ZK z@BWSZqoc&5L*2HAbIe^!rjon!Z{S{PW&N765p~wI3RFIK$p6#7hy3CT=h%r9{qCQm zBEE25u9vDGt&MYJt%0Zu#1ly3*~U<@mm3@LdifLn(_`-pltt|_f@_q~@J)5g z3I9w`-|j(JI*}h9vBKRiNQ(z)&S=CL{rxTV!fN#>au}*Wjx=^FvL1HF03JCT@Yct;SU1 z6(M0?s(!N91ho0h7=5;4R?N>;w2fr(<6IQ%3TQVPs0;BHLqIlJp9&PwJ_eCBjY!U5!~|&^f0S9%!6fTG|sm_e^>bI<}%>}NL&{C z&amGoMsQhPgn5H;_T$PS;UAwpn15T(gZW&e+?UuthvU2sL|0JP@gSIrb%NiIeouxg zZde!k{q1!je!>WruxJERv3@udR%5Kcu(L} z$2|z95TM%|CGgr#D%`1Bh7NwRJh zHvl{mziRCt*0*6`8It(STV6d`hxhl1c-%9R*xvUphcJ@Z-uG&Lz9+}r`eh#Cp(}Mm zvVL+yfrmfC&v9N(JQCfHVm(-P`F#j%IH#l7a2;M45yaO@Bnk#=%RCs3Hoxs$;tPvo z15=P#Dwf^$33x~4bBSr79^gSRmG~*Dg!75T19CpGZp^vFUZ5U~#812k~hvbcCJjolslb1)BR%0~Q+i#o~>iS&W@CYPsDfW3MzZ;Kv@U)P3AhgrD zEBq^xlx50gw2gQih~1!;xo5*_YoOU3oW5;{)R3iCn>K)n@-sU&aw z_1TE|%YC7KH#Z8;L}CPceq%Izi>&m8-T-ySLJ+2%84XK6H9vH1-I{@ub!tYu+|Wv;61Tp7E;m%MWbekoBtG-rS)Ho;-u{~9anJUpmkvZPP)|Z)9_-@O5Z?Y$#Dm$E)$QXK;QeSJ78zN6;z`M0DNRq9|GLL+FSfN|uAGie@*vnY zWVGY_@bR*E+}RF4DI+!2B1n3)@=+^t7J;L#*#%Szw>bi-)10ji@coF7=vE%d@|6uOb zu&>|)%aV#E;n5D?wQszT|41I9%Omjd>qXu>naemE?^iwtu+i^7q8; zL0y$YT`@vD2ljnCS>L=EzKX57dUV6pp>I)s9}f>MS3JrMXq>D!Y!I7a%gXfl$>uw&EtVgYpwWV7> zzB=ebd%%LV5aLmA=?}QUQSudKOV#-HG4Y6K70&V?xLiitQZbcRD&vXIaqgV*LVh>2 z_nn2bxoKy66n+2Ya{jKj#fIaMxTV;Oq}@wCc%_{GPEfZ*Vhts4ylgVsadK8&@Xsn; zDf})HBiLUfqj9beJSw96&K{{4ZS(kYPWSwi`HchDhOi4jV;_6g6g|$plZxntGA4fh zPdWdmx5fs}AW69g;9cy=^W!Z)new~q0WrW@eSf+< z-pqP9e9fNE{8f>dhvZEUw|#}_Kfcd_=e;mI@Yzzq*W{PS$wH_y2(?xYYB9})Kp!rHLNHnvD1J4uIRTLj^7^flWLxDKeqdB-lPBdcY=DQ z2fel{V>kzTRg;z+>h1M^w0l$Ju-@#cN4QSG*5)q*B7F>k%^cy zxe{0S8z8afTtD2JhzkHjSx`&j_F%M)2>v)TJ{Pr%z2^Y8n)sdj>ycF5<%w4#{mQfx z){wuP9of69?`aQ4aNFSi`LYh-;PM?INnR@B=gvl~z#A2Vw@DJeQT!HhuXLQxY7P!h zI`Ma+B!0pO*3U-p6EDJe?=9%=uKaHN13Jzb zAT+2Yak*ZEX&dZ$p?1GT&A#QST`t%52^h@Y;u8yC9r`ZMI6O13l?jXNt=0+An_)D6 zi0{Ys_oeBWO_P+3s6KH}@FDP+2_9T7zk}lKu|`sw-nUh3>e+Rc<(O4xrs6lsMwC8( zH2x0o_}KFfa$jOJtc;A~i8BtxhWjG%z5)A8mEYYHjgJgJo;VEDlDr7ByTE9SyUu=p z`Bry@CGm~|dyBOZ&Hmfxe-_jViN8$PTP&j;hu6aSyT-c~Zaf4#&Yt)03YUJZOrRzb zBP4IA#AEN~>(g{*WAQNl6A-09Es39a5ynHX{dLFv%KLR^JhS8MAT;j=tz$fO#kF8- z-n&zaK)n;kLjN8=7Gf&)VaxS~ue+osxyElTYc7bAkEZE8ec|_-&X=tmtN;jM`9}Rrki>uP1o){ z-QtH`Wl;A3SG2_`HXB1>S+ka%W$ZQ!g(a{UPx~P)p(`UWD;N>^O5ued(Y0 zd)0s>MzFVG_Ckykzh%DkzX0lD9t2Z~AK~8{rt1bRhf{RRdg>mxu9mjJ3WhEdmd#@BMcmF@ilBGe4}*1JMiAbCFmoM%#$IItO!i zW3Gk2`v4=vyK%9Zi0;>SPQ)Lwt9jz|w$V|PPl#|$^H~bVX>)dA{Ab{CD|qk|3xOzn z$$QiF?cK$rb@l(8(8p6_!;%JE%YOAS7;|DVNdkOnLi4pE;|>l5kxy z8s}uX4UTJqvHeYq?M%gWWh2rrIHALkC{Ls!NhEJ#4%$-f`{TRNlv}EV`yw%dqX{w^ zak_s0E|dZ4inrPbmWt8Xk6Kb84s&3*9omCOA0AQp{db%Oi9h8G2cjns{6u`6ztAO3 z|1lXJ-)W3-oJTr88Qyn#onI1{D|s_IGa5IluKOy-cu-~pQ!#Jsf4o0CClyk?1gV%x zJY^4ocl3`>yBNQ{uYp|ZVy7lmLdmJoaNWlIFj2|Ct;W#=A) z+h@gFgx~M~eMk~hu}5=3xN$9uLNJxh${9bU6p;r|9ts|;Upwp$7S#DHUfG?e3#0X2usD1NZ$2(-e=E4(UTlpi(YgJqPWIX z5ewjQJWRQQwh+-<@!r~vwi?&8({W1odsY6@96f|#6loyfVwpjKk*`r z7khYg0HP_VB{2{FK4Uai=udUxVp@NOhVu*ZK! z<14lJBmVj*|2a=jWVFrW-D(#S_n`dMNc;`Ru>hDKJWbzmA#oq5Kl4b%Xd5wkM7_N0 zFfvFKS8hT624plMb#|+rcRl> zz6bg3&Mh+(L99XTm7bM`M>1l%&PzfHwbGdI|pACoYwjpB*XIEth4FN#a_-*>Lda zhjvd~G+Zt1m#K$e7^YhO{FHiYYo`8t*k~1-^KkU{0v({aM5B$&hg_ChS20O${^iUr zxpMEWf``ZZ_rIvNmCn-7FBoU>!#C%Bp9L4Arze3AOI2%onkpWjrEh(9f?_nHTJ>ud zZ=$|2Vo?8=)hlhX^w>`(Dn{dtO6vb%A>3g!;Mq`CDV$ik2)!28l^kezc6r&xdTCtCE9srNI9v(eA zy{?M+Gj-YR1C`{*yq2j0`4S29zF&Tg!#-*x(jW6+D&~i?Q5WVXm^g}OUYx9wU&+)5 zR%NI)&&^c5T4w3TUz@0^ReVFeH#}1pzf*YJ9{WOG4M?>aQhn8Vs_L1RsdZBu!6iYD zsoB00KsX-6=pEBk<-VDE!Ome8KSa*To*8@$h}VH&*;x{n5YdMwJez|ZfQYvqMlhA+ zUml;OTdtlAJ+>~pHhu>Xh|w9S{Om0CX3tFh)VD(|NmxQeMSpvxZwnCBJ&0}N=cuG- zGxcBVZ8R*I-rxB)0MQ5trYcioo@&%1Q~xr_i%{2{jKnxZ^sE3=?Jv1dEq)eh}9}QiXIbrRCC|+ zx|`ng$w`hUpt;};6^~JpH)9M&o~fsEu~rhsn#m;yLsjPP)~ejHOkJ(NL~-4CMXg*D zbCP*5RnO&})asuy^|GJG0`b^Wv2mv$72=;+JecZq2c>Q+o2_S*ka7Okor~kmNe-s+ z?`@_kR?OC;m(Ec(V%O458-&NdQoF=`&G1&LZ;fpI;h?#S(OB!-(I-yckN-S~a;3Vf zhZ|(+Go>deMmtWZukNvmkB+}AE}(s8$1T1 zJ(%jug+6NDkSsm>&>O1F&)wBmDqFu&MGzgf9`Q8+Vl@!Vqv`fGYDB$k?fh@9N2;-3 zca9GOQPYDsQ~P1{n=f1cH*%h0wBy`cp;zufAaE+h;*r?vF|`$vw7WA?F&Zby_m9q- zk6v;=>WXDwvoS^ek(H%u{xL=Qa(k;YvDvzLGx{il>(TS`=hU>s zY+d2dJWqS<`eIk!4oG#IM|0Me(T+3s-MbSyK&q*bitDH4)V``3dSd;T=X&buvo38C zwjyGBC0yFuCE#rmtogVEeJnAN`N8w??aTTR4p z8(TeHwSGQZ51E~EfvD53BI-0t#b_JxV6aoLIP^FMJ-FU?zCKy` z@6OgAADCdI!>x@L2XiG+5XeO22h;I?5T4lnB=bGsR@7>#=qZSsQgK&&n3>OcH0U{bliGJ6O9!S%Z%_RD=iHV+WO)!wwfOqYYW6+Z`sFMcsl8T) zYfZTPglflug*s!mw7D0-PmF{@_4zG}baL$lg&_C|Bc6VAkzUtsaUlqP!idM_EYj`t zvO*C2gc0MvU8J9KRu+QbCye;C`eNN~%<4iA{Dcu_`YqNOd)5?!;3tgO|IT9l#_@H9 zAovL*l1eVo4MJ}L@ki|bhz<9-r_I?ylKewYXX^tMvVmB>^IqQ)ydQE#SX#=I%T#;H zwoo^{kgadd6~vg*=Y2JR2zwArH7Ki<>Y19Y19fCf>~!tKAog&=86K&a>gLjIRlkAR zde2-zG`yy81XGpi{IJR$nyp*>B8c|Wh9@LoELnrGg!N#mV|_cSzeZ&1&W)ufzL&c( zZ&1m&@QcN41XHyf_L!YrTVE(d4(YO2_q()SPDHn2vN}CCybDML3Aams|cd)Ys;c# zG2;BeWtU17L2y}&;F6*(7(sAZjA&7Bxm{NoAeR(<&n5r&(B;uK_`e~F=dFm2QL|oH zadGsysCfp8lw|QZR%~T-9AtDv522Q1A+jD=qu&^?D$;6PmgdY#>58BMzXfi!%#TG1_jAfw!%S&R&cz6pSD?SOnox!DCm{dWlJL^TK0Y*K__Fp(i2;e&X6F zUPPJ3$NjM@dqfZ}70zHrt(TZ2{yh@Q)W5ED?+C)Bavax2iAmz$9cMvOp8tmfeIp2$ z%5mJ;2l0fXw$CT6yX4BAm)v(2oua*jW-|L5XGQU=eG4%UHuKDbOeOjH{j&A0vtr3y z*`Y-6KJdVqIEx2U-C1Lbn($J#Ueryjj0X-k3O)|R1P_9#TJD{y+P{*muUaXHw3~`0 zY=q5lx7TJMsxzmjt3$A4Lca-O`-f+J9|LiR2fX;)@0L zkEI7wRsP=`bq(y2>Z)&*bH*=Zo3;-W}5B@BVN@UIyx_r-uhq@oJ0oJv$<_+h5s2ib-Od zC=!j<-{PJAk>1WvTsz2%*!sj%{+G87j38Vp+}ns+NQiMr_LbwL-&o&YuIba71XAitV77B>o-eisHB3+vN1H2oJBdWa3ZC z+ia|*2x5~#@DtbGiAmz$BXO$p$G0uu;ZosjR@A17NpkZSHP1Ji^Ap$JVZ^Rk9er)l zcV~I}E`KlgT>74>*A=UgTeHxwn*8^6q2K3BRWIWs^*+9eZfY2L&wcZAjh{P_(2Q_Q zHTs!t>g|#E^14sn58roVeK$fXd{G1#!BjKf+o8tbi$(Pj#Ba6Yd}i&H5lnSt+)jFH zg^J@;v#sXZS}{rdJ0ia|J{~gjhf9UnYf&pKCW(K?sm4D`h0UzyT1YWTZr*X&)^aj1 zbB4{#=~_!Rg3*zAFl6RIR}wBaCduMq#i@3C23yE6N&LH$!EvfxizOzB?H$fXMuw0X z8C(R;rAKX_m?Zw)j#KSY;Vwed@{UR3-<=H1wIMUtx-AuxyI~X)O)r@F)#T0dG!%V%p?f}JT zcq7@iI-%YbapAmDHjjtLPE}R!&eRpxysQ`vZwm>lgGZo836CCKCrwx1)X3CNUG=hJ zH0-OPI^h8HxZb13%F;8{&}y0brH%a+qj9I@=uef-sdiqSZ;y?K$Z z9uONnh$G`ysafqZ_1m`(R*c3ey*9&wqajsyuT&G(s&~d`>e1_lD@LQ2luybvo{d=Z zBS+V%Wd}0#qm#y{BZP)`%CA0Z?CuG>wHq4up*jOSQlN*-qTja3)RUJC!mJki*MZ>s zkm@dvR5B0tM(v)@?XMV(vx8@Q_y$9B>`7Ve!R5BBvrcusHcKaeB{wIpE&hYg0php^ z!Bni1yyel+2R!&h6QA+nQDX0c-g#2HPeTvh@8@#>YX&V=&kaDmy_Nx~j`}M5HbSbq zAr&K-YEt{9>K~jI8GYAqASyglD&b)urh5=f)&95TYW&hnJ+P1L>i_rV6?p;l_GRep ziMv-sTaEeIh?|4CzBRDI6JUii;>enX>cV|lI#h0=Vl=#qUH^seFc2j@h+4%~sQx9h z^nHWJMQ$Q(uAQa7T_-oagDoCS$i{nqKi+fZ(V_EV)!&z;n+%?ym>+t3qmP2ept<;y zVT9apGUYNFkuc}<2?o&~_0b+;={<|{S5Kb7c@ONX)cibC_bNF?ar?tRwSQgSbEUDe z?ciiT^&jq6T$1CgtT8;N9i*B8skq$s&%LXjd?ZV6YyG;12lcD4w{dR^vNwZHX;%9oj`+Z`Bz+D$%lJ}9H;b*SB7o6psk zy)yO5yN0PryY@z@#7?aIRHn|pb|?@75BByQMO|Hqx~e{ChgyZxJ2&I>j>Us`J5I%< zw}Mq5)rZiW5nL{#Mc1g!5MX(kYVkeqM|CPdnB8!G!idCi??r3cMle;G83&?NHe&iM z#S`THSdRCD>uUcKyQ4MD^#)JrJ08#Z1)85hU9q-I#k@r-*Rv6iqLODT!|e+Xje0!y z8nnk(;K7K`YVS}FR?XB;q`{jNjgL6l-}X%KAP{9d2$9gtYK+D^n0R0O5$I6@Eyz5A z4}Yd+VdmV{6|)+l9p~{E_xTzDvB87LSidVeQZpJBK$j-*I0F#I2?dKs>hv#Ehxkl= z+ZO{Aqj7_0NJHAea_5ihlMh7a3`WDR!H%iH=b-s8kLFIvucIxpS`hzmrINhcYWxOQeydNMIXU1(7+cih=e+dl8@@l$6Mw}H)}Oe;+2`pj zz2>$_YEIRaYW~nHef+04RQs|^Rht*H^wMpzT3hny$H8wdwmBo#%wDOQch1rcXTygO zp&h6HPs4pU{}_G%np;%0-c&v2X6b70PVpda=>DoN8wfce#E3yxu2Dx4vh>L*6Vx0+ z<7U{hH3<&`F&zk|s$6=lYO*Cu4=j)K02ZR{mF4qr8a@n<3l@UMU`9KRYB@4`SD&eP z+_e#F`t{B=r;wP6cie5n8Q)`hpF%2$X~E|t_H6>dz^&k$!w@OF=Vhs>XHR$*^xTUxS&n#c7-uo#_m;6<3(XDy5wGXGxA}8pWs^y?H zDh_Av;!n<1HX`TKO$qxPc$z957hnXhAu?J<21FV|U16;{z_X0ZSL4xw!_H4r_KZ`x ze(Ct={SVCBC$G-dPa)1PBe*138GjYaZ2=zh!9!+FgJ3F|aST74lWBdzR~mKo4eE*! zQ+8}uE$+kWY3WQQw!}L)Es`-``tH5GugO~to1qD821dvX;IzutN2X;eMq_1r$)MoB zu=-A+1sPFn-e)R3DO*>on;Dru47#AMa4srx#)}a$a~ho(U4X#+6h7?HoV8^%;(1-S z-}eioIs>WriOlClTbswf>6P-TqL+LCgpAZ?bZ)=)Ti0gbnZbE>_RYT9kP2r;Erg8c z=EM%8anACUc%PgQTIWH?EMrdWFj_Q^+6>6sGmg9e<5(8Sis3T6pj-nY*{e`&^6` z1n-i$dt1EE#b`lbB{jNB=I(9rJ{O|}!MkMcPD4x*=iQP7v1rbGfBSN2VaYRb=o8a9AMhk-XCfpqb-d$j{Ab4-W-D!wP z;=EguAPV1`;N1mAUqnRrCfuEdm?X~2`|s{%K&nFaCU|#&(ZU0oNB1V&)n#63X0#yw z!07W^WMpvn4P@MP&k=dw^P*OSi&`DFcLChBdtS%C+%AB--X4?0dAB6d1MgrVEA+f7 z&uBpuwF}^`0^2(R_D+Ml+Rs$XPk2l!S3h*FNx{CFyLQiO`rI~xC~6nLUB_p%An*=G zcLChBdtS%C+%AB-j?ZiQj20e6t?9cf^!ECG zQ@L~a<#;d^TL4A#U@G?n%;k7667l{<%DPAaD2xxHu}OvM&J(Fmq; z=kUu(#Z+to6wQOF+;az)S)a#U zFM_GqW^hO9i-?Sf;?SI_cy7N0ApoH4S>L6yEr4iUMG)>>#IkcqUa1(tvzp6~G>`D$ z8QG<>XJ=a~cTTmXa!c|`#fZXZQIZPPW%nz#gIufTatN2owo9UVT*8CxAlJfS9 zAp2ZM5ax;8eI4?1rf&M}5D$+hpY#QegL=z?b0MbU+p?ER6~OGj3|0n9C3$mlKz8*_ zjffV7>%mm4Qv{GW?usN)yh0uek8#LJ?AK!}eMF`--C#qj6RYQeAz|Ise@rsaO(5 zU%sx4R9tV+{QD+>F1^l%hW0)iV(xNh_*&dvUT|PsL@IOZ8(x|hCWHeY|Kopp>py_&HZy* z^P?qN1eW&uKC~d^`w%1K?(>x&VW;7-35wD1nYn9XZWr{c*DwOHBwmE6w}O$ujH2G2 zXvQe%m1e{cgnL7QM<2~mywVrTi+0Wo@? zjbJJ{N0u=#Q)gA~hj;Mphido|P`hhUyK)B3@L($Ok#gX@Oue-A03d1%>fw_;$Qd34 zQ%Olb?a9>1eI&Ni-Ep)0@sR3uNX6xPr7~yZa3lB69{y_(t@#B+Yi5Mx4V8VmZO@g@ z`fmfGJd&JZYo45`JO0%ta%#?KYa@1_@8RDG>a%@q1XJ1P;Z{6ZJ}(iPuYl&vLwv7{ z&CS#^H}|*rInK7X&Ih{#F$xIQgArVBxNorMdcV^oHckMm7KZT)H_qxx?iGN=^w85cS8AjK=2bU!kqDQoFT=g`{agnwnuYDi)Wgf*JkPt zyCp`|b#3oYeDc%Af$+}877srCC+GT(;rvX+YecHY&rk5R0*_4a;Bv*c%$(Jk`oxHr zl+ELY6@ByD=Qa-Cfy7T3!MXi59w`3u@Sii|~GU5yn@Tc#SZ;3lLi|G9W&OgvzJ%lPCCpUfMXuV> zEPYqS*OiUfQnE$hRaoKqeZLPe56K%ZP_`BRWv9migWlK@`h3zB3&B2e7>(VlLB~Tc zFS;`@ZSkD}@!~M*v21-^#&pX|mhJzFd!iR%JVnUwu2-(hA@Wva>-JL%ymA>deBDE; zevoRaM=F*?d~EH`1C?F!9Xa!DdvyWct2PVM$7L;LdW@~ z#ae&$a<#)*H`fmH6ZT~xzq@*zI#)ip6+B|(9VB_hm&K>=Wb3@eb0anF@<>%{{i{*_ zNDmK2Fh8ssJd_(MTccK>Y0X*|58-FDwf%TaZJ!(31VnE2S^@Fb^>_xn=bWEudHb^c zcm>ZGfeuqqUo1O&@v!yy{ATohqD)Esr`BR(i2c7z&MgeVt;T4?P#AEJfBRL9 z!gWhFvh-ljEvysvoS(bL{|u-%maq|AE~61E<-vRW6@ge>s!^D!ByT*z*oeTHafv7A zG!DN!ud#(-UoVWtUF6(xiKju`c&?2QUoY3}%GNVWWhzEHPN}80`@0=$6j*Y!QGn}* z{oIKUx^vj6p8lxx_D64A{r~zp^YAK)tc^FIK!h-SJS-{^LV(B`g@jyDZ#S35XjB3w zk)R-;0x}8=2;v}$5bz_3vdFGN?gxVl=pc*aig3GSM;JzpCV)T?Lck<|OLUkh%e*JC z$Xos4{BfVBpWEkG=hV_&)m7bZh3BEQuRv>Of?K%!uXL)QU631Pc7=~##kvo+Vt;tU zVr`VU6xP2YD)!D+x-#D3S+%U^+D2LBF;N=9R;;UsS?%eytQTXWteo~ymT0uiMnCc1 z4~sSZJYBnO@XT7)t?i<$Rvv;Q)O}o7w>tDO+HM@$E|(S82e$>x!yS9VhdBn<=ONe! z)7WWzO}2kK>U;+3oX^OydV5E&^G(T-PR9N5?#;>AcZpgC>-#W`amk=YdFpLLjzpB| zUfu<@w|>h4oRVSeni?DW75O-eI~V z94q}ioj4R*;oOnZ+70w=tr4tw$24}kiGSSf)#(E>v*QOQkI3X$=?Lo@zkfuq6_4kx zj*wl9!d)KMvhth!uSWgVBV>%JuSuv8GSk(&bA4G9?{>$JN2&b&woZgH23pCpw>hn^ zF-}w?Rvt4#o$~Kvt&!hlI;%=E9Ui5QgyLfR>+y#A{qw3uuS(SuUs|jkYo(vbbly3g z>9Ca?D~(3o$K)?wvg#CU4cGsAYnTbqWgHz7wCA*%A7C2V(cS-?HwrV5Q!xX{QE?q` zO1NL8jr094-a{US_mKIFs9ye1Curv<&I_3Z|K>wkF^#YLJFV++ z^?L&4?#c8z9~t?k?^awNpW^!PwkuW7q62zpWzc@Ky(lOT?{kY! zR8`7Ba6ULfo#@k{!ub~W+(`WR4C{+(_h6&rs0Z97`A3|nua9UsXAM)Pu`|$954a5r zjyT_a<^>9Lo@R5&7F&H@3gYwJZU|Vao?>K&fVy7ns|B~CiI+- zwih_zv+=iE4Lk%}i56=Q=$FI}lF@6#Ba_YN=ilaTdi^$6a;|dA zdQ0+uP0kx%`7tEwtvk`$xvV%sP6_+5tY~%z6o*_~BU(O0$7#!U=>4|EOg;I=w`9Ka ziKjsrnD^myId-Now={CHSuyW6*Fau4Ew=O6&#s9)5g=6?^5IxKt=+ zRqr5q<*R|=@rWoBewgt3sCn`_{9UQPOCie1<3ng=#r_PVLDWYf7k4=MevnI-YmjRe z&*we;iE2kSCb-;~#)xdRQ}yMfJQcB(XoWrZdC-peepx^#8b<}pLugY?@#8actW?YN zcJ*9grT@r4igRm^O@xk=^~>4kftT4rBd; zN|bfg^n7&e+}+;<)-62*r^__7qsMmlFM{;|_+cwKR?nO|vDv8Q3|ScOMq>v_CNw2B zqVcx+i3{QOSm_($S?%K$K2akn$J=|CUmJ_-Jiq6dO_*zUvCoLM+Q(MB@&}FI!xzHu zU`4UvdaNi?T3#PYF9q*-eZSlHJ?i7%;)4iLzj$&Vb_}^|azOXd{^^s>g-2toI|s*T zgwQHoT_dILpHDhN2gO)DJOo>bMvCzUG;;nl0Xfh4W=cq&9;GF6RBXi&LgV+d^3Y=_ zs~pJ(r6oEXKVf%`@&hjgbYkT*C!K|ess(;bc%xFfFW9sAXtKNDy(06>yG14wa;&UG zxuq@MfVh|@Z$R|9P++c{*v@T>AD1Glk?@Kc#_h+7{Zp~7R&kAWHMPR46VAl~^U$;H z-1Z(HoLlw>4Y*4M=6(?SCbVV$PQUK#VQt0@&1(X%{bj>v9f zJ(JZ&BUpPyS5+C({EXQH#N1g}kAn6}hY7iMMyFX-YAtHLPA6i2S&>(a>!Xg`MQIh0 zBb1qtebBm}G*kA+S-sG`x)UZ_iTc5&k8wYqI6}j?tIu^; zcZuP2IpJEQj zk!!!-B&SMeLY1^(-21QfPU$Bd-MhASbU6~%oY0jiHna&l?I(PNyv$-7*mDgK3>8Ka%XG;GP(<2KPj| z_j&J$N(&2P(G}x-*ww+sx7*r%mubFZ;7R!A@jiUzT{VZyXPDrt*)VE7G{t?cD8ig| zJVK9(bhSjAO5InW>C}IUJ08R~A?S?wP_h4iH7gT6j=UAdkua@C)n&tp9(D2LbplUb zd`8qZwqZ}C^0Ze1+}^5ZSfB;qUi1*`gK2!$H*To=?Z$P^E%@;nRt@2Hj2>rHJ9o*p z8{EsE-hka`YFaoG++uJ~T-(AYS{99vb8hWSbBi&Ip1txnRp5EB!gED=BUD$HVT?vp zA0etso~Sqy-N*9lUUKU`c{JQ=#L+O957(Obt5GnU3u4L0qv5}K2)5z~|G0FS;Btd9 zUJu(k;VUzn;3t|__bys)XD@v=AevVPLd)&c(n%iew&IPSCq9ePg)*?kgs3|NOP1RU z|MsFrLqjsX-0ZV6+REDztr4uTrK`Pl{GIr~L$H--A?1%*Zl7B%Z|QaWVryP!L?!wR z92HxM`i069-mQ&ame(5u_Ri1nqe=W@4Gi zwZ@TfNn>w7OiTo>BPMtCV{YoptdA56E&J=f7&(C9TAo8JkT;-CqOGJQCRtH8@z}7aY9Ia^~(h3iD~p|sV}=v zE z7qlxU%|q#??kn@Xj#lz8S_${gqH^?FS+4!s|IP7?;STQZmM2g2xDGXf#|cd1DRsvd zUnQbCj~<7wZTZq$y|XLFv)+T&q#dkXCMP#Qkas z?mq0pd-qYaVI-6m_?yDV36Bp>mm|a|qfw!MCakyD14XfW3H6?>VSKtY#|heT?!NE`MrYhb0F4x2)1H>_#Wq<1>r|v-4Ya^@e(!l(Yk-@c$IS|L=i~!0D3;^ z?#?#9&WLlf@Z&Q~uvQ@E!n>9`AMB5|=Ix8tq7r{95?!?}+$fwVebg4z2S+7e_p5%F z{oyIKOSy9tKIV9QaJo!G1uAP$y0MHIyo9pi2%#&oILDca);^=R-rBi^bI*V| zvbS=aW+3W;;2M-;rFzLU)DU-Mn|~P?=RWSKbFOEmq4}{l+bn@~`2a*kZAp$*AH_88 z3}<^=#T&|;`RmcE5h1x%Jr2`&9t5!;#DZ%Oss@?H9P)<~t%Nh_&dsOO9WF&yq|}MW zP=j}(2A}cNpw!LPdL}JJjIWj#C#n&E)U&EJQLs{@0rriRA4D|_QMI^6RH{ej2(e=F zbPr#Bw36d!C7c%f5JJ(oC-#5YI`Dp4_?0zjVLl@&X{sO8eFzZ-@f8TR^6ILoez&?G zRDCV`5VbGmgHtk$;)hwRkHn=j{Z2{|hKrz8qT+-+{3*&rh?89fgQPtE()9_SxK1QxzUd%Nt_47!N z$B8oPtI-_{)IMlFkFx6FDP1l%y{xtmNV4{qoO0sMoO0NTRV2Bj4dauCldK)zopSbE zgBXYL=T~E~yF95@qCVy<4Xwnzqz~>TT)JYV2=6@sr%>)q92G2AUIv7bz}!x-RFtHyk!ER>j4bPl0N!Es5C1$2oq7j@f(}vNd*4GWZ*9qba52{~3)x{u~>MSKihTo?Md7Ux5>KgYZqwAYNqR}fmxhyAl&2)hrv zA+#%ASL3>oy^B`u4%$&qh|1>dA;W#2f`|pdR-CR*+*iAnGXraPrStW*JGSC=JiJw0 zua z|EMT-rh&NGL$D8~@x{nvoqVkj)n}fomhX0arD2ciZ4)=i6U#X~5Aqo%L^*b4M9}{E zb$Lo%+;YLD)gVmxV1ljq+QfHv1Bbe|oLgrOIKR&1bmdsxYnjISV65MljdnEz?TXKc z@@!{l2Tyw%?|{%f2D>pmGSpptcAZ&LzRqOA>qDt2Lp88#n(vn1%Zk%tAKa#~Ui)T_ zPia7~m8je1P72y913qsS8VX_-u0bZ)it~hb4fafQ&(=#f^XsOY zoGwQtw9+m9V}h;NA9e@E-0LxvY=cu-4ttklr3x(3)|77VIp18}dzsUcIo-vp@J&?U z&8qo!RX2EFG{d$+mrI?kMw#ZrODKQ%T6Umq4F(Z=snlUBIaca~9HC+K!fb8k;xhAr zMP(*OrR5wmw@QB*Pmo=j zH-H~L!vy=Qejbd&^I!`+5Asu-9IJ7({yf;vnd-iYtM;N?=k%N@$7-xDPe?VblV$uN#$PUag}R}9bzp;aVI zL$g2dZQfzjc_iNcD?ttllHjiG3@|p<)&7_CqV-GjhD~ z_x%9%R#Na-p?~-F3GQ|E5;TH$`ePb;GFG8K64npa)d`M@X}sT_T+v0 zk?BO{Ti^Pwm2;WDRM!WmgjwhM#}ifQ${E#uxc-^OojSTOyckgpL_Rnz)?(!d@lD*7 zg75)YyP%lhbeR^#p~CP|5C=i9l^m=3(20B2Pqf_pbo1>m(@mL;Qay_ts~&)_V5s#H z!Phy{IhddPM! zN0d2x>z6rRE7eEoMAi%C&fsvgJ8NsS%V*?R5v;`vU7S?g9aXo?8RPLG{#0+tG&EA? zMTPL}QQdz~>9Rj=Z+MP+zTEi%5jAw8wU)?{aI3`rvB<|=$VWCPPK$jo4ZXe>?@YWp zBBTC;TMV~1yd#VrXFqzJbDkcDdl{w;qX09IVT=P1E~?36}4l- 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 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 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 49/60] 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 50/60] 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 51/60] 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 52/60] 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 53/60] 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 54/60] 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 55/60] 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 56/60] 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 57/60] 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 58/60] 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 59/60] 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 60/60] 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);