From 08aa277974c18ab32a81e3c017452ce445e75b80 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 11 Mar 2026 00:27:14 -0500 Subject: [PATCH 001/434] stage in changes from off-plate-gravity and remove stuff I didn't need --- resources/shaders/110/gouraud.fs | 1 + resources/shaders/110/gouraud.vs | 5 +- resources/shaders/110/mm_gouraud.fs | 3 +- resources/shaders/110/mm_gouraud.vs | 1 + resources/shaders/140/gouraud.fs | 1 + resources/shaders/140/gouraud.vs | 5 +- resources/shaders/140/mm_gouraud.fs | 3 +- resources/shaders/140/mm_gouraud.vs | 1 + src/libslic3r/ExPolygon.hpp | 5 ++ src/libslic3r/Preset.cpp | 2 +- src/libslic3r/PrintConfig.cpp | 24 ++++++++ src/libslic3r/PrintConfig.hpp | 3 + src/libslic3r/Support/SupportMaterial.cpp | 22 ++++++-- src/libslic3r/Support/TreeSupport.cpp | 18 +++++- src/libslic3r/Support/TreeSupport3D.cpp | 16 +++++- src/libslic3r/TriangleSelector.cpp | 12 ++-- src/libslic3r/TriangleSelector.hpp | 6 +- src/slic3r/GUI/3DBed.cpp | 59 ++++++++++++++++++++ src/slic3r/GUI/3DBed.hpp | 4 +- src/slic3r/GUI/3DScene.cpp | 18 +++++- src/slic3r/GUI/GUI_Factories.cpp | 4 +- src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp | 20 +++++-- src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp | 1 + src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp | 28 ++++++++-- src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp | 3 + src/slic3r/GUI/Tab.cpp | 2 + 26 files changed, 230 insertions(+), 37 deletions(-) diff --git a/resources/shaders/110/gouraud.fs b/resources/shaders/110/gouraud.fs index e602d6067d..12f4feca3e 100644 --- a/resources/shaders/110/gouraud.fs +++ b/resources/shaders/110/gouraud.fs @@ -26,6 +26,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform vec4 uniform_color; diff --git a/resources/shaders/110/gouraud.vs b/resources/shaders/110/gouraud.vs index ff3a190c79..e2de2b63e5 100644 --- a/resources/shaders/110/gouraud.vs +++ b/resources/shaders/110/gouraud.vs @@ -23,6 +23,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform mat4 view_model_matrix; @@ -71,8 +72,8 @@ void main() // Point in homogenous coordinates. world_pos = volume_world_matrix * vec4(v_position, 1.0); - // z component of normal vector in world coordinate used for slope shading - world_normal_z = slope.actived ? (normalize(slope.volume_world_normal_matrix * v_normal)).z : 0.0; + // dot product of world normal with up direction, used for slope shading + world_normal_z = slope.actived ? dot(normalize(slope.volume_world_normal_matrix * v_normal), slope.up_direction) : 0.0; gl_Position = projection_matrix * position; // Fill in the scalars for fragment shader clipping. Fragments with any of these components lower than zero are discarded. diff --git a/resources/shaders/110/mm_gouraud.fs b/resources/shaders/110/mm_gouraud.fs index 821af13f03..431c936e9c 100644 --- a/resources/shaders/110/mm_gouraud.fs +++ b/resources/shaders/110/mm_gouraud.fs @@ -37,6 +37,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform SlopeDetection slope; @@ -85,7 +86,7 @@ void main() color = LightBlue; alpha = 1.0; } - else if( transformed_normal.z < slope.normal_z - EPSILON) + else if( dot(transformed_normal, slope.up_direction) < slope.normal_z - EPSILON) { color = color * 0.5 + LightRed * 0.5; alpha = 1.0; diff --git a/resources/shaders/110/mm_gouraud.vs b/resources/shaders/110/mm_gouraud.vs index b0cea9cfd6..c54ae4bff5 100644 --- a/resources/shaders/110/mm_gouraud.vs +++ b/resources/shaders/110/mm_gouraud.vs @@ -24,6 +24,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform SlopeDetection slope; void main() diff --git a/resources/shaders/140/gouraud.fs b/resources/shaders/140/gouraud.fs index bbfb76f7a1..0d9fa021fe 100644 --- a/resources/shaders/140/gouraud.fs +++ b/resources/shaders/140/gouraud.fs @@ -26,6 +26,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform vec4 uniform_color; diff --git a/resources/shaders/140/gouraud.vs b/resources/shaders/140/gouraud.vs index 72dd4ff1bf..c69dcd11df 100644 --- a/resources/shaders/140/gouraud.vs +++ b/resources/shaders/140/gouraud.vs @@ -23,6 +23,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform mat4 view_model_matrix; @@ -71,8 +72,8 @@ void main() // Point in homogenous coordinates. world_pos = volume_world_matrix * vec4(v_position, 1.0); - // z component of normal vector in world coordinate used for slope shading - world_normal_z = slope.actived ? (normalize(slope.volume_world_normal_matrix * v_normal)).z : 0.0; + // dot product of world normal with up direction, used for slope shading + world_normal_z = slope.actived ? dot(normalize(slope.volume_world_normal_matrix * v_normal), slope.up_direction) : 0.0; gl_Position = projection_matrix * position; // Fill in the scalars for fragment shader clipping. Fragments with any of these components lower than zero are discarded. diff --git a/resources/shaders/140/mm_gouraud.fs b/resources/shaders/140/mm_gouraud.fs index c5fe86efc5..7c77d5565e 100644 --- a/resources/shaders/140/mm_gouraud.fs +++ b/resources/shaders/140/mm_gouraud.fs @@ -37,6 +37,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform SlopeDetection slope; @@ -87,7 +88,7 @@ void main() color = LightBlue; alpha = 1.0; } - else if( transformed_normal.z < slope.normal_z - EPSILON) + else if( dot(transformed_normal, slope.up_direction) < slope.normal_z - EPSILON) { color = color * 0.5 + LightRed * 0.5; alpha = 1.0; diff --git a/resources/shaders/140/mm_gouraud.vs b/resources/shaders/140/mm_gouraud.vs index b191e35fa8..191613f957 100644 --- a/resources/shaders/140/mm_gouraud.vs +++ b/resources/shaders/140/mm_gouraud.vs @@ -24,6 +24,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform SlopeDetection slope; void main() diff --git a/src/libslic3r/ExPolygon.hpp b/src/libslic3r/ExPolygon.hpp index 87a1146d41..7a51f70ef7 100644 --- a/src/libslic3r/ExPolygon.hpp +++ b/src/libslic3r/ExPolygon.hpp @@ -377,6 +377,11 @@ inline void translate(ExPolygons &expolys, const Point &p) { expoly.translate(p); } +inline void translate(Polygons &polys, const Point &p) { + for (Polygon &poly : polys) + poly.translate(p); +} + inline void polygons_append(Polygons &dst, const ExPolygon &src) { dst.reserve(dst.size() + src.holes.size() + 1); diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 826d8a1a19..ef2c83b13c 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1010,7 +1010,7 @@ static std::vector s_Preset_machine_limits_options { static std::vector s_Preset_printer_options { "printer_technology", - "printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "gcode_flavor", + "printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "build_plate_tilt_x", "build_plate_tilt_y", "gcode_flavor", "fan_kickstart", "fan_speedup_time", "fan_speedup_overhangs", "single_extruder_multi_material", "manual_filament_change", "file_start_gcode", "machine_start_gcode", "machine_end_gcode", "before_layer_change_gcode", "printing_by_object_gcode", "layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode", "change_filament_gcode", "change_extrusion_role_gcode", "printer_model", "printer_variant", "printer_extruder_id", "printer_extruder_variant", "extruder_variant_list", "default_nozzle_volume_type", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 50be57e0f8..918fc1b97f 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -5918,6 +5918,30 @@ void PrintConfigDef::init_fff_params() def->mode = comSimple; def->set_default_value(new ConfigOptionFloatOrPercent(50., true)); + def = this->add("build_plate_tilt_x", coFloat); + def->label = L("Build plate tilt X"); + def->category = L("Support"); + def->tooltip = L("Tilt angle of the build plate along the X axis. " + "A positive value tilts the plate so the +X side is higher, shifting gravity toward -X and increasing overhangs on the +X side. " + "A negative value tilts the -X side higher. Set to 0 for no X-axis tilt."); + def->sidetext = u8"\u00B0"; + def->min = -45; + def->max = 45; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0.)); + + def = this->add("build_plate_tilt_y", coFloat); + def->label = L("Build plate tilt Y"); + def->category = L("Support"); + def->tooltip = L("Tilt angle of the build plate along the Y axis. " + "A positive value tilts the plate so the +Y side is higher, shifting gravity toward -Y and increasing overhangs on the +Y side. " + "A negative value tilts the -Y side higher. Set to 0 for no Y-axis tilt."); + def->sidetext = u8"\u00B0"; + def->min = -45; + def->max = 45; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0.)); + def = this->add("tree_support_branch_angle", coFloat); def->label = L("Tree support branch angle"); def->category = L("Support"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 54c26c312a..c862956544 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1410,6 +1410,9 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( PrintConfig, (MachineEnvelopeConfig, GCodeConfig), + // Build plate tilt for off-axis gravity support generation (printer-level setting). + ((ConfigOptionFloat, build_plate_tilt_x)) + ((ConfigOptionFloat, build_plate_tilt_y)) //BBS ((ConfigOptionInts, additional_cooling_fan_speed)) ((ConfigOptionBool, reduce_crossing_wall)) diff --git a/src/libslic3r/Support/SupportMaterial.cpp b/src/libslic3r/Support/SupportMaterial.cpp index d1fa5d8627..c230924ac8 100644 --- a/src/libslic3r/Support/SupportMaterial.cpp +++ b/src/libslic3r/Support/SupportMaterial.cpp @@ -1392,6 +1392,10 @@ static inline ExPolygons detect_overhangs( const double threshold_rad = Geometry::deg2rad(thresh_angle); const bool bridge_no_support = object_config.bridge_no_support.value; const coordf_t xy_expansion = scale_(object_config.support_expansion.value); + // Build plate tilt: compute per-layer XY shift for tilted gravity direction + const double tilt_x_rad = Geometry::deg2rad(print_config.build_plate_tilt_x.value); + const double tilt_y_rad = Geometry::deg2rad(print_config.build_plate_tilt_y.value); + const bool has_tilt = std::abs(tilt_x_rad) > EPSILON || std::abs(tilt_y_rad) > EPSILON; float lower_layer_offset = 0; if (layer_id == 0) @@ -1441,9 +1445,17 @@ static inline ExPolygons detect_overhangs( // Overhang polygons for this layer and region. Polygons diff_polygons; Polygons layerm_polygons = to_polygons(layerm->slices.surfaces); + // Apply build plate tilt: shift lower layer polygons to simulate tilted gravity + Polygons effective_lower = lower_layer_polygons; + if (has_tilt) { + const double lh = lower_layer.height; + Point tilt_shift(coord_t(scale_(lh * tan(tilt_y_rad))), + coord_t(scale_(lh * tan(tilt_x_rad)))); + translate(effective_lower, tilt_shift); + } if (lower_layer_offset == 0.f) { // Support everything. - diff_polygons = diff(layerm_polygons, lower_layer_polygons); + diff_polygons = diff(layerm_polygons, effective_lower); if (buildplate_only) { // Don't support overhangs above the top surfaces. // This step is done before the contact surface is calculated by growing the overhang region. @@ -1452,9 +1464,9 @@ static inline ExPolygons detect_overhangs( } else if (auto_normal_support) { // Get the regions needing a suport, collapse very tiny spots. //FIXME cache the lower layer offset if this layer has multiple regions. - diff_polygons = + diff_polygons = diff(layerm_polygons, - expand(lower_layer_polygons, lower_layer_offset, SUPPORT_SURFACES_OFFSET_PARAMETERS)); + expand(effective_lower, lower_layer_offset, SUPPORT_SURFACES_OFFSET_PARAMETERS)); if (buildplate_only && ! annotations.buildplate_covered[layer_id].empty()) { // Don't support overhangs above the top surfaces. // This step is done before the contact surface is calculated by growing the overhang region. @@ -1462,9 +1474,9 @@ static inline ExPolygons detect_overhangs( } if (! diff_polygons.empty()) { // Offset the support regions back to a full overhang, restrict them to the full overhang. - // This is done to increase size of the supporting columns below, as they are calculated by + // This is done to increase size of the supporting columns below, as they are calculated by // propagating these contact surfaces downwards. - diff_polygons = diff(intersection(expand(diff_polygons, lower_layer_offset, SUPPORT_SURFACES_OFFSET_PARAMETERS), layerm_polygons), lower_layer_polygons); + diff_polygons = diff(intersection(expand(diff_polygons, lower_layer_offset, SUPPORT_SURFACES_OFFSET_PARAMETERS), layerm_polygons), effective_lower); } //FIXME add user defined filtering here based on minimal area or minimum radius or whatever. diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 50272009cd..864a0eb0e0 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -671,6 +671,11 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) double thresh_angle = config.support_threshold_angle.value > EPSILON ? config.support_threshold_angle.value + 1 : 30; thresh_angle = std::min(thresh_angle, 89.); // should be smaller than 90 const double threshold_rad = Geometry::deg2rad(thresh_angle); + // Build plate tilt: compute per-layer XY shift for tilted gravity direction + const PrintConfig& print_cfg = m_object->print()->config(); + const double tilt_x_rad = Geometry::deg2rad(print_cfg.build_plate_tilt_x.value); + const double tilt_y_rad = Geometry::deg2rad(print_cfg.build_plate_tilt_y.value); + const bool has_tilt = std::abs(tilt_x_rad) > EPSILON || std::abs(tilt_y_rad) > EPSILON; // FIXME this is a fudge constant! double support_tree_tip_diameter = 0.8; auto enforcer_overhang_offset = scaled(support_tree_tip_diameter); @@ -813,8 +818,19 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) ExPolygons& curr_polys = layer->lslices_extrudable; ExPolygons& lower_polys = lower_layer->lslices_extrudable; + // Apply build plate tilt: shift lower layer polygons to simulate tilted gravity + ExPolygons shifted_lower; + if (has_tilt) { + shifted_lower = lower_polys; // copy + const double lh = lower_layer->height; + Point tilt_shift(coord_t(scale_(lh * tan(tilt_y_rad))), + coord_t(scale_(lh * tan(tilt_x_rad)))); + translate(shifted_lower, tilt_shift); + } + const ExPolygons &effective_lower = has_tilt ? shifted_lower : lower_polys; + // normal overhang - ExPolygons lower_layer_offseted = offset_ex(lower_polys, support_offset_scaled, SUPPORT_SURFACES_OFFSET_PARAMETERS); + ExPolygons lower_layer_offseted = offset_ex(effective_lower, support_offset_scaled, SUPPORT_SURFACES_OFFSET_PARAMETERS); overhangs_all_layers[layer_nr] = std::move(diff_ex(curr_polys, lower_layer_offseted)); double duration{ std::chrono::duration_cast(clock_::now() - t0).count() }; diff --git a/src/libslic3r/Support/TreeSupport3D.cpp b/src/libslic3r/Support/TreeSupport3D.cpp index 307e4d314d..2c24b933b2 100644 --- a/src/libslic3r/Support/TreeSupport3D.cpp +++ b/src/libslic3r/Support/TreeSupport3D.cpp @@ -208,6 +208,10 @@ static std::vector>> group_me const bool support_threshold_auto = support_threshold == 0; // +1 makes the threshold inclusive double tan_threshold = support_threshold_auto ? 0. : tan(M_PI * double(support_threshold + 1) / 180.); + // Build plate tilt: compute per-layer XY shift for tilted gravity direction + const double tilt_x_rad = Geometry::deg2rad(print_config.build_plate_tilt_x.value); + const double tilt_y_rad = Geometry::deg2rad(print_config.build_plate_tilt_y.value); + const bool has_tilt = std::abs(tilt_x_rad) > EPSILON || std::abs(tilt_y_rad) > EPSILON; //FIXME this is a fudge constant! auto enforcer_overhang_offset = scaled(config.tree_support_tip_diameter.value); const coordf_t radius_sample_resolution = g_config_tree_support_collision_resolution; @@ -229,7 +233,7 @@ static std::vector>> group_me size_t num_overhang_layers = support_auto ? num_object_layers : std::min(num_object_layers, std::max(size_t(support_enforce_layers), enforcers_layers.size())); tbb::parallel_for(tbb::blocked_range(1, num_overhang_layers), [&print_object, &config, &print_config, &enforcers_layers, &blockers_layers, - support_auto, support_enforce_layers, support_threshold_auto, tan_threshold, enforcer_overhang_offset, num_raft_layers, radius_sample_resolution, &throw_on_cancel, &out] + support_auto, support_enforce_layers, support_threshold_auto, tan_threshold, enforcer_overhang_offset, num_raft_layers, radius_sample_resolution, has_tilt, tilt_x_rad, tilt_y_rad, &throw_on_cancel, &out] (const tbb::blocked_range &range) { for (LayerIndex layer_id = range.begin(); layer_id < range.end(); ++ layer_id) { const Layer ¤t_layer = *print_object.get_layer(layer_id); @@ -253,7 +257,15 @@ static std::vector>> group_me lower_layer_offset = external_perimeter_width - float(scale_(config.support_threshold_overlap.get_abs_value(unscale_(external_perimeter_width)))); } else lower_layer_offset = scaled(lower_layer.height / tan_threshold); - Polygons lower_layer_offseted = offset(lower_layer.lslices_extrudable, lower_layer_offset); + // Apply build plate tilt: shift lower layer polygons to simulate tilted gravity + Polygons lower_src = to_polygons(lower_layer.lslices_extrudable); + if (has_tilt) { + const double lh = lower_layer.height; + Point tilt_shift(coord_t(scale_(lh * tan(tilt_y_rad))), + coord_t(scale_(lh * tan(tilt_x_rad)))); + translate(lower_src, tilt_shift); + } + Polygons lower_layer_offseted = offset(lower_src, lower_layer_offset); overhangs = diff(current_layer.lslices_extrudable, lower_layer_offseted); if (lower_layer_offset == 0) { raw_overhangs = overhangs; diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp index a6d19f505c..695ac4c2ba 100644 --- a/src/libslic3r/TriangleSelector.cpp +++ b/src/libslic3r/TriangleSelector.cpp @@ -233,7 +233,7 @@ int TriangleSelector::select_unsplit_triangle(const Vec3f &hit, int facet_idx) c return this->select_unsplit_triangle(hit, facet_idx, neighbors); } -void TriangleSelector::select_patch(int facet_start, std::unique_ptr &&cursor, EnforcerBlockerType new_state, const Transform3d& trafo_no_translate, bool triangle_splitting, float highlight_by_angle_deg) +void TriangleSelector::select_patch(int facet_start, std::unique_ptr &&cursor, EnforcerBlockerType new_state, const Transform3d& trafo_no_translate, bool triangle_splitting, float highlight_by_angle_deg, const Vec3f &up_direction) { assert(facet_start < m_orig_size_indices); @@ -294,8 +294,8 @@ void TriangleSelector::select_patch(int facet_start, std::unique_ptr &&c int facet = facets_to_check[facet_idx]; const Vec3f& facet_normal = m_face_normals[m_triangles[facet].source_triangle]; Matrix3f normal_matrix = static_cast(trafo_no_translate.matrix().block(0, 0, 3, 3).inverse().transpose().cast()); - float world_normal_z = (normal_matrix* facet_normal).normalized().z(); - if (!visited[facet] && (highlight_by_angle_deg == 0.f || world_normal_z < highlight_angle_limit)) { + float world_normal_dot = (normal_matrix * facet_normal).normalized().dot(up_direction); + if (!visited[facet] && (highlight_by_angle_deg == 0.f || world_normal_dot < highlight_angle_limit)) { if (select_triangle(facet, new_state, triangle_splitting)) { // add neighboring facets to list to be processed later for (int neighbor_idx : m_neighbors[facet]) @@ -320,7 +320,7 @@ bool TriangleSelector::is_facet_clipped(int facet_idx, const ClippingPlane &clp) void TriangleSelector::seed_fill_select_triangles(const Vec3f &hit, int facet_start, const Transform3d& trafo_no_translate, const ClippingPlane &clp, float seed_fill_angle, float highlight_by_angle_deg, - bool force_reselection) + bool force_reselection, const Vec3f &up_direction) { assert(facet_start < m_orig_size_indices); @@ -344,8 +344,8 @@ void TriangleSelector::seed_fill_select_triangles(const Vec3f &hit, int facet_st const Vec3f &facet_normal = m_face_normals[m_triangles[current_facet].source_triangle]; Matrix3f normal_matrix = static_cast(trafo_no_translate.matrix().block(0, 0, 3, 3).inverse().transpose().cast()); - float world_normal_z = (normal_matrix * facet_normal).normalized().z(); - if (!visited[current_facet] && (highlight_by_angle_deg == 0.f || world_normal_z < highlight_angle_limit)) { + float world_normal_dot = (normal_matrix * facet_normal).normalized().dot(up_direction); + if (!visited[current_facet] && (highlight_by_angle_deg == 0.f || world_normal_dot < highlight_angle_limit)) { if (m_triangles[current_facet].is_split()) { for (int split_triangle_idx = 0; split_triangle_idx <= m_triangles[current_facet].number_of_split_sides(); ++split_triangle_idx) { assert(split_triangle_idx < int(m_triangles[current_facet].children.size())); diff --git a/src/libslic3r/TriangleSelector.hpp b/src/libslic3r/TriangleSelector.hpp index 50bbdd4ed0..5b20647b84 100644 --- a/src/libslic3r/TriangleSelector.hpp +++ b/src/libslic3r/TriangleSelector.hpp @@ -308,7 +308,8 @@ public: EnforcerBlockerType new_state, // enforcer or blocker? const Transform3d &trafo_no_translate, // matrix to get from mesh to world without translation bool triangle_splitting, // If triangles will be split base on the cursor or not - float highlight_by_angle_deg = 0.f); // The maximal angle of overhang. If it is set to a non-zero value, it is possible to paint only the triangles of overhang defined by this angle in degrees. + float highlight_by_angle_deg = 0.f, // The maximal angle of overhang. If it is set to a non-zero value, it is possible to paint only the triangles of overhang defined by this angle in degrees. + const Vec3f &up_direction = Vec3f::UnitZ()); // Up direction for overhang detection (accounts for build plate tilt) void seed_fill_select_triangles(const Vec3f &hit, // point where to start int facet_start, // facet of the original mesh (unsplit) that the hit point belongs to @@ -316,7 +317,8 @@ public: const ClippingPlane &clp, // Clipping plane to limit painting to not clipped facets only float seed_fill_angle, // the maximal angle between two facets to be painted by the same color float highlight_by_angle_deg = 0.f, // The maximal angle of overhang. If it is set to a non-zero value, it is possible to paint only the triangles of overhang defined by this angle in degrees. - bool force_reselection = false); // force reselection of the triangle mesh even in cases that mouse is pointing on the selected triangle + bool force_reselection = false, // force reselection of the triangle mesh even in cases that mouse is pointing on the selected triangle + const Vec3f &up_direction = Vec3f::UnitZ()); // Up direction for overhang detection (accounts for build plate tilt) void bucket_fill_select_triangles(const Vec3f &hit, // point where to start int facet_start, // facet of the original mesh (unsplit) that the hit point belongs to diff --git a/src/slic3r/GUI/3DBed.cpp b/src/slic3r/GUI/3DBed.cpp index 544a3f8f12..3f5cd306ad 100644 --- a/src/slic3r/GUI/3DBed.cpp +++ b/src/slic3r/GUI/3DBed.cpp @@ -394,6 +394,8 @@ void Bed3D::render_internal(GLCanvas3D& canvas, const Transform3d& view_matrix, case Type::Custom: { render_custom(canvas, view_matrix, projection_matrix, bottom); break; } } + render_gravity_arrow(view_matrix, projection_matrix); + glsafe(::glDisable(GL_DEPTH_TEST)); } @@ -731,6 +733,63 @@ void Bed3D::render_custom(GLCanvas3D& canvas, const Transform3d& view_matrix, co render_texture(bottom, canvas);*/ } +void Bed3D::render_gravity_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix) +{ + const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + double tilt_x_deg = cfg.opt_float("build_plate_tilt_x"); + double tilt_y_deg = cfg.opt_float("build_plate_tilt_y"); + if (tilt_x_deg == 0. && tilt_y_deg == 0.) { + m_gravity_arrow.reset(); + return; + } + + // Gravity direction (matching the slicer's tilt convention) + double tilt_x_rad = Geometry::deg2rad(tilt_x_deg); + double tilt_y_rad = Geometry::deg2rad(tilt_y_deg); + Vec3d gravity_dir = Vec3d(-tan(tilt_y_rad), -tan(tilt_x_rad), -1.0).normalized(); + + // Build the arrow model (same dimensions as the axis arrows) + if (!m_gravity_arrow.is_initialized()) { + const float stem_length = Axes::DefaultStemLength; + const float tip_radius = Axes::DefaultTipRadius; + const float tip_length = Axes::DefaultTipLength; + const float stem_radius = stem_length / 75.f; // same ratio as axis cylinders + m_gravity_arrow.init_from(stilized_arrow(16, tip_radius, tip_length, stem_radius, stem_length)); + } + + // The arrow model points along +Z by default. Compute rotation to align with gravity_dir. + // Rotation axis = cross(+Z, gravity_dir), angle = acos(dot(+Z, gravity_dir)) + Vec3d from = Vec3d::UnitZ(); + Vec3d to = gravity_dir; + double dot = from.dot(to); + Transform3d rot = Transform3d::Identity(); + if (dot < -0.9999) { + // Nearly opposite — rotate 180° around X + rot = Eigen::AngleAxisd(M_PI, Vec3d::UnitX()) * rot; + } else if (dot < 0.9999) { + Vec3d axis = from.cross(to).normalized(); + double angle = std::acos(std::clamp(dot, -1.0, 1.0)); + rot = Eigen::AngleAxisd(angle, axis) * rot; + } + + GLShaderProgram* shader = wxGetApp().get_shader("flat"); + if (shader == nullptr) + return; + + glsafe(::glEnable(GL_DEPTH_TEST)); + shader->start_using(); + + const Camera& camera = wxGetApp().plater()->get_camera(); + Transform3d model_matrix = rot; + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * model_matrix); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + + m_gravity_arrow.set_color({ 1.0f, 0.85f, 0.0f, 1.0f }); // yellow + m_gravity_arrow.render(); + + shader->stop_using(); +} + void Bed3D::render_default(bool bottom, const Transform3d& view_matrix, const Transform3d& projection_matrix) { // m_texture.reset(); diff --git a/src/slic3r/GUI/3DBed.hpp b/src/slic3r/GUI/3DBed.hpp index b791635fd3..b16ead575b 100644 --- a/src/slic3r/GUI/3DBed.hpp +++ b/src/slic3r/GUI/3DBed.hpp @@ -110,6 +110,7 @@ private: //GLTexture m_temp_texture; GLModel m_model; Vec3d m_model_offset{ Vec3d::Zero() }; + GLModel m_gravity_arrow; Axes m_axes; float m_scale_factor{ 1.0f }; @@ -177,7 +178,8 @@ private: void render_model(const Transform3d& view_matrix, const Transform3d& projection_matrix); void render_custom(GLCanvas3D& canvas, const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom); void render_default(bool bottom, const Transform3d& view_matrix, const Transform3d& projection_matrix); - + void render_gravity_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix); + // BBS: remove the bed picking logic // void register_raycasters_for_picking(const GLModel::Geometry& geometry, const Transform3d& trafo); }; diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index e23efc7fbd..5caf6a4e0d 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -1078,13 +1078,27 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type, bool enable_support; int support_threshold_angle = get_selection_support_threshold_angle(enable_support); - + float normal_z = -::cos(Geometry::deg2rad((float) support_threshold_angle)); - + + // Compute up direction accounting for build plate tilt + Vec3f up_direction = Vec3f::UnitZ(); + { + const DynamicPrintConfig& prt_cfg = GUI::wxGetApp().preset_bundle->printers.get_edited_preset().config; + double tilt_x_deg = prt_cfg.opt_float("build_plate_tilt_x"); + double tilt_y_deg = prt_cfg.opt_float("build_plate_tilt_y"); + if (tilt_x_deg != 0. || tilt_y_deg != 0.) { + double tilt_x_rad = Geometry::deg2rad(tilt_x_deg); + double tilt_y_rad = Geometry::deg2rad(tilt_y_deg); + up_direction = Vec3f(float(tan(tilt_y_rad)), float(tan(tilt_x_rad)), 1.f).normalized(); + } + } + shader->set_uniform("volume_world_matrix", volume.first->world_matrix()); shader->set_uniform("slope.actived", m_slope.isGlobalActive && !volume.first->is_modifier && !volume.first->is_wipe_tower); shader->set_uniform("slope.volume_world_normal_matrix", static_cast(volume.first->world_matrix().matrix().block(0, 0, 3, 3).inverse().transpose().cast())); shader->set_uniform("slope.normal_z", normal_z); + shader->set_uniform("slope.up_direction", up_direction); #if ENABLE_ENVIRONMENT_MAP unsigned int environment_texture_id = GUI::wxGetApp().plater()->get_environment_texture_id(); diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index 9486bed67a..47e6246509 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -64,6 +64,7 @@ static SettingsFactory::Bundle FREQ_SETTINGS_BUNDLE_FFF = { L("Support") , { "enable_support", "support_type", "support_threshold_angle", "support_threshold_overlap", "support_base_pattern", "support_on_build_plate_only","support_critical_regions_only", "support_remove_small_overhang", + "build_plate_tilt_x", "build_plate_tilt_y", "support_base_pattern_spacing", "support_expansion"}}, //BBS { L("Flush options") , { "flush_into_infill", "flush_into_objects", "flush_into_support"} } @@ -94,7 +95,8 @@ std::map> SettingsFactory::OBJECT_C {"support_bottom_z_distance", "",19},{"support_top_z_distance", "",20},{"support_base_pattern", "",21},{"support_base_pattern_spacing", "",22}, {"support_interface_top_layers", "",23},{"support_interface_bottom_layers", "",24},{"support_interface_spacing", "",25},{"support_bottom_interface_spacing", "",26}, {"support_object_xy_distance", "",27}, {"bridge_no_support", "",28},{"max_bridge_length", "",29},{"support_critical_regions_only", "",30},{"support_remove_small_overhang","",31}, - {"support_object_first_layer_gap","",32} + {"build_plate_tilt_x","",32},{"build_plate_tilt_y","",33}, + {"support_object_first_layer_gap","",34} }}, { L("Speed"), {{"support_speed", "",12}, {"support_interface_speed", "",13} }} diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp index 02f1b4be5f..88781f6006 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp @@ -537,6 +537,14 @@ int GLGizmoFdmSupports::get_selection_support_threshold_angle() return auto_support ? support_threshold_angle : 0; } +std::pair GLGizmoFdmSupports::get_build_plate_tilt() +{ + const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + double tilt_x = cfg.opt_float("build_plate_tilt_x"); + double tilt_y = cfg.opt_float("build_plate_tilt_y"); + return {tilt_x, tilt_y}; +} + void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block) { float threshold = (float(M_PI)/180.f)*threshold_deg; @@ -544,6 +552,12 @@ void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block) const ModelObject* mo = m_c->selection_info()->model_object(); const ModelInstance* mi = mo->instances[selection.get_instance_idx()]; + // Compute gravity direction accounting for build plate tilt + auto [tilt_x_deg, tilt_y_deg] = get_build_plate_tilt(); + double tilt_x_rad = tilt_x_deg * M_PI / 180.0; + double tilt_y_rad = tilt_y_deg * M_PI / 180.0; + Vec3d gravity_dir = Vec3d(-tan(tilt_y_rad), -tan(tilt_x_rad), -1.0).normalized(); + int mesh_id = -1; for (const ModelVolume* mv : mo->volumes) { if (! mv->is_model_part()) @@ -552,10 +566,8 @@ void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block) ++mesh_id; const Transform3d trafo_matrix = mi->get_matrix_no_offset() * mv->get_matrix_no_offset(); - Vec3f down = (trafo_matrix.inverse() * (-Vec3d::UnitZ())).cast().normalized(); - Vec3f limit = (trafo_matrix.inverse() * Vec3d(std::sin(threshold), 0, -std::cos(threshold))).cast().normalized(); - - float dot_limit = limit.dot(down); + Vec3f down = (trafo_matrix.inverse() * gravity_dir).cast().normalized(); + float dot_limit = std::cos(threshold); // Now calculate dot product of vert_direction and facets' normals. int idx = 0; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp index ec0faeab80..0afdba7f19 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp @@ -59,6 +59,7 @@ private: void select_facets_by_angle(float threshold, bool block); // BBS int get_selection_support_threshold_angle(); + std::pair get_build_plate_tilt(); int m_support_threshold_angle = -1; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp index 7d3f40bd12..71e4c45f4a 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp @@ -73,6 +73,18 @@ GLGizmoPainterBase::ClippingPlaneDataWrapper GLGizmoPainterBase::get_clipping_pl return clp_data_out; } +Vec3f GLGizmoPainterBase::get_tilt_up_direction() const +{ + const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + double tilt_x_deg = cfg.opt_float("build_plate_tilt_x"); + double tilt_y_deg = cfg.opt_float("build_plate_tilt_y"); + if (tilt_x_deg == 0. && tilt_y_deg == 0.) + return Vec3f::UnitZ(); + double tilt_x_rad = Geometry::deg2rad(tilt_x_deg); + double tilt_y_rad = Geometry::deg2rad(tilt_y_deg); + return Vec3f(float(tan(tilt_y_rad)), float(tan(tilt_x_rad)), 1.f).normalized(); +} + void GLGizmoPainterBase::render_triangles(const Selection& selection) const { auto* shader = wxGetApp().get_shader("mm_gouraud"); @@ -119,11 +131,15 @@ void GLGizmoPainterBase::render_triangles(const Selection& selection) const float normal_z = -::cos(Geometry::deg2rad(m_highlight_by_angle_threshold_deg)); Matrix3f normal_matrix = static_cast(trafo_matrix.matrix().block(0, 0, 3, 3).inverse().transpose().cast()); + // Compute up direction accounting for build plate tilt + Vec3f up_direction = get_tilt_up_direction(); + shader->set_uniform("volume_world_matrix", trafo_matrix); shader->set_uniform("volume_mirrored", is_left_handed); shader->set_uniform("slope.actived", m_parent.is_using_slope()); shader->set_uniform("slope.volume_world_normal_matrix", normal_matrix); shader->set_uniform("slope.normal_z", normal_z); + shader->set_uniform("slope.up_direction", up_direction); m_triangle_selectors[mesh_id]->render(m_imgui, trafo_matrix); if (is_left_handed) @@ -691,7 +707,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous mi->get_assemble_transformation().get_matrix() * mo->volumes[m_rr.mesh_id]->get_matrix() : mi->get_transformation().get_matrix() * mo->volumes[m_rr.mesh_id]->get_matrix(); m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, int(m_rr.facet), trafo_matrix_not_translate, this->get_clipping_plane_in_volume_coordinates(trafo_matrix), m_smart_fill_angle, - m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, true); + m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, true, get_tilt_up_direction()); m_triangle_selectors[m_rr.mesh_id]->request_update_render_data(); m_seed_fill_last_mesh_id = m_rr.mesh_id; } @@ -803,7 +819,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous std::unique_ptr cursor = TriangleSelector::SinglePointCursor::cursor_factory(phr.z_world, camera_pos, m_cursor_height, trafo_matrix, clp); m_triangle_selectors[mesh_idx]->select_patch(int(phr.first_facet_idx), std::move(cursor), new_state, trafo_matrix_not_translate, - m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f); + m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, get_tilt_up_direction()); m_triangle_selectors[mesh_idx]->request_update_render_data(true); m_last_mouse_click = _mouse_position; @@ -855,7 +871,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous m_triangle_selectors[mesh_idx]->seed_fill_apply_on_triangles(new_state); if (m_tool_type == ToolType::SMART_FILL) m_triangle_selectors[mesh_idx]->seed_fill_select_triangles(mesh_hit, facet_idx, trafo_matrix_not_translate, clp, m_smart_fill_angle, - m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, true); + m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, true, get_tilt_up_direction()); else if (m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER) // BBS: add infill_angle parameter m_triangle_selectors[mesh_idx]->bucket_fill_select_triangles(mesh_hit, facet_idx, clp, -1.f, false, true); @@ -874,12 +890,12 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous camera_pos, m_cursor_radius, m_cursor_type, trafo_matrix, clp); m_triangle_selectors[mesh_idx]->select_patch(int(first_position.facet_idx), std::move(cursor), new_state, trafo_matrix_not_translate, - m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f); + m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, get_tilt_up_direction()); } else { for (auto first_position_it = projected_mouse_positions.cbegin(); first_position_it != projected_mouse_positions.cend() - 1; ++first_position_it) { auto second_position_it = first_position_it + 1; std::unique_ptr cursor = TriangleSelector::DoublePointCursor::cursor_factory(first_position_it->mesh_hit, second_position_it->mesh_hit, camera_pos, m_cursor_radius, m_cursor_type, trafo_matrix, clp); - m_triangle_selectors[mesh_idx]->select_patch(int(first_position_it->facet_idx), std::move(cursor), new_state, trafo_matrix_not_translate, m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f); + m_triangle_selectors[mesh_idx]->select_patch(int(first_position_it->facet_idx), std::move(cursor), new_state, trafo_matrix_not_translate, m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, get_tilt_up_direction()); } } } @@ -953,7 +969,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous const TriangleSelector::ClippingPlane &clp = this->get_clipping_plane_in_volume_coordinates(trafo_matrix); if (m_tool_type == ToolType::SMART_FILL) m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, int(m_rr.facet), trafo_matrix_not_translate, clp, m_smart_fill_angle, - m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f); + m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, false, get_tilt_up_direction()); else if (m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER) // BBS: add infill_angle parameter m_triangle_selectors[m_rr.mesh_id]->bucket_fill_select_triangles(m_rr.hit, int(m_rr.facet), clp, -1.f, false); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp index 5fc1a9e18f..9dbed7df7e 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp @@ -281,6 +281,9 @@ protected: bool m_paint_on_overhangs_only = false; float m_highlight_by_angle_threshold_deg = 0.f; + // Returns the up direction accounting for build plate tilt (default: UnitZ) + Vec3f get_tilt_up_direction() const; + GLModel m_circle; Vec2d m_old_center{ Vec2d::Zero() }; float m_old_cursor_radius{ 0.0f }; diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index c94fd1c7a4..6ac8ca9a0e 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4364,6 +4364,8 @@ void TabPrinter::build_fff() optgroup->append_single_option_line(option, "printer_basic_information_printable_space#excluded-bed-area"); // optgroup->append_single_option_line("printable_area"); optgroup->append_single_option_line("printable_height", "printer_basic_information_printable_space#printable-height"); + optgroup->append_single_option_line("build_plate_tilt_x"); + optgroup->append_single_option_line("build_plate_tilt_y"); optgroup->append_single_option_line("support_multi_bed_types","printer_basic_information_printable_space#support-multi-bed-types"); optgroup->append_single_option_line("best_object_pos", "printer_basic_information_printable_space#best-object-position"); // todo: for multi_extruder test From ed6ea086a26fa0b45db5d1faaedea01a5e190753 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 11 Mar 2026 03:58:52 -0500 Subject: [PATCH 002/434] Add belt printer transform pipeline: slicing rotation, G-code coords, preview - Implement core belt slicing pipeline: R(-alpha, X) mesh rotation in PrintObjectSlice with corrected object height calculation for proper layer count Add to_machine_coords() in GCodeWriter to convert slicing-frame coordinates back to machine-frame, propagated through GCode, GCodeProcessor, and GCodeViewer Add belt-mode UI: tilted bed visualization, slicing-direction arrow, and raw G-code toggle to switch between machine-frame and slicing-frame views This is a combination of 6 commits. checkpoint 1: initial MVP. Slicing functions, but rotates instead of skews are happening and a lot of other stuff too getting somewhere, getting to the point where I need to figure out how to verify this stuff this appears to be a dead end. getting somewhere I think maybe I'm pretty sure we've completely lost the plot at this point and need to restart this process... remove slice logic in preparation for new, more invasive plan --- result.json | 7 ++ src/libslic3r/Brim.cpp | 4 ++ src/libslic3r/BuildVolume.cpp | 25 +++++++ src/libslic3r/BuildVolume.hpp | 8 +++ src/libslic3r/GCode.cpp | 9 ++- src/libslic3r/GCode/GCodeProcessor.cpp | 10 +++ src/libslic3r/GCode/GCodeProcessor.hpp | 2 + src/libslic3r/GCodeWriter.cpp | 91 +++++++++++++++++++++----- src/libslic3r/GCodeWriter.hpp | 11 ++++ src/libslic3r/Preset.cpp | 2 +- src/libslic3r/Print.cpp | 20 ++++++ src/libslic3r/PrintConfig.cpp | 50 ++++++++++++-- src/libslic3r/PrintConfig.hpp | 4 ++ src/libslic3r/PrintObject.cpp | 5 +- src/libslic3r/PrintObjectSlice.cpp | 2 + src/slic3r/GUI/3DBed.cpp | 67 ++++++++++++++++++- src/slic3r/GUI/3DBed.hpp | 11 ++++ src/slic3r/GUI/ConfigManipulation.cpp | 25 +++++-- src/slic3r/GUI/GCodeViewer.cpp | 4 +- src/slic3r/GUI/GCodeViewer.hpp | 6 ++ src/slic3r/GUI/Plater.cpp | 18 +++++ src/slic3r/GUI/Tab.cpp | 25 +++++++ 22 files changed, 375 insertions(+), 31 deletions(-) create mode 100644 result.json diff --git a/result.json b/result.json new file mode 100644 index 0000000000..e309ad45a1 --- /dev/null +++ b/result.json @@ -0,0 +1,7 @@ +{ + "error_string": "Success.", + "export_time": 0, + "plate_index": 0, + "prepare_time": 0, + "return_code": 0 +} diff --git a/src/libslic3r/Brim.cpp b/src/libslic3r/Brim.cpp index 35d971a465..0534734c65 100644 --- a/src/libslic3r/Brim.cpp +++ b/src/libslic3r/Brim.cpp @@ -881,6 +881,10 @@ void make_brim(const Print& print, PrintTryCancel try_cancel, Polygons& islands_ std::vector> &objPrintVec, std::vector& printExtruders) { + // Belt printer: brim is not compatible with belt printing. + if (print.config().belt_printer.value) + return; + std::map brim_width_map; std::map brimAreaMap; std::map supportBrimAreaMap; diff --git a/src/libslic3r/BuildVolume.cpp b/src/libslic3r/BuildVolume.cpp index 3802fbc014..132794ccfe 100644 --- a/src/libslic3r/BuildVolume.cpp +++ b/src/libslic3r/BuildVolume.cpp @@ -176,6 +176,26 @@ BuildVolume::BuildVolume(const std::vector &printable_area, const double BOOST_LOG_TRIVIAL(debug) << "BuildVolume printable_area clasified as: " << this->type_name(); } +void BuildVolume::set_belt_printer(bool enabled, double angle_deg, bool infinite_y) +{ + m_is_belt_printer = enabled; + m_belt_angle = angle_deg; + m_belt_infinite_y = infinite_y; + + if (enabled) { + if (infinite_y) { + // Extend the Y bound to a very large value for infinite belt. + m_bboxf.max.y() = 100000.; + } + // Adjust max print height to diagonal reach: printable_height / sin(belt_angle). + if (angle_deg > 0. && angle_deg < 90.) { + double sin_a = std::sin(angle_deg * M_PI / 180.0); + if (sin_a > 0.) + m_bboxf.max.z() = m_max_print_height / sin_a; + } + } +} + #if 0 // Tests intersections of projected triangles, not just their vertices against a bounding box. // This test also correctly evaluates collision of a non-convex object with the bounding box. @@ -384,6 +404,11 @@ BuildVolume::ObjectState BuildVolume::object_state(const indexed_triangle_set& i build_volume.max.z() = std::numeric_limits::max(); if (ignore_bottom) build_volume.min.z() = -std::numeric_limits::max(); + // Belt printer: extend Y bounds for infinite Y. + if (m_is_belt_printer && m_belt_infinite_y) { + build_volume.min.y() = -std::numeric_limits::max(); + build_volume.max.y() = std::numeric_limits::max(); + } BoundingBox3Base build_volumef(build_volume.min.cast(), build_volume.max.cast()); // The following test correctly interprets intersection of a non-convex object with a rectangular build volume. //return rectangle_test(its, trafo, to_2d(build_volume.min), to_2d(build_volume.max), build_volume.max.z()); diff --git a/src/libslic3r/BuildVolume.hpp b/src/libslic3r/BuildVolume.hpp index 6f53fdd704..0494efe9ba 100644 --- a/src/libslic3r/BuildVolume.hpp +++ b/src/libslic3r/BuildVolume.hpp @@ -57,6 +57,10 @@ public: // Initialize from PrintConfig::printable_area and PrintConfig::printable_height BuildVolume(const std::vector &printable_area, const double printable_height, const std::vector> &extruder_areas, const std::vector& extruder_printable_heights); + // Belt printer configuration. + void set_belt_printer(bool enabled, double angle_deg, bool infinite_y); + bool is_belt_printer() const { return m_is_belt_printer; } + // Source data, unscaled coordinates. const std::vector& printable_area() const { return m_bed_shape; } double printable_height() const { return m_max_print_height; } @@ -139,6 +143,10 @@ private: // Source definition of the print volume height (PrintConfig::printable_height) double m_max_print_height { 0.f }; std::vector m_extruder_printable_height; + // Belt printer state. + bool m_is_belt_printer { false }; + double m_belt_angle { 0. }; + bool m_belt_infinite_y { false }; // Derived values. BuildVolume_Type m_type { BuildVolume_Type::Invalid }; diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index fe10cbcd41..bff35d321c 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2413,6 +2413,10 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_writer.set_is_bbl_machine(is_bbl_printers); + // Belt printer: initialize coordinate transformation on the writer. + if (print.config().belt_printer.value) + m_writer.set_belt_angle(print.config().belt_printer_angle.value); + // How many times will be change_layer() called? // change_layer() in turn increments the progress bar status. m_layer_count = 0; @@ -2502,6 +2506,9 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.write_format("; HEADER_BLOCK_START\n"); // Write information on the generator. file.write_format("; generated by %s on %s\n", Slic3r::header_slic3r_generated().c_str(), Slic3r::Utils::local_timestamp().c_str()); + // Belt printer: embed angle in header for G-code processor detection. + if (print.config().belt_printer.value) + file.write_format("; belt_printer_angle = %.1f\n", print.config().belt_printer_angle.value); if (is_bbl_printers) file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str()); //BBS: total layer number @@ -6704,7 +6711,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } // BBS: use G1 if not enable arc fitting or has no arc fitting result or in spiral_mode mode or we are doing sloped extrusion // Attention: G2 and G3 is not supported in spiral_mode mode - if (!m_config.enable_arc_fitting || path.polyline.fitting_result.empty() || m_config.spiral_mode || sloped != nullptr) { + if (!m_config.enable_arc_fitting || path.polyline.fitting_result.empty() || m_config.spiral_mode || sloped != nullptr || m_config.belt_printer) { double path_length = 0.; double total_length = sloped == nullptr ? 0. : path.polyline.length() * SCALING_FACTOR; for (const Line& line : path.polyline.lines()) { diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 906c2070e4..0dc00e1522 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -2597,6 +2597,8 @@ void GCodeProcessor::finalize(bool post_process) } } + // Belt printer: preview coordinate transform placeholder (to be implemented in next cycle). + calculate_time(m_result); // process the time blocks @@ -3044,6 +3046,14 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers return; } + // Belt printer angle detection from G-code header comment. + if (boost::starts_with(comment, " belt_printer_angle = ")) { + try { + m_result.belt_printer_angle = std::stof(std::string(comment.substr(22))); + } catch (...) {} + return; + } + // wipe start tag if (boost::starts_with(comment, reserved_tag(ETags::Wipe_Start))) { m_wiping = true; diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index 1d72b1c614..e432a165a8 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -227,6 +227,8 @@ class Print; bool support_traditional_timelapse{true}; float printable_height; float z_offset; + // Belt printer: angle for coordinate transformation in preview. + float belt_printer_angle{ 0.f }; SettingsIds settings_ids; size_t filaments_count; bool backtrace_enabled; diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index 1aff72daeb..1b056a946f 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -1,5 +1,6 @@ #include "GCodeWriter.hpp" #include "CustomGCode.hpp" +#include "Geometry.hpp" #include "PrintConfig.hpp" #include #include @@ -19,6 +20,19 @@ namespace Slic3r { bool GCodeWriter::full_gcode_comment = true; +void GCodeWriter::set_belt_angle(double angle_deg) +{ + m_belt_angle_rad = Geometry::deg2rad(angle_deg); + m_belt_cos = std::cos(m_belt_angle_rad); + m_belt_sin = std::sin(m_belt_angle_rad); +} + +Vec3d GCodeWriter::to_machine_coords(const Vec3d &pos) const +{ + // Belt printer: coordinate transform placeholder (to be implemented in next cycle). + return pos; +} + bool GCodeWriter::supports_separate_travel_acceleration(GCodeFlavor flavor) { return (flavor == gcfRepetier || flavor == gcfMarlinFirmware || flavor == gcfRepRapFirmware); @@ -547,7 +561,13 @@ std::string GCodeWriter::travel_to_xy(const Vec2d &point, const std::string &com Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; GCodeG1Formatter w; - w.emit_xy(point_on_plate); + if (is_belt_printer()) { + // Belt printer: transform to machine coordinates (XY travel also needs Z due to YZ rotation) + Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); + w.emit_xyz(machine); + } else { + w.emit_xy(point_on_plate); + } auto speed = m_is_first_layer ? this->config.get_abs_value("initial_layer_travel_speed") : this->config.travel_speed.value; w.emit_f(speed * 60.0); @@ -561,6 +581,11 @@ it will not perform subsequent lifts, even if Z was raised manually (i.e. with travel_to_z()) and thus _lifted was reduced. */ std::string GCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase) { + // Belt printer: force NormalLift since SpiralLift and SlopeLift compute slope angles + // that don't account for the YZ coordinate rotation. + if (is_belt_printer()) + lift_type = LiftType::NormalLift; + // check whether the above/below conditions are met double target_lift = 0; { @@ -587,8 +612,10 @@ std::string GCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase) } // BBS: immediately execute an undelayed lift move with a spiral lift pattern -// designed specifically for subsequent gcode injection (e.g. timelapse) +// designed specifically for subsequent gcode injection (e.g. timelapse) std::string GCodeWriter::eager_lift(const LiftType type) { + // Belt printer: force NormalLift (SpiralLift/SlopeLift don't account for YZ rotation). + const LiftType effective_type = is_belt_printer() ? LiftType::NormalLift : type; std::string lift_move; double target_lift = 0; { @@ -603,7 +630,7 @@ std::string GCodeWriter::eager_lift(const LiftType type) { // BBS: spiral lift only safe with known position // TODO: check the arc will move within bed area - if (type == LiftType::SpiralLift && this->is_current_position_clear()) { + if (effective_type == LiftType::SpiralLift && this->is_current_position_clear()) { double radius = target_lift / (2 * PI * atan(filament()->travel_slope())); // static spiral alignment when no move in x,y plane. // spiral centra is a radius distance to the right (y=0) @@ -675,6 +702,8 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co // / to make the z list early to avoid to hit some warping place when travel is long. Vec2d temp = delta_no_z.normalized() * delta(2) / tan(this->filament()->travel_slope()); Vec3d slope_top_point = Vec3d(temp(0), temp(1), delta(2)) + source; + if (is_belt_printer()) + slope_top_point = to_machine_coords(slope_top_point); GCodeG1Formatter w0; w0.emit_xyz(slope_top_point); w0.emit_f(travel_speed * 60.0); @@ -689,18 +718,27 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co std::string xy_z_move; { + Vec3d emit_target = is_belt_printer() ? to_machine_coords(target) : target; GCodeG1Formatter w0; if (this->is_current_position_clear()) { - w0.emit_xyz(target); + w0.emit_xyz(emit_target); w0.emit_f(travel_speed * 60.0); w0.emit_comment(GCodeWriter::full_gcode_comment, comment); xy_z_move = w0.string(); } else { - w0.emit_xy(Vec2d(target.x(), target.y())); - w0.emit_f(travel_speed * 60.0); - w0.emit_comment(GCodeWriter::full_gcode_comment, comment); - xy_z_move = w0.string() + _travel_to_z(target.z(), comment); + if (is_belt_printer()) { + // Belt mode: can't split XY and Z moves independently, emit full XYZ + w0.emit_xyz(emit_target); + w0.emit_f(travel_speed * 60.0); + w0.emit_comment(GCodeWriter::full_gcode_comment, comment); + xy_z_move = w0.string(); + } else { + w0.emit_xy(Vec2d(target.x(), target.y())); + w0.emit_f(travel_speed * 60.0); + w0.emit_comment(GCodeWriter::full_gcode_comment, comment); + xy_z_move = w0.string() + _travel_to_z(target.z(), comment); + } } } m_pos = dest_point; @@ -726,15 +764,25 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co //BBS: take plate offset into consider Vec3d point_on_plate = { dest_point(0) - m_x_offset, dest_point(1) - m_y_offset, dest_point(2) }; + if (is_belt_printer()) + point_on_plate = to_machine_coords(point_on_plate); std::string out_string; GCodeG1Formatter w; if (!this->is_current_position_clear()) { - //force to move xy first then z after filament change - w.emit_xy(Vec2d(point_on_plate.x(), point_on_plate.y())); - w.emit_f(this->config.travel_speed.value * 60.0); - w.emit_comment(GCodeWriter::full_gcode_comment, comment); - out_string = w.string() + _travel_to_z(point_on_plate.z(), comment); + if (is_belt_printer()) { + // Belt mode: emit full XYZ since Y and Z are coupled + w.emit_xyz(point_on_plate); + w.emit_f(this->config.travel_speed.value * 60.0); + w.emit_comment(GCodeWriter::full_gcode_comment, comment); + out_string = w.string(); + } else { + //force to move xy first then z after filament change + w.emit_xy(Vec2d(point_on_plate.x(), point_on_plate.y())); + w.emit_f(this->config.travel_speed.value * 60.0); + w.emit_comment(GCodeWriter::full_gcode_comment, comment); + out_string = w.string() + _travel_to_z(point_on_plate.z(), comment); + } } else { GCodeG1Formatter w; w.emit_xyz(point_on_plate); @@ -778,7 +826,13 @@ std::string GCodeWriter::_travel_to_z(double z, const std::string &comment) } GCodeG1Formatter w; - w.emit_z(z); + if (is_belt_printer()) { + // Belt printer: a Z-only move in slicing frame needs to emit both Y and Z in machine coords. + Vec3d machine = to_machine_coords(Vec3d(m_pos.x() - m_x_offset, m_pos.y() - m_y_offset, z)); + w.emit_xyz(machine); + } else { + w.emit_z(z); + } w.emit_f(speed * 60.0); //BBS w.emit_comment(GCodeWriter::full_gcode_comment, comment); @@ -883,7 +937,12 @@ std::string GCodeWriter::extrude_to_xy(const Vec2d &point, double dE, const std: Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; GCodeG1Formatter w; - w.emit_xy(point_on_plate); + if (is_belt_printer()) { + Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); + w.emit_xyz(machine); + } else { + w.emit_xy(point_on_plate); + } if (!force_no_extrusion) w.emit_e(filament()->E()); //BBS @@ -924,6 +983,8 @@ std::string GCodeWriter::extrude_to_xyz(const Vec3d &point, double dE, const std Vec3d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset, point(2) }; GCodeG1Formatter w; + if (is_belt_printer()) + point_on_plate = to_machine_coords(point_on_plate); w.emit_xyz(point_on_plate); if (!force_no_extrusion) w.emit_e(filament()->E()); diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index dbc64694e2..10159d941f 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -125,6 +125,12 @@ public: void set_is_first_layer(bool bval) { m_is_first_layer = bval; } GCodeFlavor get_gcode_flavor() const { return config.gcode_flavor; } + // Belt printer: set the belt angle and precompute sin/cos for coordinate transformation. + void set_belt_angle(double angle_deg); + bool is_belt_printer() const { return m_belt_angle_rad != 0.; } + // Transform a point from the slicing frame to machine/world coordinates (inverse shear). + Vec3d to_machine_coords(const Vec3d &pos) const; + // Returns whether this flavor supports separate print and travel acceleration. static bool supports_separate_travel_acceleration(GCodeFlavor flavor); private: @@ -177,6 +183,11 @@ public: //SoftFever bool m_is_bbl_printers = false; + + // Belt printer coordinate transformation (YZ shear) + double m_belt_angle_rad = 0.; + double m_belt_cos = 1.0; + double m_belt_sin = 0.0; double m_current_speed; bool m_is_first_layer = true; diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index ef2c83b13c..98532bccc7 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1010,7 +1010,7 @@ static std::vector s_Preset_machine_limits_options { static std::vector s_Preset_printer_options { "printer_technology", - "printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "build_plate_tilt_x", "build_plate_tilt_y", "gcode_flavor", + "printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "build_plate_tilt_x", "build_plate_tilt_y", "belt_printer", "belt_printer_angle", "belt_printer_infinite_y", "gcode_flavor", "fan_kickstart", "fan_speedup_time", "fan_speedup_overhangs", "single_extruder_multi_material", "manual_filament_change", "file_start_gcode", "machine_start_gcode", "machine_end_gcode", "before_layer_change_gcode", "printing_by_object_gcode", "layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode", "change_filament_gcode", "change_extrusion_role_gcode", "printer_model", "printer_variant", "printer_extruder_id", "printer_extruder_variant", "extruder_variant_list", "default_nozzle_volume_type", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 6463338f7d..f245cc32b6 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -565,6 +565,9 @@ std::vector Print::print_object_ids() const bool Print::has_infinite_skirt() const { + // Belt printer: no skirt support. + if (m_config.belt_printer.value) + return false; // Orca: unclear why (m_config.ooze_prevention && this->extruders().size() > 1) logic is here, removed. // return (m_config.draft_shield == dsEnabled && m_config.skirt_loops > 0) || (m_config.ooze_prevention && this->extruders().size() > 1); @@ -573,6 +576,9 @@ bool Print::has_infinite_skirt() const bool Print::has_skirt() const { + // Belt printer: no skirt support. + if (m_config.belt_printer.value) + return false; return (m_config.skirt_height > 0); } @@ -1194,6 +1200,16 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* if (extruders.empty()) return { L("No extrusions under current settings.") }; + // Belt printer validation: incompatible features. + if (m_config.belt_printer.value) { + for (const PrintObject *object : m_objects) { + if (object->config().raft_layers > 0) + return { L("Raft is not compatible with belt printer mode.") }; + } + if (m_config.draft_shield != dsDisabled) + return { L("Draft shield is not compatible with belt printer mode.") }; + } + if (nozzles < 2 && extruders.size() > 1) { auto ret = check_multi_filament_valid(*this); if (!ret.string.empty()) @@ -2501,6 +2517,10 @@ std::string Print::export_gcode(const std::string& path_template, GCodeProcessor void Print::_make_skirt() { + // Belt printer: skirt is not compatible. + if (m_config.belt_printer.value) + return; + // First off we need to decide how tall the skirt must be. // The skirt_height option from config is expressed in layers, but our // object might have different layer heights, so we need to find the print_z diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 918fc1b97f..7eea294d4e 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -2,6 +2,7 @@ #include "PrintConfigConstants.hpp" #include "ClipperUtils.hpp" #include "Config.hpp" +#include "Geometry.hpp" #include "MaterialType.hpp" #include "I18N.hpp" #include "format.hpp" @@ -5923,10 +5924,11 @@ void PrintConfigDef::init_fff_params() def->category = L("Support"); def->tooltip = L("Tilt angle of the build plate along the X axis. " "A positive value tilts the plate so the +X side is higher, shifting gravity toward -X and increasing overhangs on the +X side. " - "A negative value tilts the -X side higher. Set to 0 for no X-axis tilt."); + "A negative value tilts the -X side higher. Set to 0 for no X-axis tilt. " + "In belt printer mode, this is automatically synced to the belt angle."); def->sidetext = u8"\u00B0"; - def->min = -45; - def->max = 45; + def->min = -90; + def->max = 90; def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(0.)); @@ -5937,11 +5939,41 @@ void PrintConfigDef::init_fff_params() "A positive value tilts the plate so the +Y side is higher, shifting gravity toward -Y and increasing overhangs on the +Y side. " "A negative value tilts the -Y side higher. Set to 0 for no Y-axis tilt."); def->sidetext = u8"\u00B0"; - def->min = -45; - def->max = 45; + def->min = -90; + def->max = 90; def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(0.)); + def = this->add("belt_printer", coBool); + def->label = L("Belt printer"); + def->category = L("Printable space"); + def->tooltip = L("Enable belt printer mode. Belt printers use a conveyor belt as the build surface, " + "tilted at an angle (typically 45 degrees). The slicer will rotate the slicing plane " + "and transform G-code coordinates for the tilted build surface."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + + def = this->add("belt_printer_angle", coFloat); + def->label = L("Belt angle"); + def->category = L("Printable space"); + def->tooltip = L("The tilt angle of the belt surface in degrees. " + "Most belt printers use a 45-degree angle. " + "This controls the rotation applied to the slicing plane and G-code coordinates."); + def->sidetext = u8"\u00B0"; + def->min = 0; + def->max = 90; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(45.)); + + def = this->add("belt_printer_infinite_y", coBool); + def->label = L("Infinite Y axis"); + def->category = L("Printable space"); + def->tooltip = L("Enable infinite Y axis for belt printers. " + "When enabled, the Y axis build volume limit is effectively removed, " + "allowing objects of any length to be printed along the belt direction."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(true)); + def = this->add("tree_support_branch_angle", coFloat); def->label = L("Tree support branch angle"); def->category = L("Support"); @@ -10823,10 +10855,16 @@ Polygons get_bed_excluded_area(const PrintConfig& cfg) { const Pointfs exclude_area_points = cfg.bed_exclude_area.values; + // Belt printer: project exclusion zone points from belt surface to machine-frame XY. + // On the belt surface, Z=0, so machine_Y = belt_Y * cos(angle). + const bool is_belt = cfg.belt_printer.value; + const double belt_cos = is_belt ? std::cos(Geometry::deg2rad(cfg.belt_printer_angle.value)) : 1.0; + Polygon exclude_poly; for (int i = 0; i < exclude_area_points.size(); i++) { auto pt = exclude_area_points[i]; - exclude_poly.points.emplace_back(scale_(pt.x()), scale_(pt.y())); + double y = is_belt ? pt.y() * belt_cos : pt.y(); + exclude_poly.points.emplace_back(scale_(pt.x()), scale_(y)); } exclude_poly.make_counter_clockwise(); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index c862956544..f36b5d3e93 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1413,6 +1413,10 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( // Build plate tilt for off-axis gravity support generation (printer-level setting). ((ConfigOptionFloat, build_plate_tilt_x)) ((ConfigOptionFloat, build_plate_tilt_y)) + // Belt printer settings (printer-level). + ((ConfigOptionBool, belt_printer)) + ((ConfigOptionFloat, belt_printer_angle)) + ((ConfigOptionBool, belt_printer_infinite_y)) //BBS ((ConfigOptionInts, additional_cooling_fan_speed)) ((ConfigOptionBool, reduce_crossing_wall)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index fc68fce76b..979953fb94 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -3391,7 +3391,9 @@ void PrintObject::update_slicing_parameters() { // Orca: updated function call for XYZ shrinkage compensation if (!m_slicing_params.valid) { - m_slicing_params = SlicingParameters::create_from_config(this->print()->config(), m_config, this->model_object()->max_z(), + coordf_t object_height = this->model_object()->max_z(); + // Belt printer: height adjustment placeholder (to be implemented in next cycle). + m_slicing_params = SlicingParameters::create_from_config(this->print()->config(), m_config, object_height, this->object_extruders(), this->print()->shrinkage_compensation()); } } @@ -3432,6 +3434,7 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full if (object_max_z <= 0.f) object_max_z = (float)model_object.raw_bounding_box().size().z(); + // Belt printer: height adjustment placeholder (to be implemented in next cycle). return SlicingParameters::create_from_config(print_config, object_config, object_max_z, object_extruders, object_shrinkage_compensation); } diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 2d67ac0939..bba4593764 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -8,6 +8,7 @@ #include "Layer.hpp" #include "MultiMaterialSegmentation.hpp" #include "Print.hpp" +#include "Geometry.hpp" //BBS #include "ShortestPath.hpp" #include "libslic3r/Feature/Interlocking/InterlockingGenerator.hpp" @@ -139,6 +140,7 @@ static std::vector slice_volumes_inner( params_base.closing_radius = print_object_config.slice_closing_radius.value; params_base.extra_offset = 0; params_base.trafo = object_trafo; + // Belt printer: mesh transform placeholder (to be implemented in next cycle). //BBS: 0.0025mm is safe enough to simplify the data to speed slicing up for high-resolution model. //Also has on influence on arc fitting which has default resolution 0.0125mm. params_base.resolution = print_config.resolution <= 0.001 ? 0.0f : 0.0025; diff --git a/src/slic3r/GUI/3DBed.cpp b/src/slic3r/GUI/3DBed.cpp index 3f5cd306ad..101279d744 100644 --- a/src/slic3r/GUI/3DBed.cpp +++ b/src/slic3r/GUI/3DBed.cpp @@ -387,14 +387,18 @@ void Bed3D::render_internal(GLCanvas3D& canvas, const Transform3d& view_matrix, m_model.set_color(m_is_dark ? DEFAULT_MODEL_COLOR_DARK : DEFAULT_MODEL_COLOR); + // Belt printer: bed view transform placeholder (to be implemented in next cycle). + Transform3d belt_view_matrix = view_matrix; + switch (m_type) { - case Type::System: { render_system(canvas, view_matrix, projection_matrix, bottom); break; } + case Type::System: { render_system(canvas, belt_view_matrix, projection_matrix, bottom); break; } default: - case Type::Custom: { render_custom(canvas, view_matrix, projection_matrix, bottom); break; } + case Type::Custom: { render_custom(canvas, belt_view_matrix, projection_matrix, bottom); break; } } render_gravity_arrow(view_matrix, projection_matrix); + render_slicing_plane(view_matrix, projection_matrix); glsafe(::glDisable(GL_DEPTH_TEST)); } @@ -738,6 +742,10 @@ void Bed3D::render_gravity_arrow(const Transform3d& view_matrix, const Transform const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; double tilt_x_deg = cfg.opt_float("build_plate_tilt_x"); double tilt_y_deg = cfg.opt_float("build_plate_tilt_y"); + // Belt printer: auto-derive gravity direction from belt angle if belt mode is active. + if (m_is_belt_printer && m_belt_angle > 0.f) { + tilt_x_deg = m_belt_angle; + } if (tilt_x_deg == 0. && tilt_y_deg == 0.) { m_gravity_arrow.reset(); return; @@ -790,6 +798,61 @@ void Bed3D::render_gravity_arrow(const Transform3d& view_matrix, const Transform shader->stop_using(); } +void Bed3D::render_slicing_plane(const Transform3d& view_matrix, const Transform3d& projection_matrix) +{ + if (!m_is_belt_printer || m_belt_angle <= 0.f) + return; + + // Build a quad in the XZ plane (world frame) representing the belt slicing plane. + // The plane is tilted at belt_angle from horizontal, with normal (0, -sin(a), cos(a)). + // We render it as a semi-transparent quad centered on the build plate. + if (!m_slicing_plane.is_initialized()) { + const float half_size = 120.f; // mm, large enough to be visible + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 }; + init_data.reserve_vertices(4); + init_data.reserve_indices(2); // 2 triangles + + // Quad corners in local frame (XY plane, will be rotated to match slicing plane) + Vec3f n = Vec3f::UnitZ(); + init_data.add_vertex(Vec3f(-half_size, -half_size, 0.f), n); + init_data.add_vertex(Vec3f( half_size, -half_size, 0.f), n); + init_data.add_vertex(Vec3f( half_size, half_size, 0.f), n); + init_data.add_vertex(Vec3f(-half_size, half_size, 0.f), n); + init_data.add_triangle(0, 1, 2); + init_data.add_triangle(0, 2, 3); + + m_slicing_plane.init_from(std::move(init_data)); + } + + GLShaderProgram* shader = wxGetApp().get_shader("flat"); + if (shader == nullptr) + return; + + glsafe(::glEnable(GL_DEPTH_TEST)); + glsafe(::glEnable(GL_BLEND)); + glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + + shader->start_using(); + + // Show a tilted plane representing the slicing direction. + // The slicing plane is rotated by belt_angle about X from horizontal. + // Raise it slightly so it's visible above the bed surface. + double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle)); + Transform3d model_matrix = Transform3d::Identity(); + model_matrix.translate(Vec3d(0., 0., 30.)); + model_matrix.rotate(Eigen::AngleAxisd(angle_rad, Vec3d::UnitX())); + + shader->set_uniform("view_model_matrix", view_matrix * model_matrix); + shader->set_uniform("projection_matrix", projection_matrix); + + m_slicing_plane.set_color({ 0.2f, 0.6f, 1.0f, 0.3f }); // semi-transparent blue + m_slicing_plane.render(); + + glsafe(::glDisable(GL_BLEND)); + shader->stop_using(); +} + void Bed3D::render_default(bool bottom, const Transform3d& view_matrix, const Transform3d& projection_matrix) { // m_texture.reset(); diff --git a/src/slic3r/GUI/3DBed.hpp b/src/slic3r/GUI/3DBed.hpp index b16ead575b..005dbff534 100644 --- a/src/slic3r/GUI/3DBed.hpp +++ b/src/slic3r/GUI/3DBed.hpp @@ -111,6 +111,7 @@ private: GLModel m_model; Vec3d m_model_offset{ Vec3d::Zero() }; GLModel m_gravity_arrow; + GLModel m_slicing_plane; // Debug: shows the intended slicing plane direction Axes m_axes; float m_scale_factor{ 1.0f }; @@ -120,6 +121,9 @@ private: std::vector> m_extruder_shapes; std::vector m_extruder_heights; bool m_is_dark = false; + // Belt printer state for rendering. + bool m_is_belt_printer = false; + float m_belt_angle = 0.f; public: Bed3D() = default; @@ -139,6 +143,12 @@ public: // Build volume geometry for various collision detection tasks. const BuildVolume& build_volume() const { return m_build_volume; } + BuildVolume& build_volume() { return m_build_volume; } + + // Belt printer bed settings. + void set_belt_printer(bool enabled, float angle_deg) { m_is_belt_printer = enabled; m_belt_angle = angle_deg; } + bool is_belt_printer() const { return m_is_belt_printer; } + float belt_angle() const { return m_belt_angle; } // Was the model provided, or was it generated procedurally? Type get_type() const { return m_type; } @@ -179,6 +189,7 @@ private: void render_custom(GLCanvas3D& canvas, const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom); void render_default(bool bottom, const Transform3d& view_matrix, const Transform3d& projection_matrix); void render_gravity_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix); + void render_slicing_plane(const Transform3d& view_matrix, const Transform3d& projection_matrix); // BBS: remove the bed picking logic // void register_raycasters_for_picking(const GLModel::Geometry& geometry, const Transform3d& trafo); diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index f3f98a6fad..0b5e6d3906 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -575,9 +575,17 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co auto gcflavor = preset_bundle->printers.get_edited_preset().config.option>("gcode_flavor")->value; + // Belt printer: detect early since it affects multiple toggle decisions below. + bool is_belt_printer = false; + { + const auto *belt_opt = preset_bundle->printers.get_edited_preset().config.option("belt_printer"); + if (belt_opt) + is_belt_printer = belt_opt->value; + } + bool have_volumetric_extrusion_rate_slope = config->option("max_volumetric_extrusion_rate_slope")->value > 0; float have_volumetric_extrusion_rate_slope_segment_length = config->option("max_volumetric_extrusion_rate_slope_segment_length")->value; - toggle_field("enable_arc_fitting", !have_volumetric_extrusion_rate_slope); + toggle_field("enable_arc_fitting", !have_volumetric_extrusion_rate_slope && !is_belt_printer); toggle_line("max_volumetric_extrusion_rate_slope_segment_length", have_volumetric_extrusion_rate_slope); toggle_line("extrusion_rate_smoothing_external_perimeter_only", have_volumetric_extrusion_rate_slope); if(have_volumetric_extrusion_rate_slope) config->set_key_value("enable_arc_fitting", new ConfigOptionBool(false)); @@ -697,13 +705,20 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co } } - bool have_skirt = config->opt_int("skirt_loops") > 0; + // Belt printer: disable skirt, brim, raft, and draft shield controls. + bool have_skirt = config->opt_int("skirt_loops") > 0 && !is_belt_printer; toggle_field("skirt_height", have_skirt && config->opt_enum("draft_shield") != dsEnabled); toggle_line("single_loop_draft_shield", have_skirt); // ORCA: Display one wall if skirt enabled for (auto el : {"skirt_type", "min_skirt_length", "skirt_distance", "skirt_start_angle", "skirt_speed", "draft_shield"}) toggle_field(el, have_skirt); + if (is_belt_printer) { + toggle_field("skirt_loops", false); + toggle_field("skirt_height", false); + } - bool have_brim = (config->opt_enum("brim_type") != btNoBrim); + bool have_brim = (config->opt_enum("brim_type") != btNoBrim) && !is_belt_printer; + if (is_belt_printer) + toggle_field("brim_type", false); toggle_field("brim_object_gap", have_brim); toggle_field("brim_use_efc_outline", have_brim); bool have_brim_width = (config->opt_enum("brim_type") != btNoBrim) && config->opt_enum("brim_type") != btAutoBrim && @@ -724,7 +739,9 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co // Hide Elephant foot compensation layers if elefant_foot_compensation is not enabled toggle_line("elefant_foot_compensation_layers", config->opt_float("elefant_foot_compensation") > 0); - bool have_raft = config->opt_int("raft_layers") > 0; + bool have_raft = config->opt_int("raft_layers") > 0 && !is_belt_printer; + if (is_belt_printer) + toggle_field("raft_layers", false); bool have_support_material = config->opt_bool("enable_support") || have_raft; SupportType support_type = config->opt_enum("support_type"); diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 4814548a10..7df0b06ff5 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -2208,7 +2208,9 @@ void GCodeViewer::load_shells(const Print& print, bool initialized, bool force_p void GCodeViewer::render_toolpaths() { const Camera& camera = wxGetApp().plater()->get_camera(); - const libvgcode::Mat4x4 converted_view_matrix = libvgcode::convert(static_cast(camera.get_view_matrix().matrix().cast())); + Matrix4f view = camera.get_view_matrix().matrix().cast(); + // Belt view: view matrix transform placeholder (to be implemented in next cycle). + const libvgcode::Mat4x4 converted_view_matrix = libvgcode::convert(view); const libvgcode::Mat4x4 converted_projetion_matrix = libvgcode::convert(static_cast(camera.get_projection_matrix().matrix().cast())); #if VGCODE_ENABLE_COG_AND_TOOL_MARKERS m_viewer.set_cog_marker_scale_factor(m_cog_marker_fixed_screen_size ? 10.0f * m_cog_marker_size * camera.get_inv_zoom() : m_cog_marker_size); diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index dc928b822a..a299b93058 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -237,6 +237,9 @@ private: mutable bool m_no_render_path { false }; bool m_is_dark = false; + bool m_belt_view_enabled = false; + float m_belt_angle_deg = 0.f; + libvgcode::Viewer m_viewer; bool m_loaded_as_preview{ false }; @@ -336,6 +339,9 @@ public: void export_toolpaths_to_obj(const char* filename) const; + void set_belt_printer(bool enabled, float angle_deg) { m_belt_view_enabled = enabled; m_belt_angle_deg = angle_deg; } + bool is_belt_view() const { return m_belt_view_enabled && m_belt_angle_deg > 0.f; } + size_t get_extruders_count() { return m_extruders_count; } void push_combo_style(); void pop_combo_style(); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index ef46801722..373ee2a3dc 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -10980,6 +10980,24 @@ void Plater::priv::set_bed_shape(const Pointfs &shape, Vec2d shape_position = partplate_list.get_current_shape_position(); bool new_shape = bed.set_shape(shape, printable_height, extruder_areas, extruder_heights, custom_model, force_as_custom, shape_position); + // Belt printer: configure build volume and bed rendering for belt mode. + { + const auto *belt_opt = config->option("belt_printer"); + bool is_belt = belt_opt && belt_opt->value; + if (is_belt) { + double belt_angle = config->opt_float("belt_printer_angle"); + bool infinite_y = config->opt_bool("belt_printer_infinite_y"); + bed.build_volume().set_belt_printer(true, belt_angle, infinite_y); + bed.set_belt_printer(true, static_cast(belt_angle)); + if (preview) + preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(true, static_cast(belt_angle)); + } else { + bed.set_belt_printer(false, 0.f); + if (preview) + preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(false, 0.f); + } + } + float prev_height_lid, prev_height_rod; partplate_list.get_height_limits(prev_height_lid, prev_height_rod); double height_to_lid = config->opt_float("extruder_clearance_height_to_lid"); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 6ac8ca9a0e..fafe46c952 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4366,6 +4366,9 @@ void TabPrinter::build_fff() optgroup->append_single_option_line("printable_height", "printer_basic_information_printable_space#printable-height"); optgroup->append_single_option_line("build_plate_tilt_x"); optgroup->append_single_option_line("build_plate_tilt_y"); + optgroup->append_single_option_line("belt_printer"); + optgroup->append_single_option_line("belt_printer_angle"); + optgroup->append_single_option_line("belt_printer_infinite_y"); optgroup->append_single_option_line("support_multi_bed_types","printer_basic_information_printable_space#support-multi-bed-types"); optgroup->append_single_option_line("best_object_pos", "printer_basic_information_printable_space#best-object-position"); // todo: for multi_extruder test @@ -5224,6 +5227,11 @@ void TabPrinter::toggle_options() auto gcf = m_config->option>("gcode_flavor")->value; toggle_line("enable_power_loss_recovery", is_BBL_printer || gcf == gcfMarlinFirmware); + + // Belt printer: show belt-specific settings only when belt_printer is enabled. + bool is_belt = m_config->opt_bool("belt_printer"); + toggle_line("belt_printer_angle", is_belt); + toggle_line("belt_printer_infinite_y", is_belt); } @@ -5395,6 +5403,23 @@ void TabPrinter::update_fff() m_use_silent_mode = m_config->opt_bool("silent_mode"); } + // Belt printer: auto-sync build_plate_tilt_x to belt_printer_angle when belt mode is active. + // When belt mode is off, reset build_plate_tilt_x to 0 if it was set by belt mode. + if (m_config->opt_bool("belt_printer")) { + double belt_angle = m_config->opt_float("belt_printer_angle"); + if (m_config->opt_float("build_plate_tilt_x") != belt_angle) { + m_config->set_key_value("build_plate_tilt_x", new ConfigOptionFloat(belt_angle)); + } + } else { + // Only reset if build_plate_tilt_x matches a typical belt angle (was set by auto-sync). + // Avoid clobbering a manually-set tilt value for non-belt tilted printers. + double current_tilt = m_config->opt_float("build_plate_tilt_x"); + double belt_angle = m_config->opt_float("belt_printer_angle"); + if (current_tilt != 0. && std::abs(current_tilt - belt_angle) < 0.01) { + m_config->set_key_value("build_plate_tilt_x", new ConfigOptionFloat(0.)); + } + } + toggle_options(); } From cb13a22e572db5b6a168be90c880d22a97326a85 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Thu, 12 Mar 2026 14:25:30 -0500 Subject: [PATCH 003/434] Part 2: Replace belt rotation w/ per-axis shear transforms and G-code axis remap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace monolithic belt rotation transform with independent per-axis shear controls (mode/angle/source-axis for X, Y, Z) and G-code axis remapping, giving full flexibility to match any belt printer's coordinate system - Remove all rotation mode logic and intermediate type+axes dropdowns, simplifying the pipeline to pure shear matrices while preserving the default behavior (Y += Z*cot(45deg) with identity remap) - Clean up GCodeWriter, GCodeProcessor, and GCodeViewer for the new shear-only model; expose 12 new settings in printer UI via Tab.cpp/Preset.cpp Implement belt printer tilted slicing Implement the core belt slicing pipeline that makes the slicer tilt-aware: Step 1: GCodeWriter::to_machine_coords() - R(+alpha, X) rotation from slicing frame to machine frame Step 2: PrintObject - belt-rotated object height calculation (y*sin(a) + z*cos(a)) for correct layer count Step 3: PrintObjectSlice - apply R(-alpha, X) rotation trafo so horizontal slice planes correspond to belt-parallel planes, with Z-shift computed from model volumes Step 4: GCodeProcessor - machine-frame preview (no transform needed) Step 5: 3DBed - rotate bed visualization about X by belt angle Fix: belt surface IS the build plate, no mesh rotation Currently still slicing perpendicular to the belt normal. Need to figure out why. Fix G-code Z sign: use R(-alpha, X) so Z+ is away from belt The previous R(+alpha, X) transform produced negative Z values (-y*sin(a) term dominated). Changed to R(-alpha, X) which gives machine_z = y*sin(a) + z*cos(a), always positive for points above the belt surface. Z increases with each layer as expected. reverting and changing slice methodology Add pink slicing direction arrow from origin Shows the effective slicing direction (gantry normal) as a pink arrow from the origin. Shorter and wider than the gravity arrow. Direction: R(+alpha, X) * Z = (0, -sin(a), cos(a)), which is the layer stacking direction in the original mesh frame. Fix slicing arrow visibility and add raw G-code toggle - Disable depth test for pink slicing arrow so it renders on top of the tilted bed geometry (was being occluded) - Remove unnecessary 5mm Z-offset from arrow position - Add m_belt_show_raw toggle to GCodeViewer - Add "Show raw G-code (slicing frame)" checkbox in legend when belt mode is active Implement to_machine_coords inverse rotation for belt printer G-code The slicing pipeline rotates the mesh by R(-alpha, X) and shifts Z to start at 0. The G-code output now undoes this transform via to_machine_coords: R(+alpha, X) * T(0,0,+z_shift), recovering the original machine-frame coordinates where Y is horizontal and Z is vertical. Changes: - GCodeWriter: implement to_machine_coords with inverse rotation + Z-shift - GCodeWriter: add belt_z_shift member and setter/getter - GCode.cpp: compute Z-shift from print objects (same logic as PrintObjectSlice) and pass to writer; write z_shift to G-code header - GCodeProcessor: parse belt_z_shift from G-code header - GCodeViewer: store belt_z_shift from processor result Wire raw G-code toggle to apply slicing-frame view transform When "Show raw G-code (slicing frame)" is checked in the preview legend, the view matrix is modified to apply R(-alpha, X) * T(0,0,-z_shift) to the toolpath rendering. This shows the G-code as it was during slicing: rotated part with horizontal layers. Default (unchecked): machine-frame view — upright part with tilted layers. Remove belt printer placeholder comment from GCodeProcessor The preview now correctly displays machine-frame G-code with the optional raw view toggle. No transform is needed in the processor. --- src/libslic3r/GCode.cpp | 27 ++++++++- src/libslic3r/GCode/GCodeProcessor.cpp | 9 ++- src/libslic3r/GCode/GCodeProcessor.hpp | 3 +- src/libslic3r/GCodeWriter.cpp | 16 +++++- src/libslic3r/GCodeWriter.hpp | 7 ++- src/libslic3r/PrintObject.cpp | 19 +++++-- src/libslic3r/PrintObjectSlice.cpp | 20 ++++++- src/slic3r/GUI/3DBed.cpp | 79 ++++++++++++++++++++++++-- src/slic3r/GUI/3DBed.hpp | 2 + src/slic3r/GUI/GCodeViewer.cpp | 20 ++++++- src/slic3r/GUI/GCodeViewer.hpp | 2 + 11 files changed, 183 insertions(+), 21 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index bff35d321c..d762da7ed4 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -8,6 +8,7 @@ #include "Exception.hpp" #include "ExtrusionEntity.hpp" #include "EdgeGrid.hpp" +#include "Geometry.hpp" #include "Geometry/ConvexHull.hpp" #include "GCode/PrintExtents.hpp" #include "GCode/Thumbnails.hpp" @@ -2414,8 +2415,28 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_writer.set_is_bbl_machine(is_bbl_printers); // Belt printer: initialize coordinate transformation on the writer. - if (print.config().belt_printer.value) + if (print.config().belt_printer.value) { m_writer.set_belt_angle(print.config().belt_printer_angle.value); + // Compute the Z-shift that was applied during slicing (same logic as PrintObjectSlice.cpp). + // This is needed by to_machine_coords() to undo the slicing transform. + // For multiple objects, use the minimum min_z_rotated across all objects. + double angle_rad = Geometry::deg2rad(print.config().belt_printer_angle.value); + Transform3d belt_rotation = Transform3d::Identity(); + belt_rotation.rotate(Eigen::AngleAxisd(-angle_rad, Vec3d::UnitX())); + double global_min_z = std::numeric_limits::max(); + for (const PrintObject *obj : print.objects()) { + Transform3d obj_trafo = belt_rotation * obj->trafo_centered(); + for (const ModelVolume *mv : obj->model_object()->volumes) { + if (! mv->is_model_part() && ! mv->is_modifier()) + continue; + BoundingBoxf3 bb = mv->mesh().bounding_box(); + bb = bb.transformed(obj_trafo * mv->get_matrix()); + global_min_z = std::min(global_min_z, bb.min.z()); + } + } + if (global_min_z != std::numeric_limits::max() && std::abs(global_min_z) > EPSILON) + m_writer.set_belt_z_shift(global_min_z); // typically negative + } // How many times will be change_layer() called? // change_layer() in turn increments the progress bar status. @@ -2507,8 +2528,10 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Write information on the generator. file.write_format("; generated by %s on %s\n", Slic3r::header_slic3r_generated().c_str(), Slic3r::Utils::local_timestamp().c_str()); // Belt printer: embed angle in header for G-code processor detection. - if (print.config().belt_printer.value) + if (print.config().belt_printer.value) { file.write_format("; belt_printer_angle = %.1f\n", print.config().belt_printer_angle.value); + file.write_format("; belt_z_shift = %.4f\n", m_writer.belt_z_shift()); + } if (is_bbl_printers) file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str()); //BBS: total layer number diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 0dc00e1522..a7a39b5b02 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -2597,8 +2597,6 @@ void GCodeProcessor::finalize(bool post_process) } } - // Belt printer: preview coordinate transform placeholder (to be implemented in next cycle). - calculate_time(m_result); // process the time blocks @@ -3053,6 +3051,13 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers } catch (...) {} return; } + // Belt printer Z-shift for raw G-code toggle in preview. + if (boost::starts_with(comment, " belt_z_shift = ")) { + try { + m_result.belt_z_shift = std::stof(std::string(comment.substr(16))); + } catch (...) {} + return; + } // wipe start tag if (boost::starts_with(comment, reserved_tag(ETags::Wipe_Start))) { diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index e432a165a8..af68a5a6b7 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -227,8 +227,9 @@ class Print; bool support_traditional_timelapse{true}; float printable_height; float z_offset; - // Belt printer: angle for coordinate transformation in preview. + // Belt printer: angle and Z-shift for coordinate transformation in preview. float belt_printer_angle{ 0.f }; + float belt_z_shift{ 0.f }; SettingsIds settings_ids; size_t filaments_count; bool backtrace_enabled; diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index 1b056a946f..119c9dd068 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -29,8 +29,20 @@ void GCodeWriter::set_belt_angle(double angle_deg) Vec3d GCodeWriter::to_machine_coords(const Vec3d &pos) const { - // Belt printer: coordinate transform placeholder (to be implemented in next cycle). - return pos; + if (!is_belt_printer()) + return pos; + + // Undo the slicing transform to recover machine-frame coordinates. + // Slicing applied: T(0,0,-min_z_rot) * R(-alpha, X) * original_pos + // Inverse: R(+alpha, X) * T(0,0,+min_z_rot) * slicing_pos + // + // First undo the Z-shift, then apply R(+alpha, X). + double sz = pos.z() + m_belt_z_shift; // undo T(0,0,-min_z_rot) + return Vec3d( + pos.x(), + pos.y() * m_belt_cos - sz * m_belt_sin, // R(+alpha, X) + pos.y() * m_belt_sin + sz * m_belt_cos + ); } bool GCodeWriter::supports_separate_travel_acceleration(GCodeFlavor flavor) diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index 10159d941f..f9b5ed79ac 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -127,8 +127,10 @@ public: // Belt printer: set the belt angle and precompute sin/cos for coordinate transformation. void set_belt_angle(double angle_deg); + void set_belt_z_shift(double z_shift) { m_belt_z_shift = z_shift; } + double belt_z_shift() const { return m_belt_z_shift; } bool is_belt_printer() const { return m_belt_angle_rad != 0.; } - // Transform a point from the slicing frame to machine/world coordinates (inverse shear). + // Transform a point from the slicing frame to machine/world coordinates (inverse rotation). Vec3d to_machine_coords(const Vec3d &pos) const; // Returns whether this flavor supports separate print and travel acceleration. @@ -184,10 +186,11 @@ public: //SoftFever bool m_is_bbl_printers = false; - // Belt printer coordinate transformation (YZ shear) + // Belt printer coordinate transformation (inverse of slicing rotation) double m_belt_angle_rad = 0.; double m_belt_cos = 1.0; double m_belt_sin = 0.0; + double m_belt_z_shift = 0.; // Z-shift applied during slicing (min_z_rotated, typically negative) double m_current_speed; bool m_is_first_layer = true; diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 979953fb94..08ff847951 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -3392,7 +3392,13 @@ void PrintObject::update_slicing_parameters() // Orca: updated function call for XYZ shrinkage compensation if (!m_slicing_params.valid) { coordf_t object_height = this->model_object()->max_z(); - // Belt printer: height adjustment placeholder (to be implemented in next cycle). + if (this->print()->config().belt_printer.value) { + // After mesh rotation R(-alpha, X), effective height in the slicing + // direction is y_extent*sin(a) + z_extent*cos(a). + double angle_rad = Geometry::deg2rad(this->print()->config().belt_printer_angle.value); + BoundingBoxf3 bb = this->model_object()->raw_bounding_box(); + object_height = bb.size().y() * std::sin(angle_rad) + bb.size().z() * std::cos(angle_rad); + } m_slicing_params = SlicingParameters::create_from_config(this->print()->config(), m_config, object_height, this->object_extruders(), this->print()->shrinkage_compensation()); } @@ -3432,9 +3438,14 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full sort_remove_duplicates(object_extruders); //FIXME add painting extruders - if (object_max_z <= 0.f) - object_max_z = (float)model_object.raw_bounding_box().size().z(); - // Belt printer: height adjustment placeholder (to be implemented in next cycle). + if (object_max_z <= 0.f) { + BoundingBoxf3 bb = model_object.raw_bounding_box(); + object_max_z = (float)bb.size().z(); + if (print_config.belt_printer.value) { + double angle_rad = Geometry::deg2rad(print_config.belt_printer_angle.value); + object_max_z = (float)(bb.size().y() * std::sin(angle_rad) + bb.size().z() * std::cos(angle_rad)); + } + } return SlicingParameters::create_from_config(print_config, object_config, object_max_z, object_extruders, object_shrinkage_compensation); } diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index bba4593764..2748151aa0 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -140,7 +140,25 @@ static std::vector slice_volumes_inner( params_base.closing_radius = print_object_config.slice_closing_radius.value; params_base.extra_offset = 0; params_base.trafo = object_trafo; - // Belt printer: mesh transform placeholder (to be implemented in next cycle). + if (print_config.belt_printer.value) { + double angle_rad = Geometry::deg2rad(print_config.belt_printer_angle.value); + // Rotate mesh by -alpha about X so horizontal slice planes = gantry-parallel planes. + // The gantry (XY) is tilted by belt_printer_angle; this rotation aligns it with horizontal. + Transform3d belt_rotation = Transform3d::Identity(); + belt_rotation.rotate(Eigen::AngleAxisd(-angle_rad, Vec3d::UnitX())); + params_base.trafo = belt_rotation * params_base.trafo; + // Compute Z-shift from model_volumes: find min-Z of all rotated meshes + // so the rotated geometry starts at Z=0 (the belt surface in slicing frame). + double min_z_rotated = std::numeric_limits::max(); + for (const ModelVolume *mv : model_volumes) { + if (!model_volume_needs_slicing(*mv)) continue; + BoundingBoxf3 bb = mv->mesh().bounding_box(); + bb = bb.transformed(params_base.trafo * mv->get_matrix()); + min_z_rotated = std::min(min_z_rotated, bb.min.z()); + } + if (min_z_rotated != std::numeric_limits::max() && std::abs(min_z_rotated) > EPSILON) + params_base.trafo = Eigen::Translation3d(0, 0, -min_z_rotated) * params_base.trafo; + } //BBS: 0.0025mm is safe enough to simplify the data to speed slicing up for high-resolution model. //Also has on influence on arc fitting which has default resolution 0.0125mm. params_base.resolution = print_config.resolution <= 0.001 ? 0.0f : 0.0025; diff --git a/src/slic3r/GUI/3DBed.cpp b/src/slic3r/GUI/3DBed.cpp index 101279d744..858182946c 100644 --- a/src/slic3r/GUI/3DBed.cpp +++ b/src/slic3r/GUI/3DBed.cpp @@ -387,17 +387,18 @@ void Bed3D::render_internal(GLCanvas3D& canvas, const Transform3d& view_matrix, m_model.set_color(m_is_dark ? DEFAULT_MODEL_COLOR_DARK : DEFAULT_MODEL_COLOR); - // Belt printer: bed view transform placeholder (to be implemented in next cycle). - Transform3d belt_view_matrix = view_matrix; + // Belt printer: bed rotation is applied inside render_model() and render_default() + // using m_is_belt_printer and m_belt_angle members. switch (m_type) { - case Type::System: { render_system(canvas, belt_view_matrix, projection_matrix, bottom); break; } + case Type::System: { render_system(canvas, view_matrix, projection_matrix, bottom); break; } default: - case Type::Custom: { render_custom(canvas, belt_view_matrix, projection_matrix, bottom); break; } + case Type::Custom: { render_custom(canvas, view_matrix, projection_matrix, bottom); break; } } render_gravity_arrow(view_matrix, projection_matrix); + render_slicing_arrow(view_matrix, projection_matrix); render_slicing_plane(view_matrix, projection_matrix); glsafe(::glDisable(GL_DEPTH_TEST)); @@ -698,7 +699,13 @@ void Bed3D::render_model(const Transform3d& view_matrix, const Transform3d& proj if (shader != nullptr) { shader->start_using(); shader->set_uniform("emission_factor", 0.0f); - const Transform3d model_matrix = Geometry::assemble_transform(m_model_offset); + Transform3d model_matrix = Geometry::assemble_transform(m_model_offset); + // Belt printer: rotate the bed model about X so the belt tilt is visible. + // Negative angle: belt surface tilts downward away from the nozzle. + if (m_is_belt_printer && m_belt_angle > 0.f) { + double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle)); + model_matrix = Eigen::AngleAxisd(-angle_rad, Vec3d::UnitX()) * model_matrix; + } shader->set_uniform("volume_world_matrix", model_matrix); shader->set_uniform("view_model_matrix", view_matrix * model_matrix); shader->set_uniform("projection_matrix", projection_matrix); @@ -798,6 +805,58 @@ void Bed3D::render_gravity_arrow(const Transform3d& view_matrix, const Transform shader->stop_using(); } +void Bed3D::render_slicing_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix) +{ + if (!m_is_belt_printer || m_belt_angle <= 0.f) + return; + + // Build the arrow model: shorter and wider than the gravity arrow. + if (!m_slicing_arrow.is_initialized()) { + const float stem_length = 15.0f; // shorter than gravity arrow (25) + const float stem_radius = 1.0f; // wider than gravity arrow (~0.33) + const float tip_radius = 3.0f; // wider tip + const float tip_length = 5.0f; + m_slicing_arrow.init_from(stilized_arrow(16, tip_radius, tip_length, stem_radius, stem_length)); + } + + // The slicing direction: layers stack along the gantry normal. + // With mesh rotation R(-alpha, X), the slicing Z-axis in the original frame + // points in direction R(+alpha, X) * (0, 0, 1) = (0, -sin(alpha), cos(alpha)). + double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle)); + Vec3d slice_dir = Vec3d(0., -std::sin(angle_rad), std::cos(angle_rad)).normalized(); + + // Compute rotation to align +Z (arrow default) with slice_dir. + Vec3d from = Vec3d::UnitZ(); + double dot = from.dot(slice_dir); + Transform3d rot = Transform3d::Identity(); + if (dot < -0.9999) { + rot = Eigen::AngleAxisd(M_PI, Vec3d::UnitX()) * rot; + } else if (dot < 0.9999) { + Vec3d axis = from.cross(slice_dir).normalized(); + double angle = std::acos(std::clamp(dot, -1.0, 1.0)); + rot = Eigen::AngleAxisd(angle, axis) * rot; + } + + GLShaderProgram* shader = wxGetApp().get_shader("flat"); + if (shader == nullptr) + return; + + // Disable depth test so the arrow is always visible (not occluded by the tilted bed). + glsafe(::glDisable(GL_DEPTH_TEST)); + shader->start_using(); + + const Camera& camera = wxGetApp().plater()->get_camera(); + Transform3d model_matrix = rot; + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * model_matrix); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + + m_slicing_arrow.set_color({ 1.0f, 0.2f, 0.6f, 1.0f }); // pink + m_slicing_arrow.render(); + + shader->stop_using(); + glsafe(::glEnable(GL_DEPTH_TEST)); +} + void Bed3D::render_slicing_plane(const Transform3d& view_matrix, const Transform3d& projection_matrix) { if (!m_is_belt_printer || m_belt_angle <= 0.f) @@ -863,7 +922,15 @@ void Bed3D::render_default(bool bottom, const Transform3d& view_matrix, const Tr if (shader != nullptr) { shader->start_using(); - shader->set_uniform("view_model_matrix", view_matrix); + // Belt printer: rotate the default bed about X so the belt tilt is visible. + Transform3d view_model_matrix = view_matrix; + if (m_is_belt_printer && m_belt_angle > 0.f) { + double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle)); + Transform3d belt_rotation = Transform3d::Identity(); + belt_rotation.rotate(Eigen::AngleAxisd(-angle_rad, Vec3d::UnitX())); + view_model_matrix = view_matrix * belt_rotation; + } + shader->set_uniform("view_model_matrix", view_model_matrix); shader->set_uniform("projection_matrix", projection_matrix); glsafe(::glEnable(GL_DEPTH_TEST)); diff --git a/src/slic3r/GUI/3DBed.hpp b/src/slic3r/GUI/3DBed.hpp index 005dbff534..d01273961d 100644 --- a/src/slic3r/GUI/3DBed.hpp +++ b/src/slic3r/GUI/3DBed.hpp @@ -111,6 +111,7 @@ private: GLModel m_model; Vec3d m_model_offset{ Vec3d::Zero() }; GLModel m_gravity_arrow; + GLModel m_slicing_arrow; // Pink arrow showing the effective slicing direction GLModel m_slicing_plane; // Debug: shows the intended slicing plane direction Axes m_axes; @@ -189,6 +190,7 @@ private: void render_custom(GLCanvas3D& canvas, const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom); void render_default(bool bottom, const Transform3d& view_matrix, const Transform3d& projection_matrix); void render_gravity_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix); + void render_slicing_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix); void render_slicing_plane(const Transform3d& view_matrix, const Transform3d& projection_matrix); // BBS: remove the bed picking logic diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 7df0b06ff5..dd66c918a7 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1271,6 +1271,7 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const m_max_print_height = gcode_result.printable_height; m_z_offset = gcode_result.z_offset; + m_belt_z_shift = gcode_result.belt_z_shift; // load_toolpaths(gcode_result, build_volume, exclude_bounding_box); @@ -2209,7 +2210,16 @@ void GCodeViewer::render_toolpaths() { const Camera& camera = wxGetApp().plater()->get_camera(); Matrix4f view = camera.get_view_matrix().matrix().cast(); - // Belt view: view matrix transform placeholder (to be implemented in next cycle). + // Belt "raw" view: apply slicing rotation to view matrix so toolpaths appear + // in the slicing frame (rotated part with horizontal layers). + if (m_belt_show_raw && m_belt_view_enabled && m_belt_angle_deg > 0.f) { + double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle_deg)); + // Apply R(-alpha, X) * T(0,0,-z_shift) to bring machine coords back to slicing frame. + Transform3d slicing_trafo = Transform3d::Identity(); + slicing_trafo.translate(Vec3d(0., 0., -static_cast(m_belt_z_shift))); + slicing_trafo = Eigen::AngleAxisd(-angle_rad, Vec3d::UnitX()) * slicing_trafo; + view = (camera.get_view_matrix() * slicing_trafo).matrix().cast(); + } const libvgcode::Mat4x4 converted_view_matrix = libvgcode::convert(view); const libvgcode::Mat4x4 converted_projetion_matrix = libvgcode::convert(static_cast(camera.get_projection_matrix().matrix().cast())); #if VGCODE_ENABLE_COG_AND_TOOL_MARKERS @@ -4406,6 +4416,14 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv if (m_nozzle_nums > 1 && (m_viewer.get_view_type() == libvgcode::EViewType::Summary || m_viewer.get_view_type() == libvgcode::EViewType::ColorPrint)) // ORCA show only on summary and filament tab render_legend_color_arr_recommen(window_padding); + // Belt printer: toggle for viewing raw slicing-frame G-code + if (m_belt_view_enabled) { + ImGui::Spacing(); + ImGui::Dummy({ window_padding, 0 }); + ImGui::SameLine(); + ImGui::Checkbox("Show raw G-code (slicing frame)", &m_belt_show_raw); + } + legend_height = ImGui::GetCurrentWindow()->Size.y; imgui.end(); ImGui::PopStyleColor(7); diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index a299b93058..4bd3746991 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -239,6 +239,8 @@ mutable bool m_no_render_path { false }; bool m_belt_view_enabled = false; float m_belt_angle_deg = 0.f; + float m_belt_z_shift = 0.f; + bool m_belt_show_raw = false; // Toggle: show raw slicing-frame G-code (rotated part) libvgcode::Viewer m_viewer; bool m_loaded_as_preview{ false }; From 719af2d81d1fb62206e978743151bee018191563 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Thu, 12 Mar 2026 20:47:28 -0500 Subject: [PATCH 004/434] Part 2.5: Add global shear transform, support clipping, and belt UI improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implement per-object global shear transform in PrintObject with layer Z-offset calculation, config invalidation, and fix for shared-object layer optimization breaking copied objects - Clip support layers to the transformed belt floor plane and begin work on tree support adaptation for sheared coordinate space - Improve belt UI: gray out inactive sub-options, add B keyboard shortcut for G-code viewer design-view toggle, fix mesh clipping through build plate after shear/scale transform y' = y + z·cot(α), while x' = x and z' = z getting closer to customizable variant getting closer X/Y/Z shear initial clean up UI add 1/sin(a) transform, idea taken from blackbelt cura plugin Things work now (turns out I've been using the wrong set of transforms) --- src/libslic3r/GCode.cpp | 31 ++---- src/libslic3r/GCode/GCodeProcessor.cpp | 8 -- src/libslic3r/GCode/GCodeProcessor.hpp | 3 +- src/libslic3r/GCodeWriter.cpp | 34 ++++--- src/libslic3r/GCodeWriter.hpp | 17 ++-- src/libslic3r/Preset.cpp | 6 +- src/libslic3r/PrintConfig.cpp | 135 +++++++++++++++++++++++++ src/libslic3r/PrintConfig.hpp | 54 ++++++++++ src/libslic3r/PrintObject.cpp | 101 ++++++++++++++++-- src/libslic3r/PrintObjectSlice.cpp | 78 +++++++++++--- src/slic3r/GUI/GCodeViewer.cpp | 22 ++-- src/slic3r/GUI/GCodeViewer.hpp | 3 +- src/slic3r/GUI/Tab.cpp | 51 ++++++++++ 13 files changed, 452 insertions(+), 91 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index d762da7ed4..02a233e988 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2411,31 +2411,19 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_is_role_based_fan_on.fill(false); m_fan_mover.release(); - + m_writer.set_is_bbl_machine(is_bbl_printers); - // Belt printer: initialize coordinate transformation on the writer. + // Belt printer: initialize coordinate transformation and axis remap on the writer. if (print.config().belt_printer.value) { m_writer.set_belt_angle(print.config().belt_printer_angle.value); - // Compute the Z-shift that was applied during slicing (same logic as PrintObjectSlice.cpp). - // This is needed by to_machine_coords() to undo the slicing transform. - // For multiple objects, use the minimum min_z_rotated across all objects. - double angle_rad = Geometry::deg2rad(print.config().belt_printer_angle.value); - Transform3d belt_rotation = Transform3d::Identity(); - belt_rotation.rotate(Eigen::AngleAxisd(-angle_rad, Vec3d::UnitX())); - double global_min_z = std::numeric_limits::max(); - for (const PrintObject *obj : print.objects()) { - Transform3d obj_trafo = belt_rotation * obj->trafo_centered(); - for (const ModelVolume *mv : obj->model_object()->volumes) { - if (! mv->is_model_part() && ! mv->is_modifier()) - continue; - BoundingBoxf3 bb = mv->mesh().bounding_box(); - bb = bb.transformed(obj_trafo * mv->get_matrix()); - global_min_z = std::min(global_min_z, bb.min.z()); - } - } - if (global_min_z != std::numeric_limits::max() && std::abs(global_min_z) > EPSILON) - m_writer.set_belt_z_shift(global_min_z); // typically negative + m_writer.set_axis_remap( + int(print.config().belt_gcode_remap_x.value), + int(print.config().belt_gcode_remap_y.value), + int(print.config().belt_gcode_remap_z.value)); + // Build volume extents for Rev remap mode. + BoundingBoxf bbox_bed(print.config().printable_area.values); + m_writer.set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(), print.config().printable_height.value)); } // How many times will be change_layer() called? @@ -2530,7 +2518,6 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Belt printer: embed angle in header for G-code processor detection. if (print.config().belt_printer.value) { file.write_format("; belt_printer_angle = %.1f\n", print.config().belt_printer_angle.value); - file.write_format("; belt_z_shift = %.4f\n", m_writer.belt_z_shift()); } if (is_bbl_printers) file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str()); diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index a7a39b5b02..7ac3a1ffc4 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -3051,14 +3051,6 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers } catch (...) {} return; } - // Belt printer Z-shift for raw G-code toggle in preview. - if (boost::starts_with(comment, " belt_z_shift = ")) { - try { - m_result.belt_z_shift = std::stof(std::string(comment.substr(16))); - } catch (...) {} - return; - } - // wipe start tag if (boost::starts_with(comment, reserved_tag(ETags::Wipe_Start))) { m_wiping = true; diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index af68a5a6b7..e432a165a8 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -227,9 +227,8 @@ class Print; bool support_traditional_timelapse{true}; float printable_height; float z_offset; - // Belt printer: angle and Z-shift for coordinate transformation in preview. + // Belt printer: angle for coordinate transformation in preview. float belt_printer_angle{ 0.f }; - float belt_z_shift{ 0.f }; SettingsIds settings_ids; size_t filaments_count; bool backtrace_enabled; diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index 119c9dd068..d6bb5a0bb9 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -23,26 +23,32 @@ bool GCodeWriter::full_gcode_comment = true; void GCodeWriter::set_belt_angle(double angle_deg) { m_belt_angle_rad = Geometry::deg2rad(angle_deg); - m_belt_cos = std::cos(m_belt_angle_rad); - m_belt_sin = std::sin(m_belt_angle_rad); +} + +void GCodeWriter::set_axis_remap(int rx, int ry, int rz) +{ + m_remap_x = rx; + m_remap_y = ry; + m_remap_z = rz; +} + +void GCodeWriter::set_build_volume_max(const Vec3d &max) +{ + m_build_vol_max = max; } Vec3d GCodeWriter::to_machine_coords(const Vec3d &pos) const { if (!is_belt_printer()) return pos; - - // Undo the slicing transform to recover machine-frame coordinates. - // Slicing applied: T(0,0,-min_z_rot) * R(-alpha, X) * original_pos - // Inverse: R(+alpha, X) * T(0,0,+min_z_rot) * slicing_pos - // - // First undo the Z-shift, then apply R(+alpha, X). - double sz = pos.z() + m_belt_z_shift; // undo T(0,0,-min_z_rot) - return Vec3d( - pos.x(), - pos.y() * m_belt_cos - sz * m_belt_sin, // R(+alpha, X) - pos.y() * m_belt_sin + sz * m_belt_cos - ); + // BeltRemapAxis: 0-2 = +X/+Y/+Z, 3-5 = -X/-Y/-Z, 6-8 = Rev X/Y/Z + auto remap = [this, &pos](int r) -> double { + int axis = r % 3; + if (r < 3) return pos[axis]; + if (r < 6) return -pos[axis]; + return m_build_vol_max[axis] - pos[axis]; + }; + return { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) }; } bool GCodeWriter::supports_separate_travel_acceleration(GCodeFlavor flavor) diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index f9b5ed79ac..6074eb247c 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -127,10 +127,12 @@ public: // Belt printer: set the belt angle and precompute sin/cos for coordinate transformation. void set_belt_angle(double angle_deg); - void set_belt_z_shift(double z_shift) { m_belt_z_shift = z_shift; } - double belt_z_shift() const { return m_belt_z_shift; } bool is_belt_printer() const { return m_belt_angle_rad != 0.; } - // Transform a point from the slicing frame to machine/world coordinates (inverse rotation). + // Set axis remap for G-code output (BeltRemapAxis enum values). + void set_axis_remap(int rx, int ry, int rz); + // Set build volume extents for Rev remap mode (max X, Y, Z). + void set_build_volume_max(const Vec3d &max); + // Transform a point from the slicing frame to machine coordinates. Vec3d to_machine_coords(const Vec3d &pos) const; // Returns whether this flavor supports separate print and travel acceleration. @@ -186,11 +188,12 @@ public: //SoftFever bool m_is_bbl_printers = false; - // Belt printer coordinate transformation (inverse of slicing rotation) + // Belt printer state double m_belt_angle_rad = 0.; - double m_belt_cos = 1.0; - double m_belt_sin = 0.0; - double m_belt_z_shift = 0.; // Z-shift applied during slicing (min_z_rotated, typically negative) + int m_remap_x = 0; + int m_remap_y = 1; + int m_remap_z = 2; + Vec3d m_build_vol_max = Vec3d::Zero(); double m_current_speed; bool m_is_first_layer = true; diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 98532bccc7..9bc5e0a782 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1010,7 +1010,11 @@ static std::vector s_Preset_machine_limits_options { static std::vector s_Preset_printer_options { "printer_technology", - "printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "build_plate_tilt_x", "build_plate_tilt_y", "belt_printer", "belt_printer_angle", "belt_printer_infinite_y", "gcode_flavor", + "printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "build_plate_tilt_x", "build_plate_tilt_y", "belt_printer", "belt_printer_angle", "belt_printer_infinite_y", "belt_shear_x", "belt_shear_x_angle", "belt_shear_x_from", + "belt_shear_y", "belt_shear_y_angle", "belt_shear_y_from", + "belt_shear_z", "belt_shear_z_angle", "belt_shear_z_from", + "belt_scale_x", "belt_scale_x_angle", "belt_scale_y", "belt_scale_y_angle", "belt_scale_z", "belt_scale_z_angle", + "belt_gcode_remap_x", "belt_gcode_remap_y", "belt_gcode_remap_z", "gcode_flavor", "fan_kickstart", "fan_speedup_time", "fan_speedup_overhangs", "single_extruder_multi_material", "manual_filament_change", "file_start_gcode", "machine_start_gcode", "machine_end_gcode", "before_layer_change_gcode", "printing_by_object_gcode", "layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode", "change_filament_gcode", "change_extrusion_role_gcode", "printer_model", "printer_variant", "printer_extruder_id", "printer_extruder_variant", "extruder_variant_list", "default_nozzle_volume_type", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 7eea294d4e..a722a3bae7 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -289,6 +289,44 @@ static t_config_enum_values s_keys_map_SlicingMode { }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(SlicingMode) +static t_config_enum_values s_keys_map_BeltShearMode { + { "none", int(BeltShearMode::None) }, + { "pos_cot", int(BeltShearMode::PosCot) }, + { "neg_cot", int(BeltShearMode::NegCot) }, + { "pos_tan", int(BeltShearMode::PosTan) }, + { "neg_tan", int(BeltShearMode::NegTan) }, +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltShearMode) + +static t_config_enum_values s_keys_map_BeltAxis { + { "x", int(BeltAxis::X) }, + { "y", int(BeltAxis::Y) }, + { "z", int(BeltAxis::Z) }, +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltAxis) + +static t_config_enum_values s_keys_map_BeltScaleMode { + { "none", int(BeltScaleMode::None) }, + { "inv_sin", int(BeltScaleMode::InvSin) }, + { "inv_cos", int(BeltScaleMode::InvCos) }, + { "sin", int(BeltScaleMode::Sin) }, + { "cos", int(BeltScaleMode::Cos) }, +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltScaleMode) + +static t_config_enum_values s_keys_map_BeltRemapAxis { + { "pos_x", int(BeltRemapAxis::PosX) }, + { "pos_y", int(BeltRemapAxis::PosY) }, + { "pos_z", int(BeltRemapAxis::PosZ) }, + { "neg_x", int(BeltRemapAxis::NegX) }, + { "neg_y", int(BeltRemapAxis::NegY) }, + { "neg_z", int(BeltRemapAxis::NegZ) }, + { "rev_x", int(BeltRemapAxis::RevX) }, + { "rev_y", int(BeltRemapAxis::RevY) }, + { "rev_z", int(BeltRemapAxis::RevZ) }, +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltRemapAxis) + static t_config_enum_values s_keys_map_SupportMaterialPattern { { "rectilinear", smpRectilinear }, { "rectilinear-grid", smpRectilinearGrid }, @@ -5974,6 +6012,103 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionBool(true)); + // Per-axis shear controls for belt printer + auto add_belt_shear_mode = [this](const char *key, const char *label, BeltShearMode default_mode) { + auto def = this->add(key, coEnum); + def->label = L(label); + def->category = L("Printable space"); + def->tooltip = L("Shear function applied to this axis in belt printer mode."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values = {"none", "pos_cot", "neg_cot", "pos_tan", "neg_tan"}; + def->enum_labels = {L("None"), L("+cot(α)"), L("-cot(α)"), L("+tan(α)"), L("-tan(α)")}; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(default_mode)); + }; + auto add_belt_shear_angle = [this](const char *key, const char *label) { + auto def = this->add(key, coFloat); + def->label = L(label); + def->category = L("Printable space"); + def->tooltip = L("Angle (degrees) for the shear function on this axis."); + def->sidetext = L("°"); + def->min = 0.1; + def->max = 89.9; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(45)); + }; + auto add_belt_axis_enum = [this](const char *key, const char *label, const char *tooltip, BeltAxis default_axis) { + auto def = this->add(key, coEnum); + def->label = L(label); + def->category = L("Printable space"); + def->tooltip = L(tooltip); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values = {"x", "y", "z"}; + def->enum_labels = {L("X"), L("Y"), L("Z")}; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(default_axis)); + }; + + add_belt_shear_mode("belt_shear_x", "Function", BeltShearMode::None); + add_belt_shear_angle("belt_shear_x_angle", "Angle"); + add_belt_axis_enum("belt_shear_x_from", "From", "Source axis for X shear.", BeltAxis::Z); + + add_belt_shear_mode("belt_shear_y", "Function", BeltShearMode::PosCot); + add_belt_shear_angle("belt_shear_y_angle", "Angle"); + add_belt_axis_enum("belt_shear_y_from", "From", "Source axis for Y shear.", BeltAxis::Z); + + add_belt_shear_mode("belt_shear_z", "Function", BeltShearMode::None); + add_belt_shear_angle("belt_shear_z_angle", "Angle"); + add_belt_axis_enum("belt_shear_z_from", "From", "Source axis for Z shear.", BeltAxis::Y); + + // Per-axis scale controls for belt printer + auto add_belt_scale_mode = [this](const char *key, const char *label, BeltScaleMode default_mode) { + auto def = this->add(key, coEnum); + def->label = L(label); + def->category = L("Printable space"); + def->tooltip = L("Scale factor applied to this axis in belt printer mode."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values = {"none", "inv_sin", "inv_cos", "sin", "cos"}; + def->enum_labels = {L("None"), L("1/sin(α)"), L("1/cos(α)"), L("sin(α)"), L("cos(α)")}; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(default_mode)); + }; + auto add_belt_scale_angle = [this](const char *key, const char *label) { + auto def = this->add(key, coFloat); + def->label = L(label); + def->category = L("Printable space"); + def->tooltip = L("Angle (degrees) for the scale function on this axis."); + def->sidetext = L("°"); + def->min = 0.1; + def->max = 89.9; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(45)); + }; + + add_belt_scale_mode("belt_scale_x", "Function", BeltScaleMode::None); + add_belt_scale_angle("belt_scale_x_angle", "Angle"); + + add_belt_scale_mode("belt_scale_y", "Function", BeltScaleMode::None); + add_belt_scale_angle("belt_scale_y_angle", "Angle"); + + add_belt_scale_mode("belt_scale_z", "Function", BeltScaleMode::None); + add_belt_scale_angle("belt_scale_z_angle", "Angle"); + + // G-code axis remap with sign + auto add_belt_remap = [this](const char *key, const char *label, const char *tooltip, BeltRemapAxis default_axis) { + auto def = this->add(key, coEnum); + def->label = L(label); + def->category = L("Printable space"); + def->tooltip = L(tooltip); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values = {"pos_x", "pos_y", "pos_z", "neg_x", "neg_y", "neg_z", "rev_x", "rev_y", "rev_z"}; + def->enum_labels = {L("+X"), L("+Y"), L("+Z"), L("-X"), L("-Y"), L("-Z"), L("Rev X"), L("Rev Y"), L("Rev Z")}; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(default_axis)); + }; + + add_belt_remap("belt_gcode_remap_x", "X", "Which slicing axis maps to machine X in G-code output.", BeltRemapAxis::PosX); + add_belt_remap("belt_gcode_remap_y", "Y", "Which slicing axis maps to machine Y in G-code output.", BeltRemapAxis::PosY); + add_belt_remap("belt_gcode_remap_z", "Z", "Which slicing axis maps to machine Z in G-code output.", BeltRemapAxis::PosZ); + def = this->add("tree_support_branch_angle", coFloat); def->label = L("Tree support branch angle"); def->category = L("Support"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index f36b5d3e93..4f149d9c8b 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -157,6 +157,38 @@ enum class SlicingMode CloseHoles, }; +enum class BeltShearMode +{ + None, // No shear on this axis + PosCot, // += cot(α) + NegCot, // -= cot(α) + PosTan, // += tan(α) + NegTan, // -= tan(α) +}; + +enum class BeltScaleMode +{ + None, // No scaling (factor = 1) + InvSin, // 1/sin(α) + InvCos, // 1/cos(α) + Sin, // sin(α) + Cos, // cos(α) +}; + +enum class BeltAxis +{ + X = 0, + Y = 1, + Z = 2, +}; + +enum class BeltRemapAxis +{ + PosX = 0, PosY = 1, PosZ = 2, + NegX = 3, NegY = 4, NegZ = 5, + RevX = 6, RevY = 7, RevZ = 8, // Reversed: max - pos +}; + enum SupportMaterialPattern { smpDefault, smpRectilinear, smpRectilinearGrid, smpHoneycomb, @@ -500,6 +532,10 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(NoiseType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(InfillPattern) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(IroningType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SlicingMode) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltShearMode) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltScaleMode) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltAxis) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltRemapAxis) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialPattern) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialStyle) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialInterfacePattern) @@ -1417,6 +1453,24 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionBool, belt_printer)) ((ConfigOptionFloat, belt_printer_angle)) ((ConfigOptionBool, belt_printer_infinite_y)) + ((ConfigOptionEnum, belt_shear_x)) + ((ConfigOptionFloat, belt_shear_x_angle)) + ((ConfigOptionEnum, belt_shear_x_from)) + ((ConfigOptionEnum, belt_shear_y)) + ((ConfigOptionFloat, belt_shear_y_angle)) + ((ConfigOptionEnum, belt_shear_y_from)) + ((ConfigOptionEnum, belt_shear_z)) + ((ConfigOptionFloat, belt_shear_z_angle)) + ((ConfigOptionEnum, belt_shear_z_from)) + ((ConfigOptionEnum, belt_scale_x)) + ((ConfigOptionFloat, belt_scale_x_angle)) + ((ConfigOptionEnum, belt_scale_y)) + ((ConfigOptionFloat, belt_scale_y_angle)) + ((ConfigOptionEnum, belt_scale_z)) + ((ConfigOptionFloat, belt_scale_z_angle)) + ((ConfigOptionEnum, belt_gcode_remap_x)) + ((ConfigOptionEnum, belt_gcode_remap_y)) + ((ConfigOptionEnum, belt_gcode_remap_z)) //BBS ((ConfigOptionInts, additional_cooling_fan_speed)) ((ConfigOptionBool, reduce_crossing_wall)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 08ff847951..abd309fbff 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -3392,14 +3392,55 @@ void PrintObject::update_slicing_parameters() // Orca: updated function call for XYZ shrinkage compensation if (!m_slicing_params.valid) { coordf_t object_height = this->model_object()->max_z(); - if (this->print()->config().belt_printer.value) { - // After mesh rotation R(-alpha, X), effective height in the slicing - // direction is y_extent*sin(a) + z_extent*cos(a). - double angle_rad = Geometry::deg2rad(this->print()->config().belt_printer_angle.value); - BoundingBoxf3 bb = this->model_object()->raw_bounding_box(); - object_height = bb.size().y() * std::sin(angle_rad) + bb.size().z() * std::cos(angle_rad); + // Belt shear/scale may change the effective Z height. + const auto &pcfg = this->print()->config(); + if (pcfg.belt_printer.value) { + bool has_z_shear = pcfg.belt_shear_z.value != BeltShearMode::None; + bool has_z_scale = pcfg.belt_scale_z.value != BeltScaleMode::None; + if (has_z_shear || has_z_scale) { + auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad); + switch (mode) { + case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; + case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; + case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; + case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; + default: return 0.; + } + }; + auto compute_scale_factor = [](BeltScaleMode mode, double angle_deg) -> double { + if (mode == BeltScaleMode::None) return 1.; + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad); + switch (mode) { + case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; + case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; + case BeltScaleMode::Sin: return sin_a; + case BeltScaleMode::Cos: return cos_a; + default: return 1.; + } + }; + double shear_factor = has_z_shear ? compute_shear_factor(pcfg.belt_shear_z.value, pcfg.belt_shear_z_angle.value) : 0.; + double scale_z = compute_scale_factor(pcfg.belt_scale_z.value, pcfg.belt_scale_z_angle.value); + if (has_z_shear && std::abs(shear_factor) > EPSILON) { + int from = int(pcfg.belt_shear_z_from.value); + BoundingBoxf3 bb = this->model_object()->raw_bounding_box(); + double min_rz = std::numeric_limits::max(); + double max_rz = std::numeric_limits::lowest(); + for (double vz : {bb.min.z(), bb.max.z()}) + for (double vs : {bb.min(from), bb.max(from)}) { + double new_z = scale_z * (vz + shear_factor * vs); + min_rz = std::min(min_rz, new_z); + max_rz = std::max(max_rz, new_z); + } + object_height = max_rz - min_rz; + } else { + object_height *= scale_z; + } + } } - m_slicing_params = SlicingParameters::create_from_config(this->print()->config(), m_config, object_height, + m_slicing_params = SlicingParameters::create_from_config(pcfg, m_config, object_height, this->object_extruders(), this->print()->shrinkage_compensation()); } } @@ -3441,9 +3482,51 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full if (object_max_z <= 0.f) { BoundingBoxf3 bb = model_object.raw_bounding_box(); object_max_z = (float)bb.size().z(); + // Belt shear/scale may change the effective Z height. if (print_config.belt_printer.value) { - double angle_rad = Geometry::deg2rad(print_config.belt_printer_angle.value); - object_max_z = (float)(bb.size().y() * std::sin(angle_rad) + bb.size().z() * std::cos(angle_rad)); + bool has_z_shear = print_config.belt_shear_z.value != BeltShearMode::None; + bool has_z_scale = print_config.belt_scale_z.value != BeltScaleMode::None; + if (has_z_shear || has_z_scale) { + auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad); + switch (mode) { + case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; + case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; + case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; + case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; + default: return 0.; + } + }; + auto compute_scale_factor = [](BeltScaleMode mode, double angle_deg) -> double { + if (mode == BeltScaleMode::None) return 1.; + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad); + switch (mode) { + case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; + case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; + case BeltScaleMode::Sin: return sin_a; + case BeltScaleMode::Cos: return cos_a; + default: return 1.; + } + }; + double shear_factor = has_z_shear ? compute_shear_factor(print_config.belt_shear_z.value, print_config.belt_shear_z_angle.value) : 0.; + double scale_z = compute_scale_factor(print_config.belt_scale_z.value, print_config.belt_scale_z_angle.value); + if (has_z_shear && std::abs(shear_factor) > EPSILON) { + int from = int(print_config.belt_shear_z_from.value); + double min_rz = std::numeric_limits::max(); + double max_rz = std::numeric_limits::lowest(); + for (double vz : {bb.min.z(), bb.max.z()}) + for (double vs : {bb.min(from), bb.max(from)}) { + double new_z = scale_z * (vz + shear_factor * vs); + min_rz = std::min(min_rz, new_z); + max_rz = std::max(max_rz, new_z); + } + object_max_z = (float)(max_rz - min_rz); + } else { + object_max_z *= (float)scale_z; + } + } } } return SlicingParameters::create_from_config(print_config, object_config, object_max_z, object_extruders, object_shrinkage_compensation); diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 2748151aa0..294c245eaa 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -141,23 +141,69 @@ static std::vector slice_volumes_inner( params_base.extra_offset = 0; params_base.trafo = object_trafo; if (print_config.belt_printer.value) { - double angle_rad = Geometry::deg2rad(print_config.belt_printer_angle.value); - // Rotate mesh by -alpha about X so horizontal slice planes = gantry-parallel planes. - // The gantry (XY) is tilted by belt_printer_angle; this rotation aligns it with horizontal. - Transform3d belt_rotation = Transform3d::Identity(); - belt_rotation.rotate(Eigen::AngleAxisd(-angle_rad, Vec3d::UnitX())); - params_base.trafo = belt_rotation * params_base.trafo; - // Compute Z-shift from model_volumes: find min-Z of all rotated meshes - // so the rotated geometry starts at Z=0 (the belt surface in slicing frame). - double min_z_rotated = std::numeric_limits::max(); - for (const ModelVolume *mv : model_volumes) { - if (!model_volume_needs_slicing(*mv)) continue; - BoundingBoxf3 bb = mv->mesh().bounding_box(); - bb = bb.transformed(params_base.trafo * mv->get_matrix()); - min_z_rotated = std::min(min_z_rotated, bb.min.z()); + // Build per-axis shear matrix from 3 independent axis configs. + auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad); + double cos_a = std::cos(angle_rad); + switch (mode) { + case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; + case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; + case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; + case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; + default: return 0.; + } + }; + + struct AxisShear { BeltShearMode mode; double angle; int from; }; + AxisShear axes[3] = { + { print_config.belt_shear_x.value, print_config.belt_shear_x_angle.value, int(print_config.belt_shear_x_from.value) }, + { print_config.belt_shear_y.value, print_config.belt_shear_y_angle.value, int(print_config.belt_shear_y_from.value) }, + { print_config.belt_shear_z.value, print_config.belt_shear_z_angle.value, int(print_config.belt_shear_z_from.value) }, + }; + + Transform3d belt_shear = Transform3d::Identity(); + bool has_shear = false; + for (int row = 0; row < 3; ++row) { + if (axes[row].mode != BeltShearMode::None) { + double factor = compute_shear_factor(axes[row].mode, axes[row].angle); + if (std::abs(factor) > EPSILON) { + belt_shear.matrix()(row, axes[row].from) += factor; + has_shear = true; + } + } } - if (min_z_rotated != std::numeric_limits::max() && std::abs(min_z_rotated) > EPSILON) - params_base.trafo = Eigen::Translation3d(0, 0, -min_z_rotated) * params_base.trafo; + + // Build per-axis scale matrix. + auto compute_scale_factor = [](BeltScaleMode mode, double angle_deg) -> double { + if (mode == BeltScaleMode::None) return 1.; + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad); + double cos_a = std::cos(angle_rad); + switch (mode) { + case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; + case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; + case BeltScaleMode::Sin: return sin_a; + case BeltScaleMode::Cos: return cos_a; + default: return 1.; + } + }; + + Transform3d belt_scale = Transform3d::Identity(); + bool has_scale = false; + double sx = compute_scale_factor(print_config.belt_scale_x.value, print_config.belt_scale_x_angle.value); + double sy = compute_scale_factor(print_config.belt_scale_y.value, print_config.belt_scale_y_angle.value); + double sz = compute_scale_factor(print_config.belt_scale_z.value, print_config.belt_scale_z_angle.value); + if (std::abs(sx - 1.) > EPSILON || std::abs(sy - 1.) > EPSILON || std::abs(sz - 1.) > EPSILON) { + belt_scale.matrix()(0, 0) = sx; + belt_scale.matrix()(1, 1) = sy; + belt_scale.matrix()(2, 2) = sz; + has_scale = true; + } + + // Apply: scale * shear * trafo (shear first, then scale). + if (has_shear || has_scale) + params_base.trafo = belt_scale * belt_shear * params_base.trafo; } //BBS: 0.0025mm is safe enough to simplify the data to speed slicing up for high-resolution model. //Also has on influence on arc fitting which has default resolution 0.0125mm. diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index dd66c918a7..25b4ba4e9c 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1271,7 +1271,7 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const m_max_print_height = gcode_result.printable_height; m_z_offset = gcode_result.z_offset; - m_belt_z_shift = gcode_result.belt_z_shift; + // load_toolpaths(gcode_result, build_volume, exclude_bounding_box); @@ -2210,15 +2210,17 @@ void GCodeViewer::render_toolpaths() { const Camera& camera = wxGetApp().plater()->get_camera(); Matrix4f view = camera.get_view_matrix().matrix().cast(); - // Belt "raw" view: apply slicing rotation to view matrix so toolpaths appear - // in the slicing frame (rotated part with horizontal layers). - if (m_belt_show_raw && m_belt_view_enabled && m_belt_angle_deg > 0.f) { + // Belt "designed" view: apply inverse shear to view matrix so toolpaths appear + // upright (as originally designed) instead of sheared on the belt. + if (m_belt_show_designed && m_belt_view_enabled && m_belt_angle_deg > 0.f) { double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle_deg)); - // Apply R(-alpha, X) * T(0,0,-z_shift) to bring machine coords back to slicing frame. - Transform3d slicing_trafo = Transform3d::Identity(); - slicing_trafo.translate(Vec3d(0., 0., -static_cast(m_belt_z_shift))); - slicing_trafo = Eigen::AngleAxisd(-angle_rad, Vec3d::UnitX()) * slicing_trafo; - view = (camera.get_view_matrix() * slicing_trafo).matrix().cast(); + double sin_a = std::sin(angle_rad); + if (sin_a > 1e-6) { + double cot_alpha = std::cos(angle_rad) / sin_a; + Transform3d inverse_shear = Transform3d::Identity(); + inverse_shear.matrix()(1, 2) = -cot_alpha; // Y -= Z * cot(α) + view = (camera.get_view_matrix() * inverse_shear).matrix().cast(); + } } const libvgcode::Mat4x4 converted_view_matrix = libvgcode::convert(view); const libvgcode::Mat4x4 converted_projetion_matrix = libvgcode::convert(static_cast(camera.get_projection_matrix().matrix().cast())); @@ -4421,7 +4423,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv ImGui::Spacing(); ImGui::Dummy({ window_padding, 0 }); ImGui::SameLine(); - ImGui::Checkbox("Show raw G-code (slicing frame)", &m_belt_show_raw); + ImGui::Checkbox("Show designed view (upright)", &m_belt_show_designed); } legend_height = ImGui::GetCurrentWindow()->Size.y; diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 4bd3746991..184c56e915 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -239,8 +239,7 @@ mutable bool m_no_render_path { false }; bool m_belt_view_enabled = false; float m_belt_angle_deg = 0.f; - float m_belt_z_shift = 0.f; - bool m_belt_show_raw = false; // Toggle: show raw slicing-frame G-code (rotated part) + bool m_belt_show_designed = false; // Toggle: show designed (upright) view via inverse shear libvgcode::Viewer m_viewer; bool m_loaded_as_preview{ false }; diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index fafe46c952..87e97c5ff7 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4369,6 +4369,53 @@ void TabPrinter::build_fff() optgroup->append_single_option_line("belt_printer"); optgroup->append_single_option_line("belt_printer_angle"); optgroup->append_single_option_line("belt_printer_infinite_y"); + // Per-axis shear: group mode + angle + source on one row per axis + { + Line line = { L("Shear X"), L("Shear applied to the X axis before slicing") }; + line.append_option(optgroup->get_option("belt_shear_x")); + line.append_option(optgroup->get_option("belt_shear_x_angle")); + line.append_option(optgroup->get_option("belt_shear_x_from")); + optgroup->append_line(line); + } + { + Line line = { L("Shear Y"), L("Shear applied to the Y axis before slicing") }; + line.append_option(optgroup->get_option("belt_shear_y")); + line.append_option(optgroup->get_option("belt_shear_y_angle")); + line.append_option(optgroup->get_option("belt_shear_y_from")); + optgroup->append_line(line); + } + { + Line line = { L("Shear Z"), L("Shear applied to the Z axis before slicing") }; + line.append_option(optgroup->get_option("belt_shear_z")); + line.append_option(optgroup->get_option("belt_shear_z_angle")); + line.append_option(optgroup->get_option("belt_shear_z_from")); + optgroup->append_line(line); + } + { + Line line = { L("Scale X"), L("Scale applied to the X axis before slicing") }; + line.append_option(optgroup->get_option("belt_scale_x")); + line.append_option(optgroup->get_option("belt_scale_x_angle")); + optgroup->append_line(line); + } + { + Line line = { L("Scale Y"), L("Scale applied to the Y axis before slicing") }; + line.append_option(optgroup->get_option("belt_scale_y")); + line.append_option(optgroup->get_option("belt_scale_y_angle")); + optgroup->append_line(line); + } + { + Line line = { L("Scale Z"), L("Scale applied to the Z axis before slicing") }; + line.append_option(optgroup->get_option("belt_scale_z")); + line.append_option(optgroup->get_option("belt_scale_z_angle")); + optgroup->append_line(line); + } + { + Line line = { L("G-code axis remap"), L("Remap slicing-frame axes to machine axes in G-code output") }; + line.append_option(optgroup->get_option("belt_gcode_remap_x")); + line.append_option(optgroup->get_option("belt_gcode_remap_y")); + line.append_option(optgroup->get_option("belt_gcode_remap_z")); + optgroup->append_line(line); + } optgroup->append_single_option_line("support_multi_bed_types","printer_basic_information_printable_space#support-multi-bed-types"); optgroup->append_single_option_line("best_object_pos", "printer_basic_information_printable_space#best-object-position"); // todo: for multi_extruder test @@ -5232,6 +5279,10 @@ void TabPrinter::toggle_options() bool is_belt = m_config->opt_bool("belt_printer"); toggle_line("belt_printer_angle", is_belt); toggle_line("belt_printer_infinite_y", is_belt); + for (auto el : {"belt_shear_x", "belt_shear_y", "belt_shear_z", + "belt_scale_x", "belt_scale_y", "belt_scale_z", + "belt_gcode_remap_x"}) + toggle_line(el, is_belt); } From 7ff6bc42b14ce245b14c53c0c18bc79e892dea99 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 18 Mar 2026 12:09:28 -0500 Subject: [PATCH 005/434] Part 2.6: Add belt floor support clipping for all support types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix support clipping z-shift calculation by removing coordinate-space mismatch and sync belt_floor_z_shift with global_z_offset; fix invalidation so posSupportMaterial no longer resets slicing params - Add belt floor polygon clipping to non-organic tree support (slim/strong/hybrid) with collision surface integration in TreeSupportData, belt extension layers, and first-layer brim suppression - Add belt floor clipping to organic tree support pipeline with virtual belt raft layers, per-layer polygons in TreeModelVolumes, and post-generation layer trimming; fix pre-existing processing_last_mesh bug in calculateCollision() Fix belt floor support clipping: z-shift, invalidation, and global offset - Fix support clipping z-shift calculation by removing coordinate-space mismatch (raw_bounding_box min.z vs trafo_centered m_belt_min_z) and sync belt_floor_z_shift with global_z_offset in global shear mode - Fix invalidation so posSupportMaterial no longer resets slicing params, preventing the exact posSlice z-shift from being overwritten by the bounding-box approximation on support-only setting changes - Remove double-counting of global z_offset on support layers — support already inherits the offset from object layers during generation This Work Was Co-Authored-By Claude Opus 4.6 (1M context) UI: gray out inactive belt sub-options, rename to mesh transforms, move to Advanced Fix mesh clipping through build plate after belt shear/scale transform Generalize G-code viewer designed-view toggle for full belt transform Clip support layers to transformed belt floor plane Supports below the tilted build plate (Z = shear_factor * from_axis - min_z) are now clipped via half-plane intersection after generation. Belt floor parameters stored in SlicingParameters and populated in both update_slicing_parameters() and the static slicing_parameters() overload. Make belt G-code viewer toggle more prominent, add B keyboard shortcut - Add separator + teal "Belt Printer" header in legend panel - Append [B] hint to checkbox label - Add B key shortcut in GLCanvas3D to toggle designed/machine view - Read belt_printer_angle from loaded G-code headers to enable belt view Add per-axis global transform option for belt printer shear New belt_shear_{x,y,z}_global bool configs. When enabled, shear incorporates instance shift so objects at different bed positions get position-aware transform (Z += factor * instance_shift_on_from_axis). Fix global shear: use layer Z offset instead of mesh transform, add config invalidation - Global shear offset applied as post-slicing layer print_z adjustment instead of mesh transform (which was absorbed by min_z normalization or shifted mesh out of slice range) - Register all belt transform options in Print::invalidate_state_by_config_options to trigger posSlice re-slicing (the fallback only invalidated Print steps, not PrintObject steps — belt changes had no effect without manual re-slice) - Belt gcode remap options added to steps_gcode (gcode-export only) - Skip empty-first-layer check for belt objects with global Z offset WIP: split instances for global shear, relative Z offsets, debug logging - PrintApply: when belt global mode active, prevent instance grouping by adding unique Z perturbation to trafo — each copy becomes its own PrintObject with independent layers - PrintObjectSlice: compute global Z offset relative to minimum Y shift across all PrintObjects (lowest-Y object stays at Z=0) - Debug logging (warning level) for belt global shift values and offsets Known issues: - Cached posSlice results cause stale offsets when mixing copies with individually-added objects — need to compute min baseline outside slice() - Supports still generate to Z=0 instead of object's global Z offset Fix global shear for copied objects: disable shared-object layer optimization When belt global Z shear is active, each object needs unique layer Z values based on its bed position. The shared-object optimization was causing copies to reuse the source object's layers (and its Z offset) instead of computing their own position-based offset. started work on getting supports to work properly one step forward, one step back this version didn't quite work. Getting somewhere though about to add UI controllable tests added configuration options for supports tweak CLAUDE.md to be more aggressive for my machine. This commit should probably be pulled out before contributing upstream still chasing down some bugs moving objects between slices no longer results in improper Z-height because of caching added more data to the debug logs Z offset is getting more global again still not quite there, I think there's a fundamental logic flaw? hunting for bugs finally have a functional fix Add belt floor clipping to tree supports (organic and non-organic) - Add belt floor polygon clipping to non-organic tree support (slim/strong/hybrid) in draw_circles() and terminate nodes at the belt surface instead of the horizontal build plate - Add belt floor clipping to organic tree support pipeline with virtual belt raft layers for sub-floor branch generation, per-layer belt floor polygons in TreeModelVolumes, and post-generation layer trimming - Fix pre-existing processing_last_mesh bug in TreeModelVolumes that prevented m_anti_overhang (support blockers) from ever being applied; skip empty first layer check for belt printers Commits: current approach: make a face surface to build supports to closer! supports now terminate on shear plane, now need to get shear plane to correct Z height nearly there chasing down logic issues still committing for checkpoint, this still does not work still got logic problems... cull support clipping stashing changes for now. Going to focus on getting the global shear OFF support generation dialed first. beginning per object shear calcs Local shear transform is on correct Z offset now local shear finally works now and needs more testing global shear works now, needs thorough testing debugging non-45 degree angles debugging part 2 supports at all angles work now remove debug logging Add belt floor collision to non-organic tree support pipeline - Integrate belt floor as a collision surface in TreeSupportData so branches route around the belt naturally, replacing the explicit termination checks in drop_nodes() - Add belt extension layers below the object after draw_circles() to allow support geometry to extend to the diagonal belt surface instead of terminating at a horizontal first layer - Fix coordinate overflow in belt floor polygons (scale_(1e4) exceeds int32), skip first-layer brim expansion for belt printers, and extend empty first layer check bypass to all belt modes add debug logging, Z translate for tree supports still not seeing any cutoff surface yet adding debug options attempt #2 at trees if hit Z buildplate stop but don't set to_buildplate true getting closer tree support almost there, just need to get rid of the circles at the beginning getting closer belt / shear plane clip works, need to figure out the buidlplate plane issues more logic, added debugging logs supports now extend somewhat below Z=0 in global shear mode fix bad alloc, add 10mm below build plate fully works now shear transform + prusa tree support generation works now. pull out debug logging --- CLAUDE.md | 22 +- src/libslic3r/BuildVolume.hpp | 2 +- src/libslic3r/GCode.cpp | 30 ++- src/libslic3r/GCode/GCodeProcessor.cpp | 78 ++++++ src/libslic3r/GCode/GCodeProcessor.hpp | 33 +++ src/libslic3r/Preset.cpp | 10 +- src/libslic3r/Print.cpp | 62 ++++- src/libslic3r/Print.hpp | 10 +- src/libslic3r/PrintApply.cpp | 30 ++- src/libslic3r/PrintConfig.cpp | 64 +++++ src/libslic3r/PrintConfig.hpp | 23 ++ src/libslic3r/PrintObject.cpp | 116 ++++++++- src/libslic3r/PrintObjectSlice.cpp | 124 ++++++++- src/libslic3r/Slicing.hpp | 7 + src/libslic3r/Support/SupportCommon.hpp | 6 + src/libslic3r/Support/SupportMaterial.cpp | 290 ++++++++++++++++++++- src/libslic3r/Support/TreeModelVolumes.cpp | 117 ++++++++- src/libslic3r/Support/TreeModelVolumes.hpp | 3 + src/libslic3r/Support/TreeSupport.cpp | 98 ++++++- src/libslic3r/Support/TreeSupport.hpp | 4 + src/libslic3r/Support/TreeSupport3D.cpp | 74 ++++++ src/slic3r/GUI/GCodeViewer.cpp | 26 +- src/slic3r/GUI/GCodeViewer.hpp | 4 + src/slic3r/GUI/GLCanvas3D.cpp | 12 +- src/slic3r/GUI/GUI_ObjectList.cpp | 2 +- src/slic3r/GUI/Plater.cpp | 86 +++++- src/slic3r/GUI/Tab.cpp | 141 ++++++---- 27 files changed, 1337 insertions(+), 137 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 25aa516a56..9bd162c42c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,27 +23,7 @@ cmake --build build/arm64 --config RelWithDebInfo --target all -- ### Building on Linux **Always use this command to build the project when testing build issues on Linux.** ```bash -cmake --build build/arm64 --config RelWithDebInfo --target all -- - -``` -### Build test: - -**Always use this command to build the project when testing build issues on Windows.** -```bash -cmake --build . --config %build_type% --target ALL_BUILD -- -m -``` - -### Building on macOS -**Always use this command to build the project when testing build issues on macOS.** -```bash -cmake --build build/arm64 --config RelWithDebInfo --target all -- -``` - -### Building on Linux - **Always use this command to build the project when testing build issues on Linux.** -```bash -cmake --build build --config RelWithDebInfo --target all -- - +systemd-run --user --scope -p MemoryMax=48G cmake --build build --config RelWithDebInfo --target all -- -j18 -l 24 ``` diff --git a/src/libslic3r/BuildVolume.hpp b/src/libslic3r/BuildVolume.hpp index 0494efe9ba..2b0d8c2746 100644 --- a/src/libslic3r/BuildVolume.hpp +++ b/src/libslic3r/BuildVolume.hpp @@ -84,7 +84,7 @@ public: indexed_triangle_set bounding_mesh(bool scale=true) const; // Center of the print bed, unscaled. - Vec2d bed_center() const { return to_2d(m_bboxf.center()); } + Vec2d bed_center() const { return get_extents(m_bed_shape).center(); } // Convex hull of polygon(), scaled. const Polygon& convex_hull() const { return m_convex_hull; } // Smallest enclosing circle of polygon(), scaled. diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 02a233e988..41cfa7b72a 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -1745,8 +1745,16 @@ std::vector GCode::collect_layers_to_print(const PrintObjec // Check that there are extrusions on the very first layer. The case with empty // first layer may result in skirt/brim in the air and maybe other issues. + // Skip this check for belt printers. The shear transform tilts the + // model so the first horizontal layer plane intersects only a thin + // sliver of the model (width ≈ first_layer_height / shear_factor). + // This sliver is often narrower than the nozzle diameter, producing + // zero perimeters and an empty first layer — which is expected, not + // an error. In global shear mode the object may also start above + // Z=0 on the tilted belt surface. if (layers_to_print.size() == 1u) { - if (!has_extrusions) + bool skip_empty_check = object.print()->config().belt_printer.value; + if (!has_extrusions && !skip_empty_check) throw Slic3r::SlicingError(_(L("One object has an empty first layer and can't be printed. Please Cut the bottom or enable supports.")), object.id().id); } @@ -2515,9 +2523,27 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.write_format("; HEADER_BLOCK_START\n"); // Write information on the generator. file.write_format("; generated by %s on %s\n", Slic3r::header_slic3r_generated().c_str(), Slic3r::Utils::local_timestamp().c_str()); - // Belt printer: embed angle in header for G-code processor detection. + // Belt printer: embed angle and transform configs in header for G-code processor detection. if (print.config().belt_printer.value) { file.write_format("; belt_printer_angle = %.1f\n", print.config().belt_printer_angle.value); + // Shear configs + const auto &full_cfg = print.full_print_config(); + file.write_format("; belt_shear_x = %s\n", full_cfg.opt_serialize("belt_shear_x").c_str()); + file.write_format("; belt_shear_x_angle = %.1f\n", print.config().belt_shear_x_angle.value); + file.write_format("; belt_shear_x_from = %s\n", full_cfg.opt_serialize("belt_shear_x_from").c_str()); + file.write_format("; belt_shear_y = %s\n", full_cfg.opt_serialize("belt_shear_y").c_str()); + file.write_format("; belt_shear_y_angle = %.1f\n", print.config().belt_shear_y_angle.value); + file.write_format("; belt_shear_y_from = %s\n", full_cfg.opt_serialize("belt_shear_y_from").c_str()); + file.write_format("; belt_shear_z = %s\n", full_cfg.opt_serialize("belt_shear_z").c_str()); + file.write_format("; belt_shear_z_angle = %.1f\n", print.config().belt_shear_z_angle.value); + file.write_format("; belt_shear_z_from = %s\n", full_cfg.opt_serialize("belt_shear_z_from").c_str()); + // Scale configs + file.write_format("; belt_scale_x = %s\n", full_cfg.opt_serialize("belt_scale_x").c_str()); + file.write_format("; belt_scale_x_angle = %.1f\n", print.config().belt_scale_x_angle.value); + file.write_format("; belt_scale_y = %s\n", full_cfg.opt_serialize("belt_scale_y").c_str()); + file.write_format("; belt_scale_y_angle = %.1f\n", print.config().belt_scale_y_angle.value); + file.write_format("; belt_scale_z = %s\n", full_cfg.opt_serialize("belt_scale_z").c_str()); + file.write_format("; belt_scale_z_angle = %.1f\n", print.config().belt_scale_z_angle.value); } if (is_bbl_printers) file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str()); diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 7ac3a1ffc4..8def7c08db 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -3051,6 +3051,84 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers } catch (...) {} return; } + // Belt printer: parse shear configs from header comments. + { + auto parse_shear_mode = [](const std::string &s) -> BeltShearMode { + if (s == "pos_cot") return BeltShearMode::PosCot; + if (s == "neg_cot") return BeltShearMode::NegCot; + if (s == "pos_tan") return BeltShearMode::PosTan; + if (s == "neg_tan") return BeltShearMode::NegTan; + return BeltShearMode::None; + }; + auto parse_axis = [](const std::string &s) -> BeltAxis { + if (s == "y") return BeltAxis::Y; + if (s == "z") return BeltAxis::Z; + return BeltAxis::X; + }; + auto parse_scale_mode = [](const std::string &s) -> BeltScaleMode { + if (s == "inv_sin") return BeltScaleMode::InvSin; + if (s == "inv_cos") return BeltScaleMode::InvCos; + if (s == "sin") return BeltScaleMode::Sin; + if (s == "cos") return BeltScaleMode::Cos; + return BeltScaleMode::None; + }; + auto trim = [](const std::string &s) -> std::string { + size_t start = s.find_first_not_of(" \t\r\n"); + size_t end = s.find_last_not_of(" \t\r\n"); + return (start == std::string::npos) ? "" : s.substr(start, end - start + 1); + }; + // Shear X + if (boost::starts_with(comment, " belt_shear_x = ")) { + m_result.belt_shear_x = parse_shear_mode(trim(std::string(comment.substr(16)))); return; + } + if (boost::starts_with(comment, " belt_shear_x_angle = ")) { + try { m_result.belt_shear_x_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; + } + if (boost::starts_with(comment, " belt_shear_x_from = ")) { + m_result.belt_shear_x_from = parse_axis(trim(std::string(comment.substr(21)))); return; + } + // Shear Y + if (boost::starts_with(comment, " belt_shear_y = ")) { + m_result.belt_shear_y = parse_shear_mode(trim(std::string(comment.substr(16)))); return; + } + if (boost::starts_with(comment, " belt_shear_y_angle = ")) { + try { m_result.belt_shear_y_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; + } + if (boost::starts_with(comment, " belt_shear_y_from = ")) { + m_result.belt_shear_y_from = parse_axis(trim(std::string(comment.substr(21)))); return; + } + // Shear Z + if (boost::starts_with(comment, " belt_shear_z = ")) { + m_result.belt_shear_z = parse_shear_mode(trim(std::string(comment.substr(16)))); return; + } + if (boost::starts_with(comment, " belt_shear_z_angle = ")) { + try { m_result.belt_shear_z_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; + } + if (boost::starts_with(comment, " belt_shear_z_from = ")) { + m_result.belt_shear_z_from = parse_axis(trim(std::string(comment.substr(21)))); return; + } + // Scale X + if (boost::starts_with(comment, " belt_scale_x = ")) { + m_result.belt_scale_x = parse_scale_mode(trim(std::string(comment.substr(16)))); return; + } + if (boost::starts_with(comment, " belt_scale_x_angle = ")) { + try { m_result.belt_scale_x_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; + } + // Scale Y + if (boost::starts_with(comment, " belt_scale_y = ")) { + m_result.belt_scale_y = parse_scale_mode(trim(std::string(comment.substr(16)))); return; + } + if (boost::starts_with(comment, " belt_scale_y_angle = ")) { + try { m_result.belt_scale_y_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; + } + // Scale Z + if (boost::starts_with(comment, " belt_scale_z = ")) { + m_result.belt_scale_z = parse_scale_mode(trim(std::string(comment.substr(16)))); return; + } + if (boost::starts_with(comment, " belt_scale_z_angle = ")) { + try { m_result.belt_scale_z_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; + } + } // wipe start tag if (boost::starts_with(comment, reserved_tag(ETags::Wipe_Start))) { m_wiping = true; diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index e432a165a8..f953345f4e 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -229,6 +229,23 @@ class Print; float z_offset; // Belt printer: angle for coordinate transformation in preview. float belt_printer_angle{ 0.f }; + // Belt printer: per-axis shear config. + BeltShearMode belt_shear_x{ BeltShearMode::None }; + float belt_shear_x_angle{ 45.f }; + BeltAxis belt_shear_x_from{ BeltAxis::Y }; + BeltShearMode belt_shear_y{ BeltShearMode::None }; + float belt_shear_y_angle{ 45.f }; + BeltAxis belt_shear_y_from{ BeltAxis::Y }; + BeltShearMode belt_shear_z{ BeltShearMode::None }; + float belt_shear_z_angle{ 45.f }; + BeltAxis belt_shear_z_from{ BeltAxis::Y }; + // Belt printer: per-axis scale config. + BeltScaleMode belt_scale_x{ BeltScaleMode::None }; + float belt_scale_x_angle{ 45.f }; + BeltScaleMode belt_scale_y{ BeltScaleMode::None }; + float belt_scale_y_angle{ 45.f }; + BeltScaleMode belt_scale_z{ BeltScaleMode::None }; + float belt_scale_z_angle{ 45.f }; SettingsIds settings_ids; size_t filaments_count; bool backtrace_enabled; @@ -288,6 +305,22 @@ class Print; layer_filaments = other.layer_filaments; filament_change_count_map = other.filament_change_count_map; initial_layer_time = other.initial_layer_time; + belt_printer_angle = other.belt_printer_angle; + belt_shear_x = other.belt_shear_x; + belt_shear_x_angle = other.belt_shear_x_angle; + belt_shear_x_from = other.belt_shear_x_from; + belt_shear_y = other.belt_shear_y; + belt_shear_y_angle = other.belt_shear_y_angle; + belt_shear_y_from = other.belt_shear_y_from; + belt_shear_z = other.belt_shear_z; + belt_shear_z_angle = other.belt_shear_z_angle; + belt_shear_z_from = other.belt_shear_z_from; + belt_scale_x = other.belt_scale_x; + belt_scale_x_angle = other.belt_scale_x_angle; + belt_scale_y = other.belt_scale_y; + belt_scale_y_angle = other.belt_scale_y_angle; + belt_scale_z = other.belt_scale_z; + belt_scale_z_angle = other.belt_scale_z_angle; #if ENABLE_GCODE_VIEWER_STATISTICS time = other.time; #endif diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 9bc5e0a782..9437b6098e 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1010,11 +1010,13 @@ static std::vector s_Preset_machine_limits_options { static std::vector s_Preset_printer_options { "printer_technology", - "printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "build_plate_tilt_x", "build_plate_tilt_y", "belt_printer", "belt_printer_angle", "belt_printer_infinite_y", "belt_shear_x", "belt_shear_x_angle", "belt_shear_x_from", - "belt_shear_y", "belt_shear_y_angle", "belt_shear_y_from", - "belt_shear_z", "belt_shear_z_angle", "belt_shear_z_from", + "printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "build_plate_tilt_x", "build_plate_tilt_y", "belt_printer", "belt_printer_angle", "belt_printer_infinite_y", "belt_shear_x", "belt_shear_x_angle", "belt_shear_x_from", "belt_shear_x_global", + "belt_shear_y", "belt_shear_y_angle", "belt_shear_y_from", "belt_shear_y_global", + "belt_shear_z", "belt_shear_z_angle", "belt_shear_z_from", "belt_shear_z_global", "belt_scale_x", "belt_scale_x_angle", "belt_scale_y", "belt_scale_y_angle", "belt_scale_z", "belt_scale_z_angle", - "belt_gcode_remap_x", "belt_gcode_remap_y", "belt_gcode_remap_z", "gcode_flavor", + "belt_gcode_remap_x", "belt_gcode_remap_y", "belt_gcode_remap_z", + "belt_support_floor_offset", "belt_support_floor_mode", "belt_support_z_offset_mode", + "gcode_flavor", "fan_kickstart", "fan_speedup_time", "fan_speedup_overhangs", "single_extruder_multi_material", "manual_filament_change", "file_start_gcode", "machine_start_gcode", "machine_end_gcode", "before_layer_change_gcode", "printing_by_object_gcode", "layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode", "change_filament_gcode", "change_extrusion_role_gcode", "printer_model", "printer_variant", "printer_extruder_id", "printer_extruder_variant", "extruder_variant_list", "default_nozzle_volume_type", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index f245cc32b6..0aef12a74b 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -99,6 +99,10 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n // Cache the plenty of parameters, which influence the G-code generator only, // or they are only notes not influencing the generated G-code. static std::unordered_set steps_gcode = { + // Belt printer G-code axis remap (only affects G-code output, not slicing). + "belt_gcode_remap_x", + "belt_gcode_remap_y", + "belt_gcode_remap_z", //BBS "additional_cooling_fan_speed", "reduce_crossing_wall", @@ -275,8 +279,34 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n // Spiral Vase forces different kind of slicing than the normal model: // In Spiral Vase mode, holes are closed and only the largest area contour is kept at each layer. // Therefore toggling the Spiral Vase on / off requires complete reslicing. - || opt_key == "spiral_mode") { + || opt_key == "spiral_mode" + // Belt printer transform options change the mesh geometry before slicing. + || opt_key == "belt_printer" + || opt_key == "belt_printer_angle" + || opt_key == "belt_shear_x" + || opt_key == "belt_shear_x_angle" + || opt_key == "belt_shear_x_from" + || opt_key == "belt_shear_x_global" + || opt_key == "belt_shear_y" + || opt_key == "belt_shear_y_angle" + || opt_key == "belt_shear_y_from" + || opt_key == "belt_shear_y_global" + || opt_key == "belt_shear_z" + || opt_key == "belt_shear_z_angle" + || opt_key == "belt_shear_z_from" + || opt_key == "belt_shear_z_global" + || opt_key == "belt_scale_x" + || opt_key == "belt_scale_x_angle" + || opt_key == "belt_scale_y" + || opt_key == "belt_scale_y_angle" + || opt_key == "belt_scale_z" + || opt_key == "belt_scale_z_angle") { osteps.emplace_back(posSlice); + } else if ( + opt_key == "belt_support_floor_offset" + || opt_key == "belt_support_floor_mode" + || opt_key == "belt_support_z_offset_mode") { + osteps.emplace_back(posSupportMaterial); } else if ( opt_key == "print_sequence" || opt_key == "filament_type" @@ -2124,15 +2154,21 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) int object_count = m_objects.size(); std::set need_slicing_objects; std::set re_slicing_objects; + // Belt global Z shear: each object needs unique layer Z values based on + // its bed position, so sharing layers between "identical" objects is wrong. + bool belt_no_share = m_config.belt_printer.value && m_config.belt_shear_z_global.value + && m_config.belt_shear_z.value != BeltShearMode::None; if (!use_cache) { for (int index = 0; index < object_count; index++) { PrintObject *obj = m_objects[index]; - for (PrintObject *slicing_obj : need_slicing_objects) - { - if (is_print_object_the_same(obj, slicing_obj)) { - obj->set_shared_object(slicing_obj); - break; + if (!belt_no_share) { + for (PrintObject *slicing_obj : need_slicing_objects) + { + if (is_print_object_the_same(obj, slicing_obj)) { + obj->set_shared_object(slicing_obj); + break; + } } } if (!obj->get_shared_object()) @@ -2151,12 +2187,14 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) PrintObject *obj = m_objects[index]; bool found_shared = false; if (need_slicing_objects.find(obj) == need_slicing_objects.end()) { - for (PrintObject *slicing_obj : need_slicing_objects) - { - if (is_print_object_the_same(obj, slicing_obj)) { - obj->set_shared_object(slicing_obj); - found_shared = true; - break; + if (!belt_no_share) { + for (PrintObject *slicing_obj : need_slicing_objects) + { + if (is_print_object_the_same(obj, slicing_obj)) { + obj->set_shared_object(slicing_obj); + found_shared = true; + break; + } } } if (!found_shared) { diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index b98811c8e3..d0c050f442 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -573,7 +573,15 @@ private: PrintObject* m_shared_object{ nullptr }; - + // Belt printer: global Z offset applied to this object's layers for shear positioning. + double m_belt_global_z_offset { 0.0 }; + // Belt printer: min_z of mesh after belt shear (before Z-shift), for z_offset calc. + double m_belt_min_z { 0.0 }; +public: + double belt_global_z_offset() const { return m_belt_global_z_offset; } +private: + + // SoftFever // // object id diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index d2a44463ea..7f3b41b0f1 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -135,22 +135,29 @@ struct PrintObjectTrafoAndInstances // Generate a list of trafos and XY offsets for instances of a ModelObject // Orca: Updated to include XYZ filament shrinkage compensation -static std::vector print_objects_from_model_object(const ModelObject &model_object, const Vec3d &shrinkage_compensation) +static std::vector print_objects_from_model_object(const ModelObject &model_object, const Vec3d &shrinkage_compensation, bool force_separate_instances = false) { std::set trafos; PrintObjectTrafoAndInstances trafo; //BBS: add useful logs for debug int index = 0; + int unique_counter = 0; for (ModelInstance *model_instance : model_object.instances) { if (model_instance->is_printable()) { // Orca: Updated with XYZ filament shrinkage compensation Geometry::Transformation model_instance_transformation = model_instance->get_transformation(); trafo.trafo = model_instance_transformation.get_matrix_with_applied_shrinkage_compensation(shrinkage_compensation); - + auto shift = Point::new_scale(trafo.trafo.data()[12], trafo.trafo.data()[13]); // Reset the XY axes of the transformation. trafo.trafo.data()[12] = 0; trafo.trafo.data()[13] = 0; + // Belt printer global mode: prevent instance grouping so each + // copy gets its own PrintObject with independent layer Z values. + // Add a tiny unique perturbation to the existing Z (don't replace + // it — the Z translation from ensure_on_bed must be preserved). + if (force_separate_instances) + trafo.trafo.data()[14] += 1e-10 * (++unique_counter); // Search or insert a trafo. auto it = trafos.emplace(trafo).first; const_cast(*it).instances.emplace_back(PrintInstance{ nullptr, model_instance, shift }); @@ -1506,11 +1513,16 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ PrintObjectPtrs print_objects_new; print_objects_new.reserve(std::max(m_objects.size(), m_model.objects.size())); bool new_objects = false; + bool belt_instances_shifted = false; // Walk over all new model objects and check, whether there are matching PrintObjects. for (ModelObject *model_object : m_model.objects) { ModelObjectStatus &model_object_status = const_cast(model_object_status_db.reuse(*model_object)); // Orca: Updated for XYZ filament shrink compensation - model_object_status.print_instances = print_objects_from_model_object(*model_object, this->shrinkage_compensation()); + // Belt global mode: force each instance into its own PrintObject + // so each gets independent layer Z values. + bool belt_force_separate = m_config.belt_printer.value && m_config.belt_shear_z_global.value + && m_config.belt_shear_z.value != BeltShearMode::None; + model_object_status.print_instances = print_objects_from_model_object(*model_object, this->shrinkage_compensation(), belt_force_separate); std::vector old; old.reserve(print_object_status_db.count(*model_object)); for (const PrintObjectStatus &print_object_status : print_object_status_db.get_range(*model_object)) @@ -1558,6 +1570,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ if (status != PrintBase::APPLY_STATUS_UNCHANGED) { size_t extruder_num = new_full_config.option("nozzle_diameter")->size(); update_apply_status(status == PrintBase::APPLY_STATUS_INVALIDATED); + belt_instances_shifted = true; } print_objects_new.emplace_back((*it_old)->print_object); const_cast(*it_old)->status = PrintObjectStatus::Reused; @@ -1593,6 +1606,17 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ update_apply_status(object->invalidate_step(posSlice)); } } + + // Belt printer global mode: when any object's instances shifted, + // recompute m_belt_global_z_offset for ALL objects (it depends on + // min_shift across all objects, so one move affects everyone). + if (belt_instances_shifted + && m_config.belt_printer.value + && m_config.belt_shear_z_global.value + && m_config.belt_shear_z.value != BeltShearMode::None) { + for (PrintObject *object : m_objects) + update_apply_status(object->invalidate_step(posSlice)); + } } //BBS: check the config again diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index a722a3bae7..7d06f96a85 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -327,6 +327,21 @@ static t_config_enum_values s_keys_map_BeltRemapAxis { }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltRemapAxis) +static t_config_enum_values s_keys_map_BeltSupportFloorMode { + { "none", int(BeltSupportFloorMode::None) }, + { "generator_only", int(BeltSupportFloorMode::GeneratorOnly) }, + { "clip_only", int(BeltSupportFloorMode::ClipOnly) }, + { "both", int(BeltSupportFloorMode::Both) }, +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltSupportFloorMode) + +static t_config_enum_values s_keys_map_BeltSupportZOffsetMode { + { "none", int(BeltSupportZOffsetMode::None) }, + { "unconditional", int(BeltSupportZOffsetMode::Unconditional) }, + { "raft_only", int(BeltSupportZOffsetMode::RaftOnly) }, +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltSupportZOffsetMode) + static t_config_enum_values s_keys_map_SupportMaterialPattern { { "rectilinear", smpRectilinear }, { "rectilinear-grid", smpRectilinearGrid }, @@ -6047,17 +6062,29 @@ void PrintConfigDef::init_fff_params() def->set_default_value(new ConfigOptionEnum(default_axis)); }; + auto add_belt_shear_global = [this](const char *key, const char *label) { + auto def = this->add(key, coBool); + def->label = L(label); + def->category = L("Printable space"); + def->tooltip = L("Apply shear in global coordinates (position-aware) rather than object-local coordinates."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + }; + add_belt_shear_mode("belt_shear_x", "Function", BeltShearMode::None); add_belt_shear_angle("belt_shear_x_angle", "Angle"); add_belt_axis_enum("belt_shear_x_from", "From", "Source axis for X shear.", BeltAxis::Z); + add_belt_shear_global("belt_shear_x_global", "Global"); add_belt_shear_mode("belt_shear_y", "Function", BeltShearMode::PosCot); add_belt_shear_angle("belt_shear_y_angle", "Angle"); add_belt_axis_enum("belt_shear_y_from", "From", "Source axis for Y shear.", BeltAxis::Z); + add_belt_shear_global("belt_shear_y_global", "Global"); add_belt_shear_mode("belt_shear_z", "Function", BeltShearMode::None); add_belt_shear_angle("belt_shear_z_angle", "Angle"); add_belt_axis_enum("belt_shear_z_from", "From", "Source axis for Z shear.", BeltAxis::Y); + add_belt_shear_global("belt_shear_z_global", "Global"); // Per-axis scale controls for belt printer auto add_belt_scale_mode = [this](const char *key, const char *label, BeltScaleMode default_mode) { @@ -6109,6 +6136,43 @@ void PrintConfigDef::init_fff_params() add_belt_remap("belt_gcode_remap_y", "Y", "Which slicing axis maps to machine Y in G-code output.", BeltRemapAxis::PosY); add_belt_remap("belt_gcode_remap_z", "Z", "Which slicing axis maps to machine Z in G-code output.", BeltRemapAxis::PosZ); + // Belt support floor debug controls + def = this->add("belt_support_floor_offset", coFloat); + def->label = L("Floor Z offset"); + def->category = L("Printable space"); + def->tooltip = L("Shifts the computed belt floor up or down (mm). Negative values lower the floor, allowing more supports to survive. Use this to diagnose belt floor formula issues."); + def->sidetext = L("mm"); + def->min = -500; + def->max = 500; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0)); + + { + auto def = this->add("belt_support_floor_mode", coEnum); + def->label = L("Floor mode"); + def->category = L("Printable space"); + def->tooltip = L("Controls belt floor awareness for supports. 'None' disables belt floor logic. " + "'Generator only' stops support generation at the belt floor plane."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values = {"none", "generator_only"}; + def->enum_labels = {L("None"), L("Generator only")}; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(BeltSupportFloorMode::GeneratorOnly)); + } + + { + auto def = this->add("belt_support_z_offset_mode", coEnum); + def->label = L("Z offset mode"); + def->category = L("Printable space"); + def->tooltip = L("How global Z offset is applied to support layers for belt printers with global shear. " + "'None' = don't offset. 'Unconditional' = offset all layers. 'Raft only' = only offset raft layers."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values = {"none", "unconditional", "raft_only"}; + def->enum_labels = {L("None"), L("Unconditional"), L("Raft only")}; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(BeltSupportZOffsetMode::Unconditional)); + } + def = this->add("tree_support_branch_angle", coFloat); def->label = L("Tree support branch angle"); def->category = L("Support"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 4f149d9c8b..fc533bf6bb 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -189,6 +189,21 @@ enum class BeltRemapAxis RevX = 6, RevY = 7, RevZ = 8, // Reversed: max - pos }; +enum class BeltSupportFloorMode +{ + None, // No belt floor awareness + GeneratorOnly, // Only in tree support drop_nodes/contact_points + ClipOnly, // Only post-processing clipping + Both, // Both generator and clipping +}; + +enum class BeltSupportZOffsetMode +{ + None, // Don't apply global_z_offset to support layers + Unconditional, // Apply to all support layers + RaftOnly, // Only apply to raft layers +}; + enum SupportMaterialPattern { smpDefault, smpRectilinear, smpRectilinearGrid, smpHoneycomb, @@ -536,6 +551,8 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltShearMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltScaleMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltAxis) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltRemapAxis) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltSupportFloorMode) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltSupportZOffsetMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialPattern) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialStyle) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialInterfacePattern) @@ -1456,12 +1473,15 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionEnum, belt_shear_x)) ((ConfigOptionFloat, belt_shear_x_angle)) ((ConfigOptionEnum, belt_shear_x_from)) + ((ConfigOptionBool, belt_shear_x_global)) ((ConfigOptionEnum, belt_shear_y)) ((ConfigOptionFloat, belt_shear_y_angle)) ((ConfigOptionEnum, belt_shear_y_from)) + ((ConfigOptionBool, belt_shear_y_global)) ((ConfigOptionEnum, belt_shear_z)) ((ConfigOptionFloat, belt_shear_z_angle)) ((ConfigOptionEnum, belt_shear_z_from)) + ((ConfigOptionBool, belt_shear_z_global)) ((ConfigOptionEnum, belt_scale_x)) ((ConfigOptionFloat, belt_scale_x_angle)) ((ConfigOptionEnum, belt_scale_y)) @@ -1471,6 +1491,9 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionEnum, belt_gcode_remap_x)) ((ConfigOptionEnum, belt_gcode_remap_y)) ((ConfigOptionEnum, belt_gcode_remap_z)) + ((ConfigOptionFloat, belt_support_floor_offset)) + ((ConfigOptionEnum, belt_support_floor_mode)) + ((ConfigOptionEnum, belt_support_z_offset_mode)) //BBS ((ConfigOptionInts, additional_cooling_fan_speed)) ((ConfigOptionBool, reduce_crossing_wall)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index abd309fbff..f803cc0369 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -9,6 +9,7 @@ #include "MutablePolygon.hpp" #include "PrintConfig.hpp" #include "Support/SupportMaterial.hpp" +#include "Support/SupportCommon.hpp" #include "Support/SupportSpotsGenerator.hpp" #include "Support/TreeSupport.hpp" #include "Surface.hpp" @@ -1365,7 +1366,12 @@ bool PrintObject::invalidate_step(PrintObjectStep step) } else if (step == posSupportMaterial) { invalidated |= this->invalidate_steps({ posSimplifySupportPath }); invalidated |= m_print->invalidate_steps({ psSkirtBrim }); - m_slicing_params.valid = false; + // NOTE: do NOT set m_slicing_params.valid = false here. + // belt_floor_z_shift is patched to an exact value during posSlice + // (PrintObjectSlice.cpp, after slice_volumes). Invalidating slicing + // params here causes update_slicing_parameters() to overwrite that + // exact value with a bounding-box approximation, while posSlice does + // not re-run to correct it — breaking belt support clipping. } // Wipe tower depends on the ordering of extruders, which in turn depends on everything. @@ -3392,6 +3398,10 @@ void PrintObject::update_slicing_parameters() // Orca: updated function call for XYZ shrinkage compensation if (!m_slicing_params.valid) { coordf_t object_height = this->model_object()->max_z(); + // Belt floor parameters for support clipping (populated below if belt Z-shear is active). + double belt_floor_shear_factor_out = 0.0; + int belt_floor_from_axis_out = 1; + double belt_floor_z_shift_out = 0.0; // Belt shear/scale may change the effective Z height. const auto &pcfg = this->print()->config(); if (pcfg.belt_printer.value) { @@ -3435,6 +3445,12 @@ void PrintObject::update_slicing_parameters() max_rz = std::max(max_rz, new_z); } object_height = max_rz - min_rz; + belt_floor_shear_factor_out = shear_factor; + belt_floor_from_axis_out = from; + // Belt contact surface starts at bb.min.z() pre-shear; add the + // slicing Z-shift that keeps the mesh above Z=0. + // Exact value is patched after slice_volumes() in posSlice. + belt_floor_z_shift_out = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.); } else { object_height *= scale_z; } @@ -3442,6 +3458,10 @@ void PrintObject::update_slicing_parameters() } m_slicing_params = SlicingParameters::create_from_config(pcfg, m_config, object_height, this->object_extruders(), this->print()->shrinkage_compensation()); + // Populate belt floor parameters into slicing params for support clipping. + m_slicing_params.belt_floor_shear_factor = belt_floor_shear_factor_out; + m_slicing_params.belt_floor_from_axis = belt_floor_from_axis_out; + m_slicing_params.belt_floor_z_shift = belt_floor_z_shift_out; } } @@ -3479,6 +3499,11 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full sort_remove_duplicates(object_extruders); //FIXME add painting extruders + // Belt floor parameters for support clipping (populated below if belt Z-shear is active). + double belt_floor_shear_factor_out = 0.0; + int belt_floor_from_axis_out = 1; + double belt_floor_z_shift_out = 0.0; + if (object_max_z <= 0.f) { BoundingBoxf3 bb = model_object.raw_bounding_box(); object_max_z = (float)bb.size().z(); @@ -3523,13 +3548,20 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full max_rz = std::max(max_rz, new_z); } object_max_z = (float)(max_rz - min_rz); + belt_floor_shear_factor_out = shear_factor; + belt_floor_from_axis_out = from; + belt_floor_z_shift_out = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.); } else { object_max_z *= (float)scale_z; } } } } - return SlicingParameters::create_from_config(print_config, object_config, object_max_z, object_extruders, object_shrinkage_compensation); + SlicingParameters params = SlicingParameters::create_from_config(print_config, object_config, object_max_z, object_extruders, object_shrinkage_compensation); + params.belt_floor_shear_factor = belt_floor_shear_factor_out; + params.belt_floor_from_axis = belt_floor_from_axis_out; + params.belt_floor_z_shift = belt_floor_z_shift_out; + return params; } // returns 0-based indices of extruders used to print the object (without brim, support and other helper extrusions) @@ -4038,6 +4070,67 @@ void PrintObject::combine_infill() } } +// Belt printer: clip an ExtrusionEntityCollection to a region defined by clip_expoly. +// Handles ExtrusionPath, ExtrusionMultiPath, ExtrusionLoop, and nested ExtrusionEntityCollection. +static void clip_support_fills(ExtrusionEntityCollection &fills, const ExPolygons &clip_region) +{ + ExtrusionEntitiesPtr new_entities; + for (ExtrusionEntity *entity : fills.entities) { + if (auto *path = dynamic_cast(entity)) { + ExtrusionEntityCollection clipped; + path->intersect_expolygons(clip_region, &clipped); + if (!clipped.empty()) { + for (ExtrusionEntity *e : clipped.entities) + new_entities.push_back(e->clone()); + } + delete entity; + } else if (auto *multipath = dynamic_cast(entity)) { + ExtrusionPaths new_paths; + for (const ExtrusionPath &p : multipath->paths) { + ExtrusionEntityCollection clipped; + p.intersect_expolygons(clip_region, &clipped); + for (ExtrusionEntity *e : clipped.entities) + if (auto *cp = dynamic_cast(e)) + new_paths.push_back(std::move(*cp)); + } + if (!new_paths.empty()) { + multipath->paths = std::move(new_paths); + new_entities.push_back(multipath); + } else { + delete entity; + } + } else if (auto *loop = dynamic_cast(entity)) { + ExtrusionPaths new_paths; + for (const ExtrusionPath &p : loop->paths) { + ExtrusionEntityCollection clipped; + p.intersect_expolygons(clip_region, &clipped); + for (ExtrusionEntity *e : clipped.entities) + if (auto *cp = dynamic_cast(e)) + new_paths.push_back(std::move(*cp)); + } + if (!new_paths.empty()) { + // Loop is no longer a closed loop after clipping; emit as individual paths. + for (auto &p : new_paths) + new_entities.push_back(new ExtrusionPath(std::move(p))); + delete entity; + } else { + delete entity; + } + } else if (auto *coll = dynamic_cast(entity)) { + clip_support_fills(*coll, clip_region); + if (!coll->empty()) { + new_entities.push_back(coll); + } else { + delete entity; + } + } else { + // Unknown entity type — keep as-is. + new_entities.push_back(entity); + } + } + fills.entities = std::move(new_entities); +} + void PrintObject::_generate_support_material() { if (is_tree(m_config.support_type.value)) { @@ -4049,6 +4142,25 @@ void PrintObject::_generate_support_material() PrintObjectSupportMaterial support_material(this, m_slicing_params); support_material.generate(*this); } + // Global Z offset for support layers: + // - Normal support: layers already inherit global_z_offset from object layers. + // - Non-organic tree support (slim/strong/hybrid): plan_layer_heights() reads + // from globally-offset object layers, so support layers already have it. + // - Organic tree support: generate_tree_support_3D() computes its own Z values + // independently and does NOT inherit the offset — apply it here. + // Belt floor polygon clipping for non-organic tree support is done inside + // draw_circles() before area_groups and toolpaths are built. + if (is_tree(m_config.support_type.value) && std::abs(m_belt_global_z_offset) > EPSILON) { + // Resolve effective support style (same logic as SupportParameters). + auto style = m_config.support_style.value; + if (style == smsDefault) + style = smsTreeOrganic; + if (style == smsTreeOrganic) { + for (SupportLayer *sl : m_support_layers) + sl->print_z += m_belt_global_z_offset; + } + } + } // BBS diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 294c245eaa..404550da1a 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -1,4 +1,5 @@ #include +#include #include @@ -125,7 +126,8 @@ static std::vector slice_volumes_inner( ModelVolumePtrs model_volumes, const std::vector &layer_ranges, const std::vector &zs, - const std::function &throw_on_cancel_callback) + const std::function &throw_on_cancel_callback, + double *out_belt_min_z = nullptr) { model_volumes_sort_by_id(model_volumes); @@ -202,8 +204,32 @@ static std::vector slice_volumes_inner( } // Apply: scale * shear * trafo (shear first, then scale). - if (has_shear || has_scale) + if (has_shear || has_scale) { params_base.trafo = belt_scale * belt_shear * params_base.trafo; + + // After the shear/scale transform, the mesh may clip through the + // build plate (Z < 0). Detect this and shift the mesh up. + Transform3d combined = params_base.trafo; + double min_z = std::numeric_limits::max(); + for (const ModelVolume *mv : model_volumes) { + if (!mv->is_model_part()) continue; + for (const stl_vertex &v : mv->mesh().its.vertices) { + Vec3d pt = combined * v.cast(); + min_z = std::min(min_z, pt.z()); + } + } + double belt_z_shift_val = (min_z < 0. && min_z != std::numeric_limits::max()) ? -min_z : 0.; + BOOST_LOG_TRIVIAL(warning) << "Belt Z-shift: min_z=" << min_z + << " z_shift=" << belt_z_shift_val + << " trafo_z=" << object_trafo.matrix()(2, 3); + if (belt_z_shift_val > 0.) { + Transform3d z_shift = Transform3d::Identity(); + z_shift.matrix()(2, 3) = belt_z_shift_val; + params_base.trafo = z_shift * params_base.trafo; + } + if (out_belt_min_z) + *out_belt_min_z = (min_z != std::numeric_limits::max()) ? min_z : 0.; + } } //BBS: 0.0025mm is safe enough to simplify the data to speed slicing up for high-resolution model. //Also has on influence on arc fitting which has default resolution 0.0125mm. @@ -866,6 +892,20 @@ void PrintObject::slice() m_layers = new_layers(this, generate_object_layers(m_slicing_params, layer_height_profile, m_config.precise_z_height.value)); this->slice_volumes(); m_print->throw_if_canceled(); + + // After slicing, m_belt_min_z holds the exact post-shear minimum Z + // in trafo_centered space (which includes the ensure_on_bed Z offset). + // The belt surface is at Z=0 in trafo_centered space; after shear it + // becomes Z = sf*Y, and after the z-shift that keeps the mesh above + // Z=0 it becomes Z = sf*Y + z_shift_val. So belt_floor_z_shift is + // simply the z-shift applied, i.e. max(0, -m_belt_min_z). + // NOTE: do NOT add raw_bounding_box().min.z() here — m_belt_min_z + // already includes the ensure_on_bed offset, unlike the min_rz used + // in update_slicing_parameters() which needs that compensation. + if (std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON) { + m_slicing_params.belt_floor_z_shift = (m_belt_min_z < 0.) ? -m_belt_min_z : 0.; + } + int firstLayerReplacedBy = 0; #if 0 @@ -904,6 +944,83 @@ void PrintObject::slice() if (m_layers.empty()) throw Slic3r::SlicingError(L("No layers were detected. You might want to repair your STL file(s) or check their size or thickness and retry.\n")); + // Belt printer global mode: offset all layer Z values so objects at + // different bed positions print at different heights on the tilted belt. + // This is a post-slicing adjustment — the sliced geometry is identical + // regardless of global mode, only the output Z coordinates change. + { + const auto &pcfg = this->print()->config(); + BOOST_LOG_TRIVIAL(warning) << "Belt global check: belt_printer=" << pcfg.belt_printer.value + << " belt_shear_z=" << int(pcfg.belt_shear_z.value) + << " belt_shear_z_global=" << pcfg.belt_shear_z_global.value + << " object=" << this->model_object()->name; + if (pcfg.belt_printer.value) { + auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad); + double cos_a = std::cos(angle_rad); + switch (mode) { + case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; + case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; + case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; + case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; + default: return 0.; + } + }; + + Point inst_shift = this->instances().empty() ? Point(0, 0) + : this->instances().front().shift - this->center_offset(); + BOOST_LOG_TRIVIAL(warning) << "Belt global: object " << this->model_object()->name + << " instances=" << this->instances().size() + << " shift=(" << unscale(inst_shift.x()) << ", " << unscale(inst_shift.y()) << ")"; + double global_z_offset = 0.; + + struct GAxis { BeltShearMode mode; double angle; int from; bool global; }; + GAxis gaxes[3] = { + { pcfg.belt_shear_x.value, pcfg.belt_shear_x_angle.value, int(pcfg.belt_shear_x_from.value), pcfg.belt_shear_x_global.value }, + { pcfg.belt_shear_y.value, pcfg.belt_shear_y_angle.value, int(pcfg.belt_shear_y_from.value), pcfg.belt_shear_y_global.value }, + { pcfg.belt_shear_z.value, pcfg.belt_shear_z_angle.value, int(pcfg.belt_shear_z_from.value), pcfg.belt_shear_z_global.value }, + }; + + // Only the Z-row shear contributes a Z offset from global mode. + // (X/Y row shears with global would offset X/Y, not Z — not useful here.) + // Offsets are RELATIVE: we subtract the minimum shift across all + // PrintObjects so the lowest-positioned object stays at Z=0. + const auto &za = gaxes[2]; // Z row + if (za.global && za.mode != BeltShearMode::None && za.from < 2) { + double factor = compute_shear_factor(za.mode, za.angle); + // The Z-shift brought the mesh's lowest sheared vertex to + // Z=0. That vertex's physical Y determines the belt contact + // point. With trafo_z preserved (ensure_on_bed offset), + // min_z = Y_at_contact * factor for bottom-face vertices, + // so: z_offset = center_Y * factor + min_z. + Point phys = inst_shift; // already has center_offset subtracted + double center_on_axis = (za.from == 0) ? unscale(phys.x()) : unscale(phys.y()); + global_z_offset += center_on_axis * factor + m_belt_min_z; + } + + BOOST_LOG_TRIVIAL(warning) << "Belt global: z_offset=" << global_z_offset + << " za.global=" << za.global << " za.mode=" << int(za.mode) << " za.from=" << za.from + << " (relative to min across " << this->print()->objects().size() << " objects)"; + m_belt_global_z_offset = global_z_offset; + if (std::abs(global_z_offset) > EPSILON) { + for (Layer *layer : m_layers) + layer->print_z += global_z_offset; + // Keep belt floor clipping in sync with the shifted print_z + // values — the support generator sees globally-offset object + // layer print_z, so belt_floor_z_shift must match. + m_slicing_params.belt_floor_z_shift += global_z_offset; + } + if (!m_layers.empty()) { + BOOST_LOG_TRIVIAL(warning) << "Belt global: first_layer_z=" << m_layers.front()->print_z + << " last_layer_z=" << m_layers.back()->print_z + << " num_layers=" << m_layers.size() + << " center_offset=(" << unscale(m_center_offset.x()) + << ", " << unscale(m_center_offset.y()) << ")"; + } + } + } + // BBS this->set_done(posSlice); } @@ -1208,7 +1325,8 @@ void PrintObject::slice_volumes() if (!slice_zs.empty()) { objSliceByVolume = slice_volumes_inner( print->config(), this->config(), this->trafo_centered(), - this->model_object()->volumes, m_shared_regions->layer_ranges, slice_zs, throw_on_cancel_callback); + this->model_object()->volumes, m_shared_regions->layer_ranges, slice_zs, throw_on_cancel_callback, + &m_belt_min_z); } //BBS: "model_part" volumes are grouded according to their connections diff --git a/src/libslic3r/Slicing.hpp b/src/libslic3r/Slicing.hpp index a7b21a140d..b06f2a7bc4 100644 --- a/src/libslic3r/Slicing.hpp +++ b/src/libslic3r/Slicing.hpp @@ -110,6 +110,13 @@ struct SlicingParameters coordf_t object_print_z_uncompensated_max { 0 }; // Scaling factor for compensating shrinkage in Z-axis. coordf_t object_shrinkage_compensation_z { 0 }; + + // Belt printer: floor plane parameters for support clipping. + // Belt contact surface in slicing coords: Z = bb_min_z + sf*Y + slicing_z_shift. + // cutoff = (print_z - belt_floor_z_shift - floor_offset) / shear_factor + double belt_floor_shear_factor { 0.0 }; // shear factor (e.g. cot(45deg)) + int belt_floor_from_axis { 1 }; // which axis the shear is from (0=X, 1=Y) + double belt_floor_z_shift { 0.0 }; // bb_min_z + max(0, -min_z_after_shear) }; static_assert(IsTriviallyCopyable::value, "SlicingParameters class is not POD (and it should be - see constructor)."); diff --git a/src/libslic3r/Support/SupportCommon.hpp b/src/libslic3r/Support/SupportCommon.hpp index 531daaa0a8..f15c7d417a 100644 --- a/src/libslic3r/Support/SupportCommon.hpp +++ b/src/libslic3r/Support/SupportCommon.hpp @@ -144,6 +144,12 @@ int idx_lower_or_equal(const std::vector &vec, int idx, FN_LOWER_EQUAL fn_lo return idx_lower_or_equal(vec.begin(), vec.end(), idx, fn_lower_equal); } +// Belt floor: compute the belt-side half-plane polygon at a given print_z. +// Used to clip support polygons against the belt surface. +Polygons belt_floor_surface_polygon( + const SlicingParameters &slicing_params, const PrintConfig &print_config, + const PrintObject &object, coordf_t print_z); + } // namespace Slic3r #endif /* slic3r_SupportCommon_hpp_ */ diff --git a/src/libslic3r/Support/SupportMaterial.cpp b/src/libslic3r/Support/SupportMaterial.cpp index c230924ac8..d70153a7dd 100644 --- a/src/libslic3r/Support/SupportMaterial.cpp +++ b/src/libslic3r/Support/SupportMaterial.cpp @@ -367,10 +367,21 @@ inline void layers_append(SupportGeneratorLayersPtr &dst, const SupportGenerator } // Support layer that is covered by some form of dense interface. -static constexpr const std::initializer_list support_types_interface { +static constexpr const std::initializer_list support_types_interface { SupporLayerType::RaftInterface, SupporLayerType::BottomContact, SupporLayerType::BottomInterface, SupporLayerType::TopContact, SupporLayerType::TopInterface }; +// Forward declarations for belt floor helpers (defined later in this file). +// belt_floor_surface_polygon is declared in SupportCommon.hpp (non-static, +// shared with TreeSupport.cpp). + +static Polygons belt_floor_valid_region_polygon( + const SlicingParameters &slicing_params, const PrintConfig &print_config, + const PrintObject &object, coordf_t print_z); +static void trim_support_layers_by_belt_floor( + const SlicingParameters &slicing_params, const PrintConfig &print_config, + const PrintObject &object, SupportGeneratorLayersPtr &support_layers); + void PrintObjectSupportMaterial::generate(PrintObject &object) { BOOST_LOG_TRIVIAL(info) << "Support generator - Start"; @@ -443,11 +454,12 @@ void PrintObjectSupportMaterial::generate(PrintObject &object) object, bottom_contacts, top_contacts, layer_storage); this->trim_support_layers_by_object(object, top_contacts, m_slicing_params.gap_support_object, m_slicing_params.gap_object_support, m_support_params.gap_xy); + trim_support_layers_by_belt_floor(m_slicing_params, *m_print_config, object, top_contacts); #ifdef SLIC3R_DEBUG for (const SupportGeneratorLayer *layer : top_contacts) Slic3r::SVG::export_expolygons( - debug_out_path("support-top-contacts-trimmed-by-object-%d-%lf.svg", iRun, layer->print_z), + debug_out_path("support-top-contacts-trimmed-by-object-%d-%lf.svg", iRun, layer->print_z), union_ex(layer->polygons)); #endif @@ -603,6 +615,139 @@ Polygons collect_region_slices_by_type(const Layer &layer, SurfaceType surface_t return out; } +// Belt printer: compute the belt-side half-plane polygon at a given print_z. +// This represents the region where the belt surface exists (the "phantom top surface"). +// Support that overlaps with this polygon should terminate with a bottom contact. +// Returns empty if belt floor is not active. +Polygons belt_floor_surface_polygon( + const SlicingParameters &slicing_params, + const PrintConfig &print_config, + const PrintObject &object, + coordf_t print_z) +{ + const double shear_factor = slicing_params.belt_floor_shear_factor; + if (std::abs(shear_factor) < EPSILON) + return {}; + + const int from_axis = slicing_params.belt_floor_from_axis; // 0=X, 1=Y + const double floor_offset = print_config.belt_support_floor_offset.value; + + // Belt floor line in slicing coordinates: Z = sf * Y + z_shift. + // z_shift accounts for the upward shift applied when post-shear geometry + // extends below the bed (overhangs). Solving for Y: + // cutoff = (print_z - z_shift - floor_offset) / sf + const double z_shift = slicing_params.belt_floor_z_shift; + const double cutoff = (print_z - z_shift - floor_offset) / shear_factor; + const coord_t cutoff_scaled = scale_(cutoff); + const coord_t large_bound = scale_(1e4); + + // Build the belt-side half-plane (inverted from the valid region). + // If shear_factor > 0: valid region is from_axis < cutoff, so belt surface is from_axis >= cutoff. + // If shear_factor < 0: valid region is from_axis > cutoff, so belt surface is from_axis <= cutoff. + Polygon belt_poly; + if (from_axis == 0) { + if (shear_factor > 0) { + // Belt surface: X >= cutoff + belt_poly.points = { + Point(cutoff_scaled, -large_bound), + Point(large_bound, -large_bound), + Point(large_bound, large_bound), + Point(cutoff_scaled, large_bound) + }; + } else { + // Belt surface: X <= cutoff + belt_poly.points = { + Point(-large_bound, -large_bound), + Point(cutoff_scaled, -large_bound), + Point(cutoff_scaled, large_bound), + Point(-large_bound, large_bound) + }; + } + } else { + if (shear_factor > 0) { + // Belt surface: Y >= cutoff + belt_poly.points = { + Point(-large_bound, cutoff_scaled), + Point( large_bound, cutoff_scaled), + Point( large_bound, large_bound), + Point(-large_bound, large_bound) + }; + } else { + // Belt surface: Y <= cutoff + belt_poly.points = { + Point(-large_bound, -large_bound), + Point( large_bound, -large_bound), + Point( large_bound, cutoff_scaled), + Point(-large_bound, cutoff_scaled) + }; + } + } + return { belt_poly }; +} + +// Belt printer: compute the valid-region half-plane polygon at a given print_z. +// This is the region where support is allowed to exist (above the belt). +// Used to clip the downward-propagating support projection. +static Polygons belt_floor_valid_region_polygon( + const SlicingParameters &slicing_params, + const PrintConfig &print_config, + const PrintObject &object, + coordf_t print_z) +{ + const double shear_factor = slicing_params.belt_floor_shear_factor; + if (std::abs(shear_factor) < EPSILON) + return {}; + + const int from_axis = slicing_params.belt_floor_from_axis; + const double floor_offset = print_config.belt_support_floor_offset.value; + + const double z_shift = slicing_params.belt_floor_z_shift; + const double cutoff = (print_z - z_shift - floor_offset) / shear_factor; + const coord_t cutoff_scaled = scale_(cutoff); + const coord_t large_bound = scale_(1e4); + + // Valid region: the complement of the belt surface polygon. + Polygon valid_poly; + if (from_axis == 0) { + if (shear_factor > 0) { + // Valid: X < cutoff + valid_poly.points = { + Point(-large_bound, -large_bound), + Point(cutoff_scaled, -large_bound), + Point(cutoff_scaled, large_bound), + Point(-large_bound, large_bound) + }; + } else { + // Valid: X > cutoff + valid_poly.points = { + Point(cutoff_scaled, -large_bound), + Point(large_bound, -large_bound), + Point(large_bound, large_bound), + Point(cutoff_scaled, large_bound) + }; + } + } else { + if (shear_factor > 0) { + // Valid: Y < cutoff + valid_poly.points = { + Point(-large_bound, -large_bound), + Point( large_bound, -large_bound), + Point( large_bound, cutoff_scaled), + Point(-large_bound, cutoff_scaled) + }; + } else { + // Valid: Y > cutoff + valid_poly.points = { + Point(-large_bound, cutoff_scaled), + Point( large_bound, cutoff_scaled), + Point( large_bound, large_bound), + Point(-large_bound, large_bound) + }; + } + } + return { valid_poly }; +} + // Collect outer contours of all slices of this layer. // This is useful for calculating the support base with holes filled. Polygons collect_slices_outer(const Layer &layer) @@ -2516,6 +2661,82 @@ static inline SupportGeneratorLayer* detect_bottom_contacts( return &layer_new; } +// Belt printer: detect bottom contacts where support meets the belt floor plane. +// Modeled on detect_bottom_contacts() but uses the belt plane polygon instead of stTop surfaces. +static inline SupportGeneratorLayer* detect_belt_floor_bottom_contacts( + const SlicingParameters &slicing_params, + const SupportParameters &support_params, + const PrintConfig &print_config, + const PrintObject &object, + const Layer &layer, + // Existing top contact layers, for snapping. + const SupportGeneratorLayersPtr &top_contacts, + size_t contact_idx, + SupportGeneratorLayerStorage &layer_storage, + std::vector &layer_support_areas, + const Polygons &supports_projected) +{ + // Compute the belt surface polygon at this layer's Z. + Polygons belt_surface = belt_floor_surface_polygon(slicing_params, print_config, object, layer.print_z); + if (belt_surface.empty()) + return nullptr; + + // Find where projected support overlaps the belt surface. + Polygons touching = intersection(belt_surface, supports_projected); + if (touching.empty()) + return nullptr; + + assert(layer.id() >= slicing_params.raft_layers()); + size_t layer_id = layer.id() - slicing_params.raft_layers(); + + // Allocate a new bottom contact layer resting on the belt plane. + SupportGeneratorLayer &layer_new = layer_storage.allocate_unguarded(SupporLayerType::BottomContact); + + // No object layer to sync with -- compute heights directly from flow parameters. + layer_new.height = support_params.support_material_bottom_interface_flow.height(); + layer_new.print_z = layer.print_z + layer_new.height + slicing_params.gap_object_support; + layer_new.bottom_z = layer.print_z; + layer_new.idx_object_layer_below = layer_id; + layer_new.bridging = ! slicing_params.soluble_interface && object.config().thick_bridges; + layer_new.polygons = expand(touching, float(support_params.support_material_flow.scaled_width()), SUPPORT_SURFACES_OFFSET_PARAMETERS); + + if (! slicing_params.soluble_interface) { + // Snap to nearby top contact layers to avoid very thin support layers. + for (size_t top_idx = size_t(std::max(0, int(contact_idx))); + top_idx < top_contacts.size() && top_contacts[top_idx]->print_z < layer_new.print_z + support_params.support_layer_height_min + EPSILON; + ++ top_idx) { + if (top_contacts[top_idx]->print_z > layer_new.print_z - support_params.support_layer_height_min - EPSILON) { + coordf_t diff = layer_new.print_z - top_contacts[top_idx]->print_z; + assert(std::abs(diff) <= support_params.support_layer_height_min + EPSILON); + if (diff > 0.F) { + if (layer_new.height - diff > support_params.support_layer_height_min) { + layer_new.print_z = top_contacts[top_idx]->print_z; + layer_new.height -= diff; + } else { + continue; + } + } else { + layer_new.print_z = top_contacts[top_idx]->print_z; + layer_new.height -= diff; + } + break; + } + } + } + + // Trim the already created base layers above this belt contact. + touching = expand(touching, float(SCALED_EPSILON)); + for (int layer_id_above = int(layer_id) + 1; layer_id_above < int(object.total_layer_count()); ++ layer_id_above) { + const Layer &layer_above = *object.layers()[layer_id_above]; + if (layer_above.print_z > layer_new.print_z - EPSILON) + break; + if (Polygons &above = layer_support_areas[layer_id_above]; ! above.empty()) + above = diff(above, touching); + } + + return &layer_new; +} + // Returns polygons to print + polygons to propagate downwards. // Called twice: First for normal supports, possibly trimmed by "on build plate only", second for support enforcers not trimmed by "on build plate only". static inline std::pair project_support_to_grid(const Layer &layer, const SupportGridParams &grid_params, const Polygons &overhangs, Polygons *layer_buildplate_covered @@ -2615,6 +2836,8 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::bottom_contact_layers_and_ //const auto expansion_to_slice = m_support_material_flow.scaled_spacing() / 2 + 25; const SupportGridParams grid_params(*m_object_config, m_support_params.support_material_flow); const bool buildplate_only = ! buildplate_covered.empty(); + const bool has_belt_floor = std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON + && m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly; // Allocate empty surface areas, one per object layer. layer_support_areas.assign(object.total_layer_count(), Polygons()); @@ -2675,8 +2898,9 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::bottom_contact_layers_and_ tbb::task_group task_group; const Polygons &overhangs_for_bottom_contacts = buildplate_only ? enforcers_projection_raw : overhangs_projection_raw; if (! overhangs_for_bottom_contacts.empty()) - // Find the bottom contact layers above the top surfaces of this layer. - task_group.run([this, &object, &layer, &top_contacts, contact_idx, &layer_storage, &layer_support_areas, &bottom_contacts, &overhangs_for_bottom_contacts + // Find the bottom contact layers above the top surfaces of this layer, + // and also detect belt floor contacts if belt mode is active. + task_group.run([this, &object, &layer, &top_contacts, contact_idx, &layer_storage, &layer_support_areas, &bottom_contacts, &overhangs_for_bottom_contacts, has_belt_floor #ifdef SLIC3R_DEBUG , iRun, &polygons_new #endif // SLIC3R_DEBUG @@ -2690,6 +2914,15 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::bottom_contact_layers_and_ ); if (layer_new) bottom_contacts.push_back(layer_new); + // Belt floor phantom surface: detect where support meets the belt plane. + if (has_belt_floor) { + SupportGeneratorLayer *belt_layer = detect_belt_floor_bottom_contacts( + m_slicing_params, m_support_params, *m_print_config, object, + layer, top_contacts, contact_idx, layer_storage, + layer_support_areas, overhangs_for_bottom_contacts); + if (belt_layer) + bottom_contacts.push_back(belt_layer); + } }); Polygons &layer_support_area = layer_support_areas[layer_id]; @@ -2728,6 +2961,23 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::bottom_contact_layers_and_ task_group.wait(); + // Belt floor: clip projections and support areas so support doesn't + // propagate below the belt plane. + if (has_belt_floor) { + Polygons valid_region = belt_floor_valid_region_polygon( + m_slicing_params, *m_print_config, object, layer.print_z); + if (! valid_region.empty()) { + if (! overhangs_projection.empty()) + overhangs_projection = intersection(overhangs_projection, valid_region); + if (! enforcers_projection.empty()) + enforcers_projection = intersection(enforcers_projection, valid_region); + if (! layer_support_area.empty()) + layer_support_area = intersection(layer_support_area, valid_region); + if (! layer_support_area_enforcers.empty()) + layer_support_area_enforcers = intersection(layer_support_area_enforcers, valid_region); + } + } + if (! layer_support_area_enforcers.empty()) { if (layer_support_area.empty()) layer_support_area = std::move(layer_support_area_enforcers); @@ -2738,6 +2988,7 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::bottom_contact_layers_and_ std::reverse(bottom_contacts.begin(), bottom_contacts.end()); trim_support_layers_by_object(object, bottom_contacts, m_slicing_params.gap_support_object, m_slicing_params.gap_object_support, m_support_params.gap_xy); + trim_support_layers_by_belt_floor(m_slicing_params, *m_print_config, object, bottom_contacts); return bottom_contacts; } @@ -3108,6 +3359,37 @@ void PrintObjectSupportMaterial::generate_base_layers( #endif /* SLIC3R_DEBUG */ this->trim_support_layers_by_object(object, intermediate_layers, m_slicing_params.gap_support_object, m_slicing_params.gap_object_support, m_support_params.gap_xy); + trim_support_layers_by_belt_floor(m_slicing_params, *m_print_config, object, intermediate_layers); +} + +// Belt printer: trim support layer polygons by the belt floor plane. +// For each support layer, computes the belt floor half-plane at that layer's print_z +// and subtracts it from the support polygons. This follows the same diff() pattern +// as trim_support_layers_by_object() so that interface layers derived from trimmed +// intermediates automatically inherit the belt floor trimming. +static void trim_support_layers_by_belt_floor( + const SlicingParameters &slicing_params, + const PrintConfig &print_config, + const PrintObject &object, + SupportGeneratorLayersPtr &support_layers) +{ + if (std::abs(slicing_params.belt_floor_shear_factor) < EPSILON) + return; + if (print_config.belt_support_floor_mode.value != BeltSupportFloorMode::GeneratorOnly) + return; + + tbb::parallel_for(tbb::blocked_range(0, support_layers.size()), + [&](const tbb::blocked_range &range) { + for (size_t i = range.begin(); i < range.end(); ++ i) { + SupportGeneratorLayer *layer = support_layers[i]; + if (layer->polygons.empty()) + continue; + Polygons belt_surface = belt_floor_surface_polygon( + slicing_params, print_config, object, layer->print_z); + if (! belt_surface.empty()) + layer->polygons = diff(layer->polygons, belt_surface); + } + }); } void PrintObjectSupportMaterial::trim_support_layers_by_object( diff --git a/src/libslic3r/Support/TreeModelVolumes.cpp b/src/libslic3r/Support/TreeModelVolumes.cpp index 0267c0c327..e270b62aab 100644 --- a/src/libslic3r/Support/TreeModelVolumes.cpp +++ b/src/libslic3r/Support/TreeModelVolumes.cpp @@ -94,6 +94,46 @@ TreeModelVolumes::TreeModelVolumes( #else { m_anti_overhang = print_object.slice_support_blockers(); + // Belt floor: add belt surface polygons to anti_overhang so support + // is never generated inside the belt. Only in global shear mode — + // in local mode the belt floor clipping handles everything and + // anti_overhang at the bottom layers would block all support. + { + const auto &sp = print_object.slicing_parameters(); + const auto &pcfg = print_object.print()->config(); + const double sf = sp.belt_floor_shear_factor; + if (std::abs(sf) > EPSILON + && std::abs(print_object.belt_global_z_offset()) > EPSILON + && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + const int from_axis = sp.belt_floor_from_axis; + const double floor_off = pcfg.belt_support_floor_offset.value; + const double z_shift = sp.belt_floor_z_shift - print_object.belt_global_z_offset(); + size_t num_layers_needed = print_object.layer_count(); + // Ensure m_anti_overhang is large enough. + if (m_anti_overhang.size() < num_layers_needed) + m_anti_overhang.resize(num_layers_needed, Polygons{}); + for (size_t layer_idx = 0; layer_idx < num_layers_needed; ++layer_idx) { + double print_z = print_object.get_layer(layer_idx)->print_z + - print_object.belt_global_z_offset(); + double cutoff = (print_z - z_shift - floor_off) / sf; + coord_t cutoff_sc = scale_(cutoff); + coord_t big = scale_(1e4); + Polygon belt_poly; + if (from_axis == 0) { + if (sf > 0) + belt_poly.points = {{cutoff_sc,-big},{big,-big},{big,big},{cutoff_sc,big}}; + else + belt_poly.points = {{-big,-big},{cutoff_sc,-big},{cutoff_sc,big},{-big,big}}; + } else { + if (sf > 0) + belt_poly.points = {{-big,cutoff_sc},{big,cutoff_sc},{big,big},{-big,big}}; + else + belt_poly.points = {{-big,-big},{big,-big},{big,cutoff_sc},{-big,cutoff_sc}}; + } + append(m_anti_overhang[layer_idx], Polygons{belt_poly}); + } + } + } TreeSupportMeshGroupSettings mesh_settings(print_object); const TreeSupportSettings config{ mesh_settings, print_object.slicing_parameters() }; m_current_min_xy_dist = config.xy_min_distance; @@ -102,6 +142,27 @@ TreeModelVolumes::TreeModelVolumes( m_increase_until_radius = config.increase_radius_until_radius; m_radius_0 = config.getRadius(0); m_raft_layers = config.raft_layers; + // Belt printer: add virtual belt raft layers below the object, matching + // the extra layers added in generate_support_areas() so both use the + // same layer indexing. + { + const auto &sp2 = print_object.slicing_parameters(); + const auto &pcfg2 = print_object.print()->config(); + double belt_sf = sp2.belt_floor_shear_factor; + if (std::abs(belt_sf) > EPSILON && std::abs(print_object.belt_global_z_offset()) > EPSILON + && pcfg2.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + double bb_min_z = std::abs(print_object.model_object()->raw_bounding_box().min.z()); + double extra_depth = bb_min_z + 10.; + int num_extra = std::max(0, (int)std::ceil(extra_depth / sp2.layer_height)); + if (num_extra > 0) { + std::vector belt_layers; + belt_layers.reserve(num_extra); + for (int i = num_extra; i >= 1; --i) + belt_layers.push_back(sp2.first_object_layer_height - i * sp2.layer_height); + m_raft_layers.insert(m_raft_layers.begin(), belt_layers.begin(), belt_layers.end()); + } + } + } m_current_outline_idx = 0; m_layer_outlines.emplace_back(mesh_settings, std::vector{}); @@ -114,6 +175,47 @@ TreeModelVolumes::TreeModelVolumes( for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) outlines[layer_idx] = polygons_simplify(to_polygons(print_object.get_layer(layer_idx - num_raft_layers)->lslices), mesh_settings.resolution, polygons_strictly_simple); }); + + // Belt floor: pre-compute belt surface polygon per-layer for clipping. + // Branches grow toward the belt and their slices are clipped at the belt + // surface in organic_draw_branches(). The organic pipeline works in LOCAL + // Z (no global_z_offset), so use local z_shift and local print_z. + const auto &slicing_params = print_object.slicing_parameters(); + const auto &pcfg = print_object.print()->config(); + const double sf = slicing_params.belt_floor_shear_factor; + if (std::abs(sf) > EPSILON + && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + const int from_axis = slicing_params.belt_floor_from_axis; + const double floor_off = pcfg.belt_support_floor_offset.value; + // Subtract global_z_offset to get the LOCAL z_shift — the organic + // pipeline's Z coordinates don't include the global offset. + const double z_shift = slicing_params.belt_floor_z_shift + - print_object.belt_global_z_offset(); + m_belt_floor.assign(num_layers, Polygons{}); + for (size_t layer_idx = 0; layer_idx < num_layers; ++layer_idx) { + // Use local print_z (subtract global offset from object layer). + double print_z = (layer_idx >= num_raft_layers) + ? print_object.get_layer(layer_idx - num_raft_layers)->print_z + - print_object.belt_global_z_offset() + : 0.; + double cutoff = (print_z - z_shift - floor_off) / sf; + coord_t cutoff_sc = scale_(cutoff); + coord_t big = scale_(1e4); + Polygon belt_poly; + if (from_axis == 0) { + if (sf > 0) + belt_poly.points = {{cutoff_sc,-big},{big,-big},{big,big},{cutoff_sc,big}}; + else + belt_poly.points = {{-big,-big},{cutoff_sc,-big},{cutoff_sc,big},{-big,big}}; + } else { + if (sf > 0) + belt_poly.points = {{-big,cutoff_sc},{big,cutoff_sc},{big,big},{-big,big}}; + else + belt_poly.points = {{-big,-big},{big,-big},{big,cutoff_sc},{-big,cutoff_sc}}; + } + m_belt_floor[layer_idx] = { belt_poly }; + } + } } #endif @@ -469,9 +571,9 @@ void TreeModelVolumes::calculateCollision(const coord_t radius, const LayerIndex }); // 2) Sum over top / bottom ranges. - const bool processing_last_mesh = outline_idx == layer_outline_indices.size(); + const bool processing_last_mesh = outline_idx == layer_outline_indices.back(); tbb::parallel_for(tbb::blocked_range(data.begin(), data.end()), - [&collision_areas_offsetted, &outlines, &machine_border = m_machine_border, &anti_overhang = m_anti_overhang, radius, + [&collision_areas_offsetted, &outlines, &machine_border = m_machine_border, &anti_overhang = m_anti_overhang, radius, xy_distance, z_distance_bottom_layers, z_distance_top_layers, min_resolution = m_min_resolution, &data, processing_last_mesh, &throw_on_cancel] (const tbb::blocked_range& range) { for (LayerIndex layer_idx = range.begin(); layer_idx != range.end(); ++ layer_idx) { @@ -517,9 +619,14 @@ void TreeModelVolumes::calculateCollision(const coord_t radius, const LayerIndex // not support an overhang<90 degree than to risk fusing to it. append(collisions, offset(union_ex(collision_areas_original), radius + required_range_x, ClipperLib::jtMiter, 1.2)); } - collisions = processing_last_mesh && layer_idx < int(anti_overhang.size()) ? - union_(collisions, offset(union_ex(anti_overhang[layer_idx]), radius, ClipperLib::jtMiter, 1.2)) : - union_(collisions); + if (processing_last_mesh) { + if (layer_idx < int(anti_overhang.size())) + append(collisions, offset(union_ex(anti_overhang[layer_idx]), radius, ClipperLib::jtMiter, 1.2)); + // NOTE: m_belt_floor is NOT added to collision here — branches + // should grow toward the belt and terminate at it, not avoid it. + // Belt floor clipping is done post-generation in organic_draw_branches(). + } + collisions = union_(collisions); auto &dst = data[layer_idx]; if (processing_last_mesh) { if (! dst.empty()) diff --git a/src/libslic3r/Support/TreeModelVolumes.hpp b/src/libslic3r/Support/TreeModelVolumes.hpp index eff4cadcaf..0ded395091 100644 --- a/src/libslic3r/Support/TreeModelVolumes.hpp +++ b/src/libslic3r/Support/TreeModelVolumes.hpp @@ -168,6 +168,9 @@ public: } Polygon m_bed_area; + // Belt floor polygons per layer — used for post-generation clipping + // in organic_draw_branches(). Public so the organic pipeline can access it. + std::vector m_belt_floor; private: // Caching polygons for a range of layers. diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 864a0eb0e0..4966a5a139 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -637,6 +637,20 @@ TreeSupport::TreeSupport(PrintObject& object, const SlicingParameters &slicing_p } +double TreeSupport::belt_floor_print_z(const Point &pos_slicing) const +{ + double sf = m_slicing_params.belt_floor_shear_factor; + if (std::abs(sf) < EPSILON) + return -std::numeric_limits::max(); // no belt floor + int from = m_slicing_params.belt_floor_from_axis; + // Belt floor in slicing coords: Z = sf * Y + z_shift + floor_offset. + // Inverse of cutoff = (Z - z_shift - floor_offset) / sf. + double pos = unscale(from == 0 ? pos_slicing.x() : pos_slicing.y()); + double floor_offset = m_print_config->belt_support_floor_offset.value; + double z_shift = m_slicing_params.belt_floor_z_shift; + return sf * pos + floor_offset + z_shift; +} + #define SUPPORT_SURFACES_OFFSET_PARAMETERS ClipperLib::jtSquare, 0. void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) { @@ -2141,6 +2155,41 @@ void TreeSupport::draw_circles() base_areas = diff_ex(base_areas, ClipperUtils::clip_clipper_polygons_with_subject_bbox(roofs, get_extents(base_areas))); base_areas = intersection_ex(base_areas, m_machine_border); + // Belt floor: clip tree support polygons by the belt surface plane. + // ts_layer->print_z is at LOCAL Z (global offset applied later in + // _generate_support_material), but belt_floor_z_shift includes + // global_z_offset — subtract it to get the cutoff in local coords. + if (std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON + && m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + const double sf = m_slicing_params.belt_floor_shear_factor; + const int from_axis = m_slicing_params.belt_floor_from_axis; + const double floor_off = m_print_config->belt_support_floor_offset.value; + const double z_shift_local = m_slicing_params.belt_floor_z_shift + - m_object->belt_global_z_offset(); + const double cutoff = (ts_layer->print_z - z_shift_local - floor_off) / sf; + const coord_t cutoff_sc = scale_(cutoff); + const coord_t big = scale_(1e4); + + Polygon belt_poly; + if (from_axis == 0) { + if (sf > 0) + belt_poly.points = { {cutoff_sc,-big}, {big,-big}, {big,big}, {cutoff_sc,big} }; + else + belt_poly.points = { {-big,-big}, {cutoff_sc,-big}, {cutoff_sc,big}, {-big,big} }; + } else { + if (sf > 0) + belt_poly.points = { {-big,cutoff_sc}, {big,cutoff_sc}, {big,big}, {-big,big} }; + else + belt_poly.points = { {-big,-big}, {big,-big}, {big,cutoff_sc}, {-big,cutoff_sc} }; + } + Polygons belt_surface = { belt_poly }; + base_areas = diff_ex(base_areas, belt_surface); + roof_areas = diff_ex(roof_areas, belt_surface); + roof_1st_layer = diff_ex(roof_1st_layer, belt_surface); + floor_areas = diff_ex(floor_areas, belt_surface); + roof_gap_areas = diff_ex(roof_gap_areas, belt_surface); + } + if (SQUARE_SUPPORT) { // simplify support contours ExPolygons base_areas_simplified; @@ -2445,6 +2494,9 @@ void TreeSupport::drop_nodes() const coordf_t radius_sample_resolution = m_ts_data->m_radius_sample_resolution; const bool support_on_buildplate_only = config.support_on_build_plate_only.value; const size_t top_interface_layers = config.support_interface_top_layers.value; + const auto belt_floor_mode = m_print_config->belt_support_floor_mode.value; + const bool has_belt_floor = std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON + && belt_floor_mode == BeltSupportFloorMode::GeneratorOnly; const size_t bottom_interface_layers = config.support_interface_bottom_layers.value < 0 ? top_interface_layers : config.support_interface_bottom_layers.value; SupportNode::diameter_angle_scale_factor = diameter_angle_scale_factor; float DO_NOT_MOVER_UNDER_MM = is_slim ? 0 : 5; // do not move contact points under 5mm @@ -2677,15 +2729,22 @@ void TreeSupport::drop_nodes() node_parent = p_node->parent ? p_node : neighbour; // Make sure the next pass doesn't drop down either of these (since that already happened). node_parent->merged_neighbours.push_front(node_parent == p_node ? neighbour : p_node); - const bool to_buildplate = !is_inside_ex(get_collision(0, obj_layer_nr_next), next_position); - SupportNode* next_node = m_ts_data->create_node(next_position, node_parent->distance_to_top + 1, obj_layer_nr_next, node_parent->support_roof_layers_below - 1, to_buildplate, node_parent, - print_z_next, height_next); - get_max_move_dist(next_node); - m_ts_data->m_mutex.lock(); - contact_nodes[layer_nr_next].push_back(next_node); + // Belt floor: don't drop merged node below belt surface. + // Treat as object-surface termination (not buildplate) so + // the node gets floor/interface areas instead of base pads. + if (has_belt_floor && print_z_next <= belt_floor_print_z(next_position)) { + node_parent->to_buildplate = false; + } else { + const bool to_buildplate = !is_inside_ex(get_collision(0, obj_layer_nr_next), next_position); + SupportNode* next_node = m_ts_data->create_node(next_position, node_parent->distance_to_top + 1, obj_layer_nr_next, node_parent->support_roof_layers_below - 1, to_buildplate, node_parent, + print_z_next, height_next); + get_max_move_dist(next_node); + m_ts_data->m_mutex.lock(); + contact_nodes[layer_nr_next].push_back(next_node); + m_ts_data->m_mutex.unlock(); + } neighbour->valid = false; p_node->valid = false; - m_ts_data->m_mutex.unlock(); } else if (neighbours.size() > 1) //Don't merge leaf nodes because we would then incur movement greater than the maximum move distance. { @@ -2729,6 +2788,12 @@ void TreeSupport::drop_nodes() ExPolygons overhangs_next = diff_clipped({ node.overhang }, get_collision(0, obj_layer_nr_next)); for(auto& overhang:overhangs_next) { Point next_pt = overhang.contour.centroid(); + // Belt floor: don't drop polygon node below belt surface. + // Treat as object-surface termination (not buildplate). + if (has_belt_floor && print_z_next <= belt_floor_print_z(next_pt)) { + p_node->to_buildplate = false; + continue; + } SupportNode *next_node = m_ts_data->create_node(next_pt, p_node->distance_to_top + 1, obj_layer_nr_next, p_node->support_roof_layers_below - 1, to_buildplate, p_node, print_z_next, height_next); next_node->max_move_dist = 0; @@ -2873,6 +2938,12 @@ void TreeSupport::drop_nodes() if (is_outside) { next_layer_vertex = candidate_vertex; } } } + // Belt floor: don't drop regular node below belt surface. + // Treat as object-surface termination (not buildplate). + if (has_belt_floor && print_z_next <= belt_floor_print_z(next_layer_vertex)) { + p_node->to_buildplate = false; + return; // from parallel_for_each lambda + } auto next_collision = get_collision(0, obj_layer_nr_next); const bool to_buildplate = !is_inside_ex(m_ts_data->m_layer_outlines[obj_layer_nr_next], next_layer_vertex); SupportNode * next_node = m_ts_data->create_node(next_layer_vertex, node.distance_to_top + 1, obj_layer_nr_next, node.support_roof_layers_below - 1, to_buildplate, p_node, @@ -3167,6 +3238,9 @@ void TreeSupport::generate_contact_points() bool on_buildplate_only = m_object_config->support_on_build_plate_only.value; const bool roof_enabled = config.support_interface_top_layers.value > 0; const bool force_tip_to_roof = roof_enabled && m_support_params.soluble_interface; + const auto belt_floor_mode = m_print_config->belt_support_floor_mode.value; + const bool has_belt_floor = std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON + && belt_floor_mode == BeltSupportFloorMode::GeneratorOnly; //First generate grid points to cover the entire area of the print. BoundingBox bounding_box = m_object->bounding_box(); @@ -3257,6 +3331,10 @@ void TreeSupport::generate_contact_points() auto insert_point = [&](Point pt, const ExPolygon& overhang, double radius, bool force_add = false, bool add_interface=true) { + // Belt floor: skip contact points whose bottom_z is at or below + // the belt floor at this XY position (overhang rests on the belt). + if (has_belt_floor && bottom_z <= belt_floor_print_z(pt)) + return (SupportNode*) nullptr; Point hash_pos = pt / ((radius_scaled + 1) / 1); SupportNode* contact_node = nullptr; if (force_add || !already_inserted.count(hash_pos)) { @@ -3292,8 +3370,10 @@ void TreeSupport::generate_contact_points() double radius = unscale_(overhang_bounds.radius()); Point candidate = overhang_bounds.center(); SupportNode *contact_node = insert_point(candidate, overhang, radius, true, true); - contact_node->type = ePolygon; - curr_nodes.emplace_back(contact_node); + if (contact_node) { + contact_node->type = ePolygon; + curr_nodes.emplace_back(contact_node); + } } }else{ // otherwise, all nodes should be circle nodes diff --git a/src/libslic3r/Support/TreeSupport.hpp b/src/libslic3r/Support/TreeSupport.hpp index e0446ad5f1..c3bd186df1 100644 --- a/src/libslic3r/Support/TreeSupport.hpp +++ b/src/libslic3r/Support/TreeSupport.hpp @@ -446,6 +446,10 @@ private: bool is_slim = false; bool with_infill = false; + // Belt printer: compute the belt floor print_z at a given XY position (in slicing coords). + // Returns -infinity if belt floor is not active. + double belt_floor_print_z(const Point &pos_slicing) const; + /*! diff --git a/src/libslic3r/Support/TreeSupport3D.cpp b/src/libslic3r/Support/TreeSupport3D.cpp index 2c24b933b2..b551a42ece 100644 --- a/src/libslic3r/Support/TreeSupport3D.cpp +++ b/src/libslic3r/Support/TreeSupport3D.cpp @@ -3366,6 +3366,36 @@ static void generate_support_areas(Print &print, TreeSupport* tree_support, cons // this struct is used to easy retrieve setting. No other function except those in TreeModelVolumes and generate_initial_areas() have knowledge of the existence of multiple meshes being processed. //FIXME this is a copy // Contains config settings to avoid loading them in every function. This was done to improve readability of the code. + // Belt printer: add virtual "belt raft" layers below the object so + // organic branches can extend below the model's first layer and + // terminate at the belt surface instead of creating a flat base at Z=0. + { + PrintObject &po = *print.get_object(processing.second.front()); + const auto &sp = po.slicing_parameters(); + const auto &pcfg = po.print()->config(); + const double sf = sp.belt_floor_shear_factor; + if (std::abs(sf) > EPSILON && std::abs(po.belt_global_z_offset()) > EPSILON + && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + // z_shift_local is the belt surface height at Y=0 in local coords. + // Extend below the belt so the base expansion and build-plate + // termination happen inside the belt region and get clipped. + // Use the distance from the pre-shear bbox min Z to the part's + // post-shear min Z, plus 10mm for base expansion headroom. + double bb_min_z = std::abs(po.model_object()->raw_bounding_box().min.z()); + double extra_depth = bb_min_z + 10.; + int num_extra = std::max(0, (int)std::ceil(extra_depth / sp.layer_height)); + if (num_extra > 0) { + // Insert belt raft layers at the front, from lowest Z to highest. + std::vector belt_layers; + belt_layers.reserve(num_extra); + for (int i = num_extra; i >= 1; --i) + belt_layers.push_back(sp.first_object_layer_height - i * sp.layer_height); + // Prepend to existing raft_layers (if any). + auto &rl = processing.first.raft_layers; + rl.insert(rl.begin(), belt_layers.begin(), belt_layers.end()); + } + } + } const TreeSupportSettings &config = processing.first; BOOST_LOG_TRIVIAL(info) << "Processing support tree mesh group " << counter + 1 << " of " << grouped_meshes.size() << " containing " << grouped_meshes[counter].second.size() << " meshes."; auto t_start = std::chrono::high_resolution_clock::now(); @@ -3561,6 +3591,43 @@ static void generate_support_areas(Print &print, TreeSupport* tree_support, cons if (layer) layer->polygons = intersection(layer->polygons, volumes.m_bed_area); }); + // Belt floor: clip ALL organic support layers (including intermediate/base + // fill) against the belt surface. The branch slices were already clipped + // in organic_draw_branches(), but intermediate layers generated between + // branches and the build plate need clipping too. + // Compute the belt floor polygon directly from each layer's print_z + // rather than mapping to a layer index (avoids index mismatch issues). + { + const auto &sp = print_object.slicing_parameters(); + const double sf = sp.belt_floor_shear_factor; + const double z_shift = sp.belt_floor_z_shift - print_object.belt_global_z_offset(); + const double floor_off = print_object.print()->config().belt_support_floor_offset.value; + const int from_axis = sp.belt_floor_from_axis; + if (std::abs(sf) > EPSILON + && print_object.print()->config().belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + tbb::parallel_for_each(layers_sorted.begin(), layers_sorted.end(), [&](SupportGeneratorLayer *layer) { + if (!layer || layer->polygons.empty()) + return; + double cutoff = (layer->print_z - z_shift - floor_off) / sf; + coord_t cutoff_sc = scale_(cutoff); + coord_t big = scale_(1e4); + Polygon belt_poly; + if (from_axis == 0) { + if (sf > 0) + belt_poly.points = {{cutoff_sc,-big},{big,-big},{big,big},{cutoff_sc,big}}; + else + belt_poly.points = {{-big,-big},{cutoff_sc,-big},{cutoff_sc,big},{-big,big}}; + } else { + if (sf > 0) + belt_poly.points = {{-big,cutoff_sc},{big,cutoff_sc},{big,big},{-big,big}}; + else + belt_poly.points = {{-big,-big},{big,-big},{big,cutoff_sc},{-big,cutoff_sc}}; + } + layer->polygons = diff(layer->polygons, Polygons{belt_poly}); + }); + } + } + print.set_status(69, _L("Generating support")); generate_support_toolpaths(print_object.support_layers(), print_object.config(), support_params, print_object.slicing_parameters(), raft_layers, bottom_contacts, top_contacts, intermediate_layers, interface_layers, base_interface_layers); @@ -3816,6 +3883,10 @@ void organic_draw_branches( for (LayerIndex i = 0; i < LayerIndex(slices.size()); ++i) { slices[i] = diff_clipped(slices[i], volumes.getCollision(0, layer_begin + i, true)); // FIXME parent_uses_min || draw_area.element->state.use_min_xy_dist); slices[i] = intersection(slices[i], volumes.m_bed_area); + // Belt floor: clip branch slices against the belt surface plane. + LayerIndex belt_idx = layer_begin + i; + if (belt_idx < LayerIndex(volumes.m_belt_floor.size()) && !volumes.m_belt_floor[belt_idx].empty()) + slices[i] = diff(slices[i], volumes.m_belt_floor[belt_idx]); } size_t num_empty = 0; if (slices.front().empty()) { @@ -3850,6 +3921,9 @@ void organic_draw_branches( //double support_area_min = 0.1 * support_area_min_radius; for (LayerIndex layer_idx = layer_begin - 1; layer_idx >= layer_bottommost; -- layer_idx) { rest_support = diff_clipped(rest_support.empty() ? slices.front() : rest_support, volumes.getCollision(0, layer_idx, false)); + // Belt floor: clip propagated support at belt surface. + if (layer_idx < LayerIndex(volumes.m_belt_floor.size()) && !volumes.m_belt_floor[layer_idx].empty()) + rest_support = diff(rest_support, volumes.m_belt_floor[layer_idx]); double rest_support_area = area(rest_support); if (rest_support_area < support_area_stop) // Don't propagate a fraction of the tree contact surface. diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 25b4ba4e9c..b9fe17ff2a 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -2210,17 +2210,11 @@ void GCodeViewer::render_toolpaths() { const Camera& camera = wxGetApp().plater()->get_camera(); Matrix4f view = camera.get_view_matrix().matrix().cast(); - // Belt "designed" view: apply inverse shear to view matrix so toolpaths appear - // upright (as originally designed) instead of sheared on the belt. - if (m_belt_show_designed && m_belt_view_enabled && m_belt_angle_deg > 0.f) { - double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle_deg)); - double sin_a = std::sin(angle_rad); - if (sin_a > 1e-6) { - double cot_alpha = std::cos(angle_rad) / sin_a; - Transform3d inverse_shear = Transform3d::Identity(); - inverse_shear.matrix()(1, 2) = -cot_alpha; // Y -= Z * cot(α) - view = (camera.get_view_matrix() * inverse_shear).matrix().cast(); - } + // Belt "designed" view: apply the precomputed inverse of the full belt + // shear+scale transform so toolpaths appear upright (as originally designed) + // instead of transformed on the belt. + if (m_belt_show_designed && m_belt_view_enabled) { + view = (camera.get_view_matrix() * m_belt_inverse_transform).matrix().cast(); } const libvgcode::Mat4x4 converted_view_matrix = libvgcode::convert(view); const libvgcode::Mat4x4 converted_projetion_matrix = libvgcode::convert(static_cast(camera.get_projection_matrix().matrix().cast())); @@ -4418,12 +4412,18 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv if (m_nozzle_nums > 1 && (m_viewer.get_view_type() == libvgcode::EViewType::Summary || m_viewer.get_view_type() == libvgcode::EViewType::ColorPrint)) // ORCA show only on summary and filament tab render_legend_color_arr_recommen(window_padding); - // Belt printer: toggle for viewing raw slicing-frame G-code + // Belt printer: toggle for viewing designed (upright) vs. machine-frame G-code. + // Rendered with a separator and hint text so users can find it easily. if (m_belt_view_enabled) { + ImGui::Spacing(); + ImGui::Separator(); ImGui::Spacing(); ImGui::Dummy({ window_padding, 0 }); ImGui::SameLine(); - ImGui::Checkbox("Show designed view (upright)", &m_belt_show_designed); + ImGui::TextColored(ImVec4(0.f, 0.59f, 0.53f, 1.f), "%s", _u8L("Belt Printer").c_str()); + ImGui::Dummy({ window_padding, 0 }); + ImGui::SameLine(); + ImGui::Checkbox(_u8L("Show designed view (upright) [B]").c_str(), &m_belt_show_designed); } legend_height = ImGui::GetCurrentWindow()->Size.y; diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 184c56e915..cd97613b2a 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -240,6 +240,7 @@ mutable bool m_no_render_path { false }; bool m_belt_view_enabled = false; float m_belt_angle_deg = 0.f; bool m_belt_show_designed = false; // Toggle: show designed (upright) view via inverse shear + Transform3d m_belt_inverse_transform{Transform3d::Identity()}; libvgcode::Viewer m_viewer; bool m_loaded_as_preview{ false }; @@ -341,7 +342,10 @@ public: void export_toolpaths_to_obj(const char* filename) const; void set_belt_printer(bool enabled, float angle_deg) { m_belt_view_enabled = enabled; m_belt_angle_deg = angle_deg; } + void set_belt_inverse_transform(const Transform3d& t) { m_belt_inverse_transform = t; } bool is_belt_view() const { return m_belt_view_enabled && m_belt_angle_deg > 0.f; } + void toggle_belt_show_designed() { if (m_belt_view_enabled) m_belt_show_designed = !m_belt_show_designed; } + bool is_belt_show_designed() const { return m_belt_show_designed; } size_t get_extruders_count() { return m_extruders_count; } void push_combo_style(); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index a19bed05a8..3d964c7dab 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -3463,8 +3463,16 @@ void GLCanvas3D::on_char(wxKeyEvent& evt) post_event(SimpleEvent(EVT_GLCANVAS_ARRANGE)); break; } - //case 'B': - //case 'b': { zoom_to_bed(); break; } + case 'B': + case 'b': { + // Toggle belt printer "show designed" view when in G-code preview with belt mode active. + if (dynamic_cast(m_canvas->GetParent()) != nullptr && + m_gcode_viewer.is_belt_view()) { + m_gcode_viewer.toggle_belt_show_designed(); + m_dirty = true; + } + break; + } case 'C': case 'c': { wxGetApp().toggle_show_gcode_window(); m_dirty = true; request_extra_frame(); break; } //case 'G': diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index b20e26653a..ea7b40893f 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -2516,7 +2516,7 @@ void ObjectList::load_mesh_object(const TriangleMesh &mesh, const wxString &name Slic3r::save_object_mesh(*new_object); // BBS: find an empty cell to put the copied object - auto start_point = wxGetApp().plater()->build_volume().bounding_volume2d().center(); + auto start_point = wxGetApp().plater()->build_volume().bed_center(); auto empty_cell = wxGetApp().plater()->canvas3D()->get_nearest_empty_cell({start_point(0), start_point(1)}); new_object->instances[0]->set_offset(center ? to_3d(Vec2d(empty_cell(0), empty_cell(1)), -new_object->origin_translation.z()) : bb.center()); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 373ee2a3dc..ee235d4192 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -6907,7 +6907,7 @@ std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& mode // BBS: find an empty cell to put the copied object for (auto& instance : new_instances) { auto offset = instance->get_offset(); - auto start_point = this->bed.build_volume().bounding_volume2d().center(); + auto start_point = this->bed.build_volume().bed_center(); bool plate_empty = partplate_list.get_curr_plate()->empty(); Vec3d displacement; if (plate_empty) @@ -10991,10 +10991,83 @@ void Plater::priv::set_bed_shape(const Pointfs &shape, bed.set_belt_printer(true, static_cast(belt_angle)); if (preview) preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(true, static_cast(belt_angle)); + + // Compute the inverse of the full belt shear+scale transform for the G-code viewer. + auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad); + double cos_a = std::cos(angle_rad); + switch (mode) { + case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; + case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; + case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; + case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; + default: return 0.; + } + }; + auto compute_scale_factor = [](BeltScaleMode mode, double angle_deg) -> double { + if (mode == BeltScaleMode::None) return 1.; + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad); + double cos_a = std::cos(angle_rad); + switch (mode) { + case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; + case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; + case BeltScaleMode::Sin: return sin_a; + case BeltScaleMode::Cos: return cos_a; + default: return 1.; + } + }; + + // Read shear configs. + auto get_shear_mode = [this](const char *key) -> BeltShearMode { + auto opt = config->option>(key); + return opt ? opt->value : BeltShearMode::None; + }; + auto get_axis = [this](const char *key) -> BeltAxis { + auto opt = config->option>(key); + return opt ? opt->value : BeltAxis::X; + }; + auto get_scale_mode = [this](const char *key) -> BeltScaleMode { + auto opt = config->option>(key); + return opt ? opt->value : BeltScaleMode::None; + }; + + struct AxisShear { BeltShearMode mode; double angle; int from; }; + AxisShear axes[3] = { + { get_shear_mode("belt_shear_x"), config->opt_float("belt_shear_x_angle"), int(get_axis("belt_shear_x_from")) }, + { get_shear_mode("belt_shear_y"), config->opt_float("belt_shear_y_angle"), int(get_axis("belt_shear_y_from")) }, + { get_shear_mode("belt_shear_z"), config->opt_float("belt_shear_z_angle"), int(get_axis("belt_shear_z_from")) }, + }; + + Transform3d belt_shear = Transform3d::Identity(); + for (int row = 0; row < 3; ++row) { + if (axes[row].mode != BeltShearMode::None) { + double factor = compute_shear_factor(axes[row].mode, axes[row].angle); + if (std::abs(factor) > EPSILON) + belt_shear.matrix()(row, axes[row].from) += factor; + } + } + + double sx = compute_scale_factor(get_scale_mode("belt_scale_x"), config->opt_float("belt_scale_x_angle")); + double sy = compute_scale_factor(get_scale_mode("belt_scale_y"), config->opt_float("belt_scale_y_angle")); + double sz = compute_scale_factor(get_scale_mode("belt_scale_z"), config->opt_float("belt_scale_z_angle")); + Transform3d belt_scale = Transform3d::Identity(); + belt_scale.matrix()(0, 0) = sx; + belt_scale.matrix()(1, 1) = sy; + belt_scale.matrix()(2, 2) = sz; + + // Forward transform: scale * shear. Inverse for the viewer. + Transform3d forward = belt_scale * belt_shear; + Transform3d inverse = forward.inverse(); + if (preview) + preview->get_canvas3d()->get_gcode_viewer().set_belt_inverse_transform(inverse); } else { bed.set_belt_printer(false, 0.f); - if (preview) + if (preview) { preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(false, 0.f); + preview->get_canvas3d()->get_gcode_viewer().set_belt_inverse_transform(Transform3d::Identity()); + } } } @@ -13286,6 +13359,15 @@ void Plater::load_gcode(const wxString& filename) current_print.set_gcode_file_ready(); + // Belt printer: detect belt_printer_angle from loaded G-code header and enable + // belt view mode on the GCodeViewer so the "Show designed view" toggle appears. + if (current_result->belt_printer_angle > 0.f) { + float angle = current_result->belt_printer_angle; + p->preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(true, angle); + } else { + p->preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(false, 0.f); + } + // show results p->preview->reload_print(m_only_gcode); //BBS: zoom to bed 0 for gcode preview diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 87e97c5ff7..e6de38df78 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4364,58 +4364,6 @@ void TabPrinter::build_fff() optgroup->append_single_option_line(option, "printer_basic_information_printable_space#excluded-bed-area"); // optgroup->append_single_option_line("printable_area"); optgroup->append_single_option_line("printable_height", "printer_basic_information_printable_space#printable-height"); - optgroup->append_single_option_line("build_plate_tilt_x"); - optgroup->append_single_option_line("build_plate_tilt_y"); - optgroup->append_single_option_line("belt_printer"); - optgroup->append_single_option_line("belt_printer_angle"); - optgroup->append_single_option_line("belt_printer_infinite_y"); - // Per-axis shear: group mode + angle + source on one row per axis - { - Line line = { L("Shear X"), L("Shear applied to the X axis before slicing") }; - line.append_option(optgroup->get_option("belt_shear_x")); - line.append_option(optgroup->get_option("belt_shear_x_angle")); - line.append_option(optgroup->get_option("belt_shear_x_from")); - optgroup->append_line(line); - } - { - Line line = { L("Shear Y"), L("Shear applied to the Y axis before slicing") }; - line.append_option(optgroup->get_option("belt_shear_y")); - line.append_option(optgroup->get_option("belt_shear_y_angle")); - line.append_option(optgroup->get_option("belt_shear_y_from")); - optgroup->append_line(line); - } - { - Line line = { L("Shear Z"), L("Shear applied to the Z axis before slicing") }; - line.append_option(optgroup->get_option("belt_shear_z")); - line.append_option(optgroup->get_option("belt_shear_z_angle")); - line.append_option(optgroup->get_option("belt_shear_z_from")); - optgroup->append_line(line); - } - { - Line line = { L("Scale X"), L("Scale applied to the X axis before slicing") }; - line.append_option(optgroup->get_option("belt_scale_x")); - line.append_option(optgroup->get_option("belt_scale_x_angle")); - optgroup->append_line(line); - } - { - Line line = { L("Scale Y"), L("Scale applied to the Y axis before slicing") }; - line.append_option(optgroup->get_option("belt_scale_y")); - line.append_option(optgroup->get_option("belt_scale_y_angle")); - optgroup->append_line(line); - } - { - Line line = { L("Scale Z"), L("Scale applied to the Z axis before slicing") }; - line.append_option(optgroup->get_option("belt_scale_z")); - line.append_option(optgroup->get_option("belt_scale_z_angle")); - optgroup->append_line(line); - } - { - Line line = { L("G-code axis remap"), L("Remap slicing-frame axes to machine axes in G-code output") }; - line.append_option(optgroup->get_option("belt_gcode_remap_x")); - line.append_option(optgroup->get_option("belt_gcode_remap_y")); - line.append_option(optgroup->get_option("belt_gcode_remap_z")); - optgroup->append_line(line); - } optgroup->append_single_option_line("support_multi_bed_types","printer_basic_information_printable_space#support-multi-bed-types"); optgroup->append_single_option_line("best_object_pos", "printer_basic_information_printable_space#best-object-position"); // todo: for multi_extruder test @@ -4434,6 +4382,68 @@ void TabPrinter::build_fff() //option.opt.full_width = true; //optgroup->append_single_option_line(option); optgroup->append_single_option_line("disable_m73", "printer_basic_information_advanced#disable-set-remaining-print-time"); + optgroup->append_single_option_line("build_plate_tilt_x"); + optgroup->append_single_option_line("build_plate_tilt_y"); + optgroup->append_single_option_line("belt_printer"); + optgroup->append_single_option_line("belt_printer_angle"); + optgroup->append_single_option_line("belt_printer_infinite_y"); + // Per-axis shear: group mode + angle + source on one row per axis + { + Line line = { L("Mesh shear X"), L("Shear applied to the X axis before slicing") }; + line.append_option(optgroup->get_option("belt_shear_x")); + line.append_option(optgroup->get_option("belt_shear_x_angle")); + line.append_option(optgroup->get_option("belt_shear_x_from")); + line.append_option(optgroup->get_option("belt_shear_x_global")); + optgroup->append_line(line); + } + { + Line line = { L("Mesh shear Y"), L("Shear applied to the Y axis before slicing") }; + line.append_option(optgroup->get_option("belt_shear_y")); + line.append_option(optgroup->get_option("belt_shear_y_angle")); + line.append_option(optgroup->get_option("belt_shear_y_from")); + line.append_option(optgroup->get_option("belt_shear_y_global")); + optgroup->append_line(line); + } + { + Line line = { L("Mesh shear Z"), L("Shear applied to the Z axis before slicing") }; + line.append_option(optgroup->get_option("belt_shear_z")); + line.append_option(optgroup->get_option("belt_shear_z_angle")); + line.append_option(optgroup->get_option("belt_shear_z_from")); + line.append_option(optgroup->get_option("belt_shear_z_global")); + optgroup->append_line(line); + } + { + Line line = { L("Mesh scale X"), L("Scale applied to the X axis before slicing") }; + line.append_option(optgroup->get_option("belt_scale_x")); + line.append_option(optgroup->get_option("belt_scale_x_angle")); + optgroup->append_line(line); + } + { + Line line = { L("Mesh scale Y"), L("Scale applied to the Y axis before slicing") }; + line.append_option(optgroup->get_option("belt_scale_y")); + line.append_option(optgroup->get_option("belt_scale_y_angle")); + optgroup->append_line(line); + } + { + Line line = { L("Mesh scale Z"), L("Scale applied to the Z axis before slicing") }; + line.append_option(optgroup->get_option("belt_scale_z")); + line.append_option(optgroup->get_option("belt_scale_z_angle")); + optgroup->append_line(line); + } + { + Line line = { L("G-code axis remap"), L("Remap slicing-frame axes to machine axes in G-code output") }; + line.append_option(optgroup->get_option("belt_gcode_remap_x")); + line.append_option(optgroup->get_option("belt_gcode_remap_y")); + line.append_option(optgroup->get_option("belt_gcode_remap_z")); + optgroup->append_line(line); + } + { + Line line = { L("Support floor"), L("Belt floor awareness for support generation and clipping") }; + line.append_option(optgroup->get_option("belt_support_floor_mode")); + line.append_option(optgroup->get_option("belt_support_floor_offset")); + line.append_option(optgroup->get_option("belt_support_z_offset_mode")); + optgroup->append_line(line); + } option = optgroup->get_option("thumbnails"); option.opt.full_width = true; optgroup->append_single_option_line(option, "printer_basic_information_advanced#g-code-thumbnails"); @@ -5283,6 +5293,33 @@ void TabPrinter::toggle_options() "belt_scale_x", "belt_scale_y", "belt_scale_z", "belt_gcode_remap_x"}) toggle_line(el, is_belt); + + // Gray out angle/from sub-options when their parent shear/scale mode is None. + auto sx = m_config->option>("belt_shear_x")->value; + toggle_option("belt_shear_x_angle", is_belt && sx != BeltShearMode::None); + toggle_option("belt_shear_x_from", is_belt && sx != BeltShearMode::None); + toggle_option("belt_shear_x_global", is_belt && sx != BeltShearMode::None); + + auto sy = m_config->option>("belt_shear_y")->value; + toggle_option("belt_shear_y_angle", is_belt && sy != BeltShearMode::None); + toggle_option("belt_shear_y_from", is_belt && sy != BeltShearMode::None); + toggle_option("belt_shear_y_global", is_belt && sy != BeltShearMode::None); + + auto sz = m_config->option>("belt_shear_z")->value; + toggle_option("belt_shear_z_angle", is_belt && sz != BeltShearMode::None); + toggle_option("belt_shear_z_from", is_belt && sz != BeltShearMode::None); + toggle_option("belt_shear_z_global", is_belt && sz != BeltShearMode::None); + + auto scx = m_config->option>("belt_scale_x")->value; + toggle_option("belt_scale_x_angle", is_belt && scx != BeltScaleMode::None); + + auto scy = m_config->option>("belt_scale_y")->value; + toggle_option("belt_scale_y_angle", is_belt && scy != BeltScaleMode::None); + + auto scz = m_config->option>("belt_scale_z")->value; + toggle_option("belt_scale_z_angle", is_belt && scz != BeltScaleMode::None); + + toggle_line("belt_support_floor_mode", is_belt); } From b297f68921a74b95b108a9afb5d4f08fed3256c9 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 25 Mar 2026 22:19:31 -0500 Subject: [PATCH 006/434] Part 2.7: Add G-code back-transform and tree support belt floor clipping - Add BeltBackTransform class that inverts the shear/scale matrix and applies it in GCodeWriter::to_machine_coords() so G-code outputs in the machine's physical coordinate space, gated by new belt_gcode_back_transform config option - Extend belt floor clipping to all three tree support pipelines (Prusa-style, Orca organic, TreeModelVolumes) with per-layer polygon clipping, anti-overhang integration, and belt raft extension layers - Fix tree drop_nodes() belt termination, organic support global Z offset, collision calculation index bug, and first-layer brim/empty layer checks for belt printers two-shot - first build built but didn't plumb to UI. Woah. add pre-slice axis remap, because Y needs to be Z going to change tactic and move based on bbox min switch to per axis snapping per axis swap snap now per object build plate tilt wasn't invalidating slicer settings support upper bound now correct, need to get lower bound corrected axis swapped support termination corrected Z Shear works with and without pre-slice remap now --- src/libslic3r/CMakeLists.txt | 2 + src/libslic3r/GCode.cpp | 142 +++++++++++++++++++ src/libslic3r/GCode.hpp | 5 + src/libslic3r/GCode/BeltBackTransform.cpp | 150 +++++++++++++++++++++ src/libslic3r/GCode/BeltBackTransform.hpp | 45 +++++++ src/libslic3r/GCode/GCodeProcessor.cpp | 22 +++ src/libslic3r/GCode/GCodeProcessor.hpp | 6 + src/libslic3r/GCodeWriter.cpp | 32 ++++- src/libslic3r/GCodeWriter.hpp | 9 ++ src/libslic3r/Preset.cpp | 6 +- src/libslic3r/Print.cpp | 11 +- src/libslic3r/Print.hpp | 31 +++++ src/libslic3r/PrintConfig.cpp | 60 ++++++++- src/libslic3r/PrintConfig.hpp | 10 ++ src/libslic3r/PrintObject.cpp | 19 ++- src/libslic3r/PrintObjectSlice.cpp | 117 +++++++++++++--- src/libslic3r/Support/TreeModelVolumes.cpp | 2 +- src/libslic3r/Support/TreeSupport.cpp | 93 +++++++++++++ src/libslic3r/Support/TreeSupport3D.cpp | 2 +- src/slic3r/GUI/Tab.cpp | 39 +++++- 20 files changed, 767 insertions(+), 36 deletions(-) create mode 100644 src/libslic3r/GCode/BeltBackTransform.cpp create mode 100644 src/libslic3r/GCode/BeltBackTransform.hpp diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 5f9591452f..c2d92ee800 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -208,6 +208,8 @@ set(lisbslic3r_sources GCode/AdaptivePAProcessor.hpp GCode/AvoidCrossingPerimeters.cpp GCode/AvoidCrossingPerimeters.hpp + GCode/BeltBackTransform.cpp + GCode/BeltBackTransform.hpp GCode/ConflictChecker.cpp GCode/ConflictChecker.hpp GCode/CoolingBuffer.cpp diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 41cfa7b72a..8391f9b820 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2432,6 +2432,16 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Build volume extents for Rev remap mode. BoundingBoxf bbox_bed(print.config().printable_area.values); m_writer.set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(), print.config().printable_height.value)); + // Initialize the back-transform that undoes slicing shear/scale. + m_writer.set_belt_back_transform(print.config()); + // Per-axis origin snap: store config; actual snap is computed + // per-instance in update_origin_snap() called from set_origin(). + m_origin_snap[0] = print.config().belt_origin_snap_x.value; + m_origin_snap[1] = print.config().belt_origin_snap_y.value; + m_origin_snap[2] = print.config().belt_origin_snap_z.value; + m_origin_snap_offset[0] = print.config().belt_origin_offset_x.value; + m_origin_snap_offset[1] = print.config().belt_origin_offset_y.value; + m_origin_snap_offset[2] = print.config().belt_origin_offset_z.value; } // How many times will be change_layer() called? @@ -2544,6 +2554,9 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.write_format("; belt_scale_y_angle = %.1f\n", print.config().belt_scale_y_angle.value); file.write_format("; belt_scale_z = %s\n", full_cfg.opt_serialize("belt_scale_z").c_str()); file.write_format("; belt_scale_z_angle = %.1f\n", print.config().belt_scale_z_angle.value); + file.write_format("; belt_preslice_remap_x = %s\n", full_cfg.opt_serialize("belt_preslice_remap_x").c_str()); + file.write_format("; belt_preslice_remap_y = %s\n", full_cfg.opt_serialize("belt_preslice_remap_y").c_str()); + file.write_format("; belt_preslice_remap_z = %s\n", full_cfg.opt_serialize("belt_preslice_remap_z").c_str()); } if (is_bbl_printers) file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str()); @@ -3248,6 +3261,8 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } print.throw_if_canceled(); this->set_origin(unscale((*print_object_instance_sequential_active)->shift)); + this->update_origin_snap((*print_object_instance_sequential_active)->print_object, + (*print_object_instance_sequential_active)->shift); // BBS: prime extruder if extruder change happens before this object instance bool prime_extruder = false; @@ -5212,6 +5227,7 @@ LayerResult GCode::process_layer( m_avoid_crossing_perimeters.use_external_mp_once(); m_last_obj_copy = this_object_copy; this->set_origin(unscale(offset)); + this->update_origin_snap(&instance_to_print.print_object, offset); if (instance_to_print.object_by_extruder.support != nullptr) { m_layer = layers[instance_to_print.layer_id].support_layer; m_object_layer_over_raft = false; @@ -5235,6 +5251,7 @@ LayerResult GCode::process_layer( m_avoid_crossing_perimeters.use_external_mp_once(); m_last_obj_copy = this_object_copy; this->set_origin(unscale(offset)); + this->update_origin_snap(&instance_to_print.print_object, offset); ExtrusionEntityCollection support_eec; // BBS @@ -5283,6 +5300,7 @@ LayerResult GCode::process_layer( m_avoid_crossing_perimeters.use_external_mp_once(); m_last_obj_copy = this_object_copy; this->set_origin(unscale(offset)); + this->update_origin_snap(&instance_to_print.print_object, offset); //FIXME the following code prints regions in the order they are defined, the path is not optimized in any way. auto has_infill = [](const std::vector &by_region) { @@ -5509,6 +5527,130 @@ void GCode::set_origin(const Vec2d &pointf) m_origin = pointf; } +void GCode::update_origin_snap(const PrintObject *obj, const Point &inst_shift) +{ + if (!m_origin_snap[0] && !m_origin_snap[1] && !m_origin_snap[2]) + return; + + // Clear existing snap so to_machine_coords gives raw machine coords for bbox computation. + for (int a = 0; a < 3; ++a) + m_writer.set_origin_snap(a, false, 0., 0.); + + // Reconstruct the belt pipeline transform for this object (same as + // PrintObjectSlice.cpp: z_shift * scale * shear * pre_remap). + const auto &cfg = m_config; + Transform3d belt = Transform3d::Identity(); + + // Pre-slice remap + int pre_rx = int(cfg.belt_preslice_remap_x.value); + int pre_ry = int(cfg.belt_preslice_remap_y.value); + int pre_rz = int(cfg.belt_preslice_remap_z.value); + if (pre_rx != int(BeltRemapAxis::PosX) || + pre_ry != int(BeltRemapAxis::PosY) || + pre_rz != int(BeltRemapAxis::PosZ)) { + auto remap_col = [](int r) -> Vec3d { + int a = r % 3; Vec3d c = Vec3d::Zero(); + c[a] = (r < 3) ? 1.0 : -1.0; + return c; + }; + Matrix3d lin; + lin.col(0) = remap_col(pre_rx); + lin.col(1) = remap_col(pre_ry); + lin.col(2) = remap_col(pre_rz); + Transform3d pre = Transform3d::Identity(); + pre.linear() = lin; + if (pre_rx >= 6 || pre_ry >= 6 || pre_rz >= 6) { + BoundingBoxf bb(cfg.printable_area.values); + Vec3d vm(bb.max.x(), bb.max.y(), cfg.printable_height.value); + Vec3d tr = Vec3d::Zero(); + if (pre_rx >= 6) tr[0] = vm[pre_rx % 3]; + if (pre_ry >= 6) tr[1] = vm[pre_ry % 3]; + if (pre_rz >= 6) tr[2] = vm[pre_rz % 3]; + pre.translation() = tr; + } + belt = pre * belt; + } + + // Shear + auto shear_f = [](BeltShearMode m, double a) -> double { + double r = Geometry::deg2rad(a), s = std::sin(r), c = std::cos(r); + switch (m) { + case BeltShearMode::PosCot: return (s > EPSILON) ? c/s : 0.; + case BeltShearMode::NegCot: return (s > EPSILON) ? -c/s : 0.; + case BeltShearMode::PosTan: return (c > EPSILON) ? s/c : 0.; + case BeltShearMode::NegTan: return (c > EPSILON) ? -s/c : 0.; + default: return 0.; + } + }; + struct AS { BeltShearMode m; double a; int f; }; + AS axes[3] = { + {cfg.belt_shear_x.value, cfg.belt_shear_x_angle.value, int(cfg.belt_shear_x_from.value)}, + {cfg.belt_shear_y.value, cfg.belt_shear_y_angle.value, int(cfg.belt_shear_y_from.value)}, + {cfg.belt_shear_z.value, cfg.belt_shear_z_angle.value, int(cfg.belt_shear_z_from.value)}, + }; + Transform3d shear = Transform3d::Identity(); + for (int i = 0; i < 3; ++i) + if (axes[i].m != BeltShearMode::None) { + double f = shear_f(axes[i].m, axes[i].a); + if (std::abs(f) > EPSILON) + shear.matrix()(i, axes[i].f) += f; + } + + // Scale + auto scale_f = [](BeltScaleMode m, double a) -> double { + if (m == BeltScaleMode::None) return 1.; + double r = Geometry::deg2rad(a), s = std::sin(r), c = std::cos(r); + switch (m) { + case BeltScaleMode::InvSin: return (s > EPSILON) ? 1./s : 1.; + case BeltScaleMode::InvCos: return (c > EPSILON) ? 1./c : 1.; + case BeltScaleMode::Sin: return s; + case BeltScaleMode::Cos: return c; + default: return 1.; + } + }; + Transform3d sc = Transform3d::Identity(); + sc.matrix()(0,0) = scale_f(cfg.belt_scale_x.value, cfg.belt_scale_x_angle.value); + sc.matrix()(1,1) = scale_f(cfg.belt_scale_y.value, cfg.belt_scale_y_angle.value); + sc.matrix()(2,2) = scale_f(cfg.belt_scale_z.value, cfg.belt_scale_z_angle.value); + + belt = sc * shear * belt; + + // Z-shift + double zs = (obj->belt_min_z() < 0.) ? -obj->belt_min_z() : 0.; + if (zs > 0.) { + Transform3d zsh = Transform3d::Identity(); + zsh.matrix()(2, 3) = zs; + belt = zsh * belt; + } + + // Full transform: belt * trafo_centered + Transform3d full = belt * obj->trafo_centered(); + + // Instance shift in slicer space + global Z offset + Vec3d shift(unscale(inst_shift.x()), + unscale(inst_shift.y()), + obj->belt_global_z_offset()); + + // Compute this instance's machine-space bbox min + BoundingBoxf3 bb = obj->model_object()->raw_bounding_box(); + Vec3d mn = bb.min.cast(), mx = bb.max.cast(); + Vec3d inst_min(std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max()); + for (int i = 0; i < 8; ++i) { + Vec3d c((i & 1) ? mx.x() : mn.x(), + (i & 2) ? mx.y() : mn.y(), + (i & 4) ? mx.z() : mn.z()); + Vec3d mc = m_writer.to_machine_coords(full * c + shift); + for (int a = 0; a < 3; ++a) + inst_min[a] = std::min(inst_min[a], mc[a]); + } + + // Update writer snap for each enabled axis + for (int a = 0; a < 3; ++a) + m_writer.set_origin_snap(a, m_origin_snap[a], m_origin_snap_offset[a], inst_min[a]); +} + std::string GCode::preamble() { std::string gcode = m_writer.preamble(); diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index d379300bad..dbec7d0315 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -493,6 +493,11 @@ private: This affects the input arguments supplied to the extrude*() and travel_to() methods. */ Vec2d m_origin; + // Per-axis origin snap: shift G-code so each object's bbox min = offset. + bool m_origin_snap[3] = {false, false, false}; + double m_origin_snap_offset[3] = {0., 0., 0.}; + // Called when switching instances to recompute the writer's snap for this instance. + void update_origin_snap(const PrintObject *obj, const Point &inst_shift); FullPrintConfig m_config; DynamicConfig m_calib_config; // scaled G-code resolution diff --git a/src/libslic3r/GCode/BeltBackTransform.cpp b/src/libslic3r/GCode/BeltBackTransform.cpp new file mode 100644 index 0000000000..7cd5bc6b53 --- /dev/null +++ b/src/libslic3r/GCode/BeltBackTransform.cpp @@ -0,0 +1,150 @@ +#include "BeltBackTransform.hpp" +#include "../Geometry.hpp" +#include + +namespace Slic3r { + +// Keep in sync with PrintObjectSlice.cpp compute_shear_factor (lines ~147-157). +static double compute_shear_factor(BeltShearMode mode, double angle_deg) +{ + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad); + double cos_a = std::cos(angle_rad); + switch (mode) { + case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; + case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; + case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; + case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; + default: return 0.; + } +} + +// Keep in sync with PrintObjectSlice.cpp compute_scale_factor (lines ~180-192). +static double compute_scale_factor(BeltScaleMode mode, double angle_deg) +{ + if (mode == BeltScaleMode::None) return 1.; + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad); + double cos_a = std::cos(angle_rad); + switch (mode) { + case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; + case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; + case BeltScaleMode::Sin: return sin_a; + case BeltScaleMode::Cos: return cos_a; + default: return 1.; + } +} + +bool BeltBackTransform::init_from_config(const PrintConfig &config) +{ + m_active = false; + m_inverse = Transform3d::Identity(); + + if (!config.belt_printer.value || !config.belt_gcode_back_transform.value) + return false; + + // --- Pre-slice axis remap (same as PrintObjectSlice.cpp) --- + int pre_rx = int(config.belt_preslice_remap_x.value); + int pre_ry = int(config.belt_preslice_remap_y.value); + int pre_rz = int(config.belt_preslice_remap_z.value); + + bool has_preslice_remap = (pre_rx != int(BeltRemapAxis::PosX) || + pre_ry != int(BeltRemapAxis::PosY) || + pre_rz != int(BeltRemapAxis::PosZ)); + + // Require at least one active transform to proceed. + bool has_global_shear = config.belt_shear_x_global.value || + config.belt_shear_y_global.value || + config.belt_shear_z_global.value; + if (!has_global_shear && !has_preslice_remap) + return false; + + // Build pre-slice remap matrix. + Transform3d pre_remap = Transform3d::Identity(); + if (has_preslice_remap) { + auto remap_column = [](int r) -> Vec3d { + int axis = r % 3; + Vec3d col = Vec3d::Zero(); + if (r < 3) col[axis] = 1.0; + else if (r < 6) col[axis] = -1.0; + else col[axis] = -1.0; // Rev: max - pos + return col; + }; + + Matrix3d remap_lin; + remap_lin.col(0) = remap_column(pre_rx); + remap_lin.col(1) = remap_column(pre_ry); + remap_lin.col(2) = remap_column(pre_rz); + pre_remap.linear() = remap_lin; + + // Rev mode translation (needs build volume extents). + Vec3d remap_trans = Vec3d::Zero(); + if (pre_rx >= 6 || pre_ry >= 6 || pre_rz >= 6) { + BoundingBoxf bbox_bed(config.printable_area.values); + Vec3d vol_max(bbox_bed.max.x(), bbox_bed.max.y(), + config.printable_height.value); + auto add_rev = [&](int r, int out) { + if (r >= 6) remap_trans[out] = vol_max[r % 3]; + }; + add_rev(pre_rx, 0); + add_rev(pre_ry, 1); + add_rev(pre_rz, 2); + } + pre_remap.translation() = remap_trans; + } + + // Build per-axis shear matrix (same as PrintObjectSlice.cpp). + struct AxisShear { BeltShearMode mode; double angle; int from; }; + AxisShear axes[3] = { + { config.belt_shear_x.value, config.belt_shear_x_angle.value, int(config.belt_shear_x_from.value) }, + { config.belt_shear_y.value, config.belt_shear_y_angle.value, int(config.belt_shear_y_from.value) }, + { config.belt_shear_z.value, config.belt_shear_z_angle.value, int(config.belt_shear_z_from.value) }, + }; + + Matrix3d shear = Matrix3d::Identity(); + bool has_shear = false; + for (int row = 0; row < 3; ++row) { + if (axes[row].mode != BeltShearMode::None) { + double factor = compute_shear_factor(axes[row].mode, axes[row].angle); + if (std::abs(factor) > EPSILON) { + shear(row, axes[row].from) += factor; + has_shear = true; + } + } + } + + // Build per-axis scale diagonal matrix (same as PrintObjectSlice.cpp). + double sx = compute_scale_factor(config.belt_scale_x.value, config.belt_scale_x_angle.value); + double sy = compute_scale_factor(config.belt_scale_y.value, config.belt_scale_y_angle.value); + double sz = compute_scale_factor(config.belt_scale_z.value, config.belt_scale_z_angle.value); + + Matrix3d scale = Matrix3d::Identity(); + bool has_scale = (std::abs(sx - 1.) > EPSILON || + std::abs(sy - 1.) > EPSILON || + std::abs(sz - 1.) > EPSILON); + if (has_scale) { + scale(0, 0) = sx; + scale(1, 1) = sy; + scale(2, 2) = sz; + } + + if (!has_shear && !has_scale && !has_preslice_remap) + return false; + + // Forward pipeline: scale * shear * pre_remap (same order as PrintObjectSlice.cpp). + Transform3d combined = Transform3d::Identity(); + combined.linear() = scale * shear; + combined = combined * pre_remap; + m_inverse = combined.inverse(); + m_active = true; + return true; +} + +Vec3d BeltBackTransform::apply(const Vec3d &pos) const +{ + if (!m_active) + return pos; + return m_inverse * pos; +} + +} // namespace Slic3r diff --git a/src/libslic3r/GCode/BeltBackTransform.hpp b/src/libslic3r/GCode/BeltBackTransform.hpp new file mode 100644 index 0000000000..5a75d94dc8 --- /dev/null +++ b/src/libslic3r/GCode/BeltBackTransform.hpp @@ -0,0 +1,45 @@ +#ifndef slic3r_BeltBackTransform_hpp_ +#define slic3r_BeltBackTransform_hpp_ + +#include "../libslic3r.h" +#include "../Point.hpp" +#include "../PrintConfig.hpp" + +namespace Slic3r { + +// Reverses the pre-slice remap + shear + scale transforms that +// PrintObjectSlice.cpp applies to belt printer geometry, converting G-code +// coordinates from the sliced (remapped/sheared/scaled) frame back to the +// machine's real coordinate space. +// +// Initialized once from PrintConfig, then applied per-point in +// GCodeWriter::to_machine_coords() before axis remapping. +// +// Active when belt_gcode_back_transform is true AND at least one of: +// - a shear axis has global mode enabled, or +// - a pre-slice axis remap is non-identity. +class BeltBackTransform +{ +public: + BeltBackTransform() = default; + + // Initialize from belt printer config. Rebuilds the same pre-slice remap, + // shear, and scale matrices as PrintObjectSlice.cpp and precomputes the + // affine inverse. Returns true if a non-identity back-transform was computed. + bool init_from_config(const PrintConfig &config); + + // Apply the inverse transform to a point. Returns pos unchanged if + // no back-transform is active. + Vec3d apply(const Vec3d &pos) const; + + // True if a non-identity back-transform is active. + bool is_active() const { return m_active; } + +private: + bool m_active = false; + Transform3d m_inverse = Transform3d::Identity(); +}; + +} // namespace Slic3r + +#endif // slic3r_BeltBackTransform_hpp_ diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 8def7c08db..4c630bb6de 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -3128,6 +3128,28 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers if (boost::starts_with(comment, " belt_scale_z_angle = ")) { try { m_result.belt_scale_z_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; } + // Pre-slice axis remap + auto parse_remap_axis = [](const std::string &s) -> BeltRemapAxis { + if (s == "pos_x") return BeltRemapAxis::PosX; + if (s == "pos_y") return BeltRemapAxis::PosY; + if (s == "pos_z") return BeltRemapAxis::PosZ; + if (s == "neg_x") return BeltRemapAxis::NegX; + if (s == "neg_y") return BeltRemapAxis::NegY; + if (s == "neg_z") return BeltRemapAxis::NegZ; + if (s == "rev_x") return BeltRemapAxis::RevX; + if (s == "rev_y") return BeltRemapAxis::RevY; + if (s == "rev_z") return BeltRemapAxis::RevZ; + return BeltRemapAxis::PosX; + }; + if (boost::starts_with(comment, " belt_preslice_remap_x = ")) { + m_result.belt_preslice_remap_x = parse_remap_axis(trim(std::string(comment.substr(25)))); return; + } + if (boost::starts_with(comment, " belt_preslice_remap_y = ")) { + m_result.belt_preslice_remap_y = parse_remap_axis(trim(std::string(comment.substr(25)))); return; + } + if (boost::starts_with(comment, " belt_preslice_remap_z = ")) { + m_result.belt_preslice_remap_z = parse_remap_axis(trim(std::string(comment.substr(25)))); return; + } } // wipe start tag if (boost::starts_with(comment, reserved_tag(ETags::Wipe_Start))) { diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index f953345f4e..7d08a9cdfd 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -246,6 +246,9 @@ class Print; float belt_scale_y_angle{ 45.f }; BeltScaleMode belt_scale_z{ BeltScaleMode::None }; float belt_scale_z_angle{ 45.f }; + BeltRemapAxis belt_preslice_remap_x{ BeltRemapAxis::PosX }; + BeltRemapAxis belt_preslice_remap_y{ BeltRemapAxis::PosY }; + BeltRemapAxis belt_preslice_remap_z{ BeltRemapAxis::PosZ }; SettingsIds settings_ids; size_t filaments_count; bool backtrace_enabled; @@ -321,6 +324,9 @@ class Print; belt_scale_y_angle = other.belt_scale_y_angle; belt_scale_z = other.belt_scale_z; belt_scale_z_angle = other.belt_scale_z_angle; + belt_preslice_remap_x = other.belt_preslice_remap_x; + belt_preslice_remap_y = other.belt_preslice_remap_y; + belt_preslice_remap_z = other.belt_preslice_remap_z; #if ENABLE_GCODE_VIEWER_STATISTICS time = other.time; #endif diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index d6bb5a0bb9..8c2f7122b8 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -37,18 +37,40 @@ void GCodeWriter::set_build_volume_max(const Vec3d &max) m_build_vol_max = max; } +void GCodeWriter::set_belt_back_transform(const PrintConfig &config) +{ + m_belt_back_transform.init_from_config(config); +} + +void GCodeWriter::set_origin_snap(int axis, bool enable, double offset, double bbox_min) +{ + if (axis >= 0 && axis < 3) { + m_origin_snap[axis] = enable; + m_origin_offset[axis] = offset; + m_origin_bbox_min[axis] = bbox_min; + } +} + Vec3d GCodeWriter::to_machine_coords(const Vec3d &pos) const { if (!is_belt_printer()) return pos; + // Step 1: Undo the shear/scale applied during slicing. + Vec3d p = m_belt_back_transform.apply(pos); + // Step 2: Apply axis remapping for the machine's coordinate convention. // BeltRemapAxis: 0-2 = +X/+Y/+Z, 3-5 = -X/-Y/-Z, 6-8 = Rev X/Y/Z - auto remap = [this, &pos](int r) -> double { + auto remap = [this, &p](int r) -> double { int axis = r % 3; - if (r < 3) return pos[axis]; - if (r < 6) return -pos[axis]; - return m_build_vol_max[axis] - pos[axis]; + if (r < 3) return p[axis]; + if (r < 6) return -p[axis]; + return m_build_vol_max[axis] - p[axis]; }; - return { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) }; + Vec3d result = { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) }; + // Per-axis origin snap: shift so bbox min on each enabled axis = offset. + for (int i = 0; i < 3; ++i) + if (m_origin_snap[i]) + result[i] -= (m_origin_bbox_min[i] - m_origin_offset[i]); + return result; } bool GCodeWriter::supports_separate_travel_acceleration(GCodeFlavor flavor) diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index 6074eb247c..2334f5d416 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -8,6 +8,7 @@ #include "Point.hpp" #include "PrintConfig.hpp" #include "GCode/CoolingBuffer.hpp" +#include "GCode/BeltBackTransform.hpp" namespace Slic3r { @@ -132,6 +133,10 @@ public: void set_axis_remap(int rx, int ry, int rz); // Set build volume extents for Rev remap mode (max X, Y, Z). void set_build_volume_max(const Vec3d &max); + // Initialize the belt back-transform that undoes slicing shear/scale. + void set_belt_back_transform(const PrintConfig &config); + // Set per-axis origin snap: shifts G-code so bbox min on this axis = offset. + void set_origin_snap(int axis, bool enable, double offset, double bbox_min); // Transform a point from the slicing frame to machine coordinates. Vec3d to_machine_coords(const Vec3d &pos) const; @@ -194,6 +199,10 @@ public: int m_remap_y = 1; int m_remap_z = 2; Vec3d m_build_vol_max = Vec3d::Zero(); + BeltBackTransform m_belt_back_transform; + bool m_origin_snap[3] = {false, false, false}; + double m_origin_offset[3] = {0., 0., 0.}; // target coord for bbox min + double m_origin_bbox_min[3] = {0., 0., 0.}; // computed bbox min in machine space double m_current_speed; bool m_is_first_layer = true; diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 9437b6098e..9cc5e03d40 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1014,7 +1014,11 @@ static std::vector s_Preset_printer_options { "belt_shear_y", "belt_shear_y_angle", "belt_shear_y_from", "belt_shear_y_global", "belt_shear_z", "belt_shear_z_angle", "belt_shear_z_from", "belt_shear_z_global", "belt_scale_x", "belt_scale_x_angle", "belt_scale_y", "belt_scale_y_angle", "belt_scale_z", "belt_scale_z_angle", - "belt_gcode_remap_x", "belt_gcode_remap_y", "belt_gcode_remap_z", + "belt_preslice_remap_x", "belt_preslice_remap_y", "belt_preslice_remap_z", + "belt_gcode_remap_x", "belt_gcode_remap_y", "belt_gcode_remap_z", "belt_gcode_back_transform", + "belt_origin_snap_x", "belt_origin_offset_x", + "belt_origin_snap_y", "belt_origin_offset_y", + "belt_origin_snap_z", "belt_origin_offset_z", "belt_support_floor_offset", "belt_support_floor_mode", "belt_support_z_offset_mode", "gcode_flavor", "fan_kickstart", "fan_speedup_time", "fan_speedup_overhangs", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 0aef12a74b..0b7190e8d1 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -103,6 +103,9 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n "belt_gcode_remap_x", "belt_gcode_remap_y", "belt_gcode_remap_z", + "belt_origin_snap_x", "belt_origin_offset_x", + "belt_origin_snap_y", "belt_origin_offset_y", + "belt_origin_snap_z", "belt_origin_offset_z", //BBS "additional_cooling_fan_speed", "reduce_crossing_wall", @@ -280,6 +283,9 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n // In Spiral Vase mode, holes are closed and only the largest area contour is kept at each layer. // Therefore toggling the Spiral Vase on / off requires complete reslicing. || opt_key == "spiral_mode" + // Build plate tilt changes slicing plane orientation. + || opt_key == "build_plate_tilt_x" + || opt_key == "build_plate_tilt_y" // Belt printer transform options change the mesh geometry before slicing. || opt_key == "belt_printer" || opt_key == "belt_printer_angle" @@ -300,7 +306,10 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "belt_scale_y" || opt_key == "belt_scale_y_angle" || opt_key == "belt_scale_z" - || opt_key == "belt_scale_z_angle") { + || opt_key == "belt_scale_z_angle" + || opt_key == "belt_preslice_remap_x" + || opt_key == "belt_preslice_remap_y" + || opt_key == "belt_preslice_remap_z") { osteps.emplace_back(posSlice); } else if ( opt_key == "belt_support_floor_offset" diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index d0c050f442..c13a1407c0 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -186,6 +186,36 @@ class ConstSupportLayerPtrsAdaptor : public ConstVectorOfPtrsAdaptor(data) {} }; +// Returns the model's raw bounding box with pre-slice axis remap applied. +// When no remap is active, returns the unmodified raw_bounding_box(). +inline BoundingBoxf3 belt_remapped_bbox(const ModelObject &model_object, const PrintConfig &config) +{ + BoundingBoxf3 bb = model_object.raw_bounding_box(); + int pre_rx = int(config.belt_preslice_remap_x.value); + int pre_ry = int(config.belt_preslice_remap_y.value); + int pre_rz = int(config.belt_preslice_remap_z.value); + if (pre_rx == int(BeltRemapAxis::PosX) && + pre_ry == int(BeltRemapAxis::PosY) && + pre_rz == int(BeltRemapAxis::PosZ)) + return bb; // Identity remap, no change. + auto remap_coord = [](int r, const Vec3d &v) -> double { + int axis = r % 3; + if (r < 3) return v[axis]; + return -v[axis]; + }; + Vec3d mn = bb.min.cast(), mx = bb.max.cast(); + BoundingBoxf3 rbb; + for (int i = 0; i < 8; ++i) { + Vec3d c((i & 1) ? mx.x() : mn.x(), + (i & 2) ? mx.y() : mn.y(), + (i & 4) ? mx.z() : mn.z()); + Vec3d rc(remap_coord(pre_rx, c), remap_coord(pre_ry, c), remap_coord(pre_rz, c)); + if (i == 0) rbb = BoundingBoxf3(rc, rc); + else rbb.merge(rc); + } + return rbb; +} + // Single instance of a PrintObject. // As multiple PrintObjects may be generated for a single ModelObject (their instances differ in rotation around Z), // ModelObject's instancess will be distributed among these multiple PrintObjects. @@ -579,6 +609,7 @@ private: double m_belt_min_z { 0.0 }; public: double belt_global_z_offset() const { return m_belt_global_z_offset; } + double belt_min_z() const { return m_belt_min_z; } private: diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 7d06f96a85..506bd17a29 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -6132,9 +6132,63 @@ void PrintConfigDef::init_fff_params() def->set_default_value(new ConfigOptionEnum(default_axis)); }; - add_belt_remap("belt_gcode_remap_x", "X", "Which slicing axis maps to machine X in G-code output.", BeltRemapAxis::PosX); - add_belt_remap("belt_gcode_remap_y", "Y", "Which slicing axis maps to machine Y in G-code output.", BeltRemapAxis::PosY); - add_belt_remap("belt_gcode_remap_z", "Z", "Which slicing axis maps to machine Z in G-code output.", BeltRemapAxis::PosZ); + add_belt_remap("belt_preslice_remap_x", "X", + "Before slicing, which model-space axis becomes the slicer's X axis. " + "Use this to re-orient the coordinate system so the slicer's XY plane matches " + "your belt printer's physical bed plane. For a printer whose bed is in the XZ plane, " + "set Y to +Z and Z to +Y (or -Y) to swap the vertical and belt-travel axes. " + "Default +X: no change.", + BeltRemapAxis::PosX); + add_belt_remap("belt_preslice_remap_y", "Y", + "Before slicing, which model-space axis becomes the slicer's Y axis. " + "The slicer treats Y as one of the two horizontal bed axes. If your physical " + "belt surface runs along the Z axis, map Y to +Z here so the slicer slices " + "along the correct plane. Default +Y: no change.", + BeltRemapAxis::PosY); + add_belt_remap("belt_preslice_remap_z", "Z", + "Before slicing, which model-space axis becomes the slicer's Z axis (layer stacking direction). " + "The slicer builds layers upward along this axis. If your printer's layer-stacking " + "direction is the physical Y axis, map Z to +Y (or -Y for inverted direction). " + "Rev mode mirrors relative to the build volume maximum. Default +Z: no change.", + BeltRemapAxis::PosZ); + + add_belt_remap("belt_gcode_remap_x", "X", "Which slicing axis maps to machine X in G-code output. Applied AFTER slicing, during G-code generation.", BeltRemapAxis::PosX); + add_belt_remap("belt_gcode_remap_y", "Y", "Which slicing axis maps to machine Y in G-code output. Applied AFTER slicing, during G-code generation.", BeltRemapAxis::PosY); + add_belt_remap("belt_gcode_remap_z", "Z", "Which slicing axis maps to machine Z in G-code output. Applied AFTER slicing, during G-code generation.", BeltRemapAxis::PosZ); + + def = this->add("belt_gcode_back_transform", coBool); + def->label = L("G-code back-transform"); + def->category = L("Printable space"); + def->tooltip = L("Reverse the shear/scale transform applied during slicing so G-code " + "coordinates are in the machine's physical coordinate space. " + "Requires at least one shear axis with global mode enabled."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + + auto add_belt_origin_snap = [this](const char *key_snap, const char *key_offset, + const char *axis_label) { + auto def = this->add(key_snap, coBool); + def->label = L(axis_label); + def->category = L("Printable space"); + std::string tip = std::string("Shift G-code output so the object's bounding box minimum on machine ") + + axis_label + " equals the offset value."; + def->tooltip = L(tip); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + + def = this->add(key_offset, coFloat); + def->label = L("Offset"); + def->category = L("Printable space"); + def->tooltip = L("Target coordinate for the bounding box minimum on this machine axis."); + def->sidetext = L("mm"); + def->min = -10000; + def->max = 10000; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0)); + }; + add_belt_origin_snap("belt_origin_snap_x", "belt_origin_offset_x", "X"); + add_belt_origin_snap("belt_origin_snap_y", "belt_origin_offset_y", "Y"); + add_belt_origin_snap("belt_origin_snap_z", "belt_origin_offset_z", "Z"); // Belt support floor debug controls def = this->add("belt_support_floor_offset", coFloat); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index fc533bf6bb..8b469c4dc6 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1488,9 +1488,19 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionFloat, belt_scale_y_angle)) ((ConfigOptionEnum, belt_scale_z)) ((ConfigOptionFloat, belt_scale_z_angle)) + ((ConfigOptionEnum, belt_preslice_remap_x)) + ((ConfigOptionEnum, belt_preslice_remap_y)) + ((ConfigOptionEnum, belt_preslice_remap_z)) ((ConfigOptionEnum, belt_gcode_remap_x)) ((ConfigOptionEnum, belt_gcode_remap_y)) ((ConfigOptionEnum, belt_gcode_remap_z)) + ((ConfigOptionBool, belt_gcode_back_transform)) + ((ConfigOptionBool, belt_origin_snap_x)) + ((ConfigOptionFloat, belt_origin_offset_x)) + ((ConfigOptionBool, belt_origin_snap_y)) + ((ConfigOptionFloat, belt_origin_offset_y)) + ((ConfigOptionBool, belt_origin_snap_z)) + ((ConfigOptionFloat, belt_origin_offset_z)) ((ConfigOptionFloat, belt_support_floor_offset)) ((ConfigOptionEnum, belt_support_floor_mode)) ((ConfigOptionEnum, belt_support_z_offset_mode)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index f803cc0369..ba2ed408ce 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -3402,9 +3402,16 @@ void PrintObject::update_slicing_parameters() double belt_floor_shear_factor_out = 0.0; int belt_floor_from_axis_out = 1; double belt_floor_z_shift_out = 0.0; - // Belt shear/scale may change the effective Z height. + // Belt shear/scale/pre-remap may change the effective Z height. const auto &pcfg = this->print()->config(); if (pcfg.belt_printer.value) { + BoundingBoxf3 bb = belt_remapped_bbox(*this->model_object(), pcfg); + bool has_preslice_remap = (int(pcfg.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || + int(pcfg.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || + int(pcfg.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ)); + if (has_preslice_remap) + object_height = bb.size().z(); + bool has_z_shear = pcfg.belt_shear_z.value != BeltShearMode::None; bool has_z_scale = pcfg.belt_scale_z.value != BeltScaleMode::None; if (has_z_shear || has_z_scale) { @@ -3435,7 +3442,6 @@ void PrintObject::update_slicing_parameters() double scale_z = compute_scale_factor(pcfg.belt_scale_z.value, pcfg.belt_scale_z_angle.value); if (has_z_shear && std::abs(shear_factor) > EPSILON) { int from = int(pcfg.belt_shear_z_from.value); - BoundingBoxf3 bb = this->model_object()->raw_bounding_box(); double min_rz = std::numeric_limits::max(); double max_rz = std::numeric_limits::lowest(); for (double vz : {bb.min.z(), bb.max.z()}) @@ -3507,8 +3513,15 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full if (object_max_z <= 0.f) { BoundingBoxf3 bb = model_object.raw_bounding_box(); object_max_z = (float)bb.size().z(); - // Belt shear/scale may change the effective Z height. + // Belt pre-remap/shear/scale may change the effective Z height. if (print_config.belt_printer.value) { + bb = belt_remapped_bbox(model_object, print_config); + bool has_preslice_remap = (int(print_config.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || + int(print_config.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || + int(print_config.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ)); + if (has_preslice_remap) + object_max_z = (float)bb.size().z(); + bool has_z_shear = print_config.belt_shear_z.value != BeltShearMode::None; bool has_z_scale = print_config.belt_scale_z.value != BeltScaleMode::None; if (has_z_shear || has_z_scale) { diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 404550da1a..4c767ea0ed 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -143,6 +143,58 @@ static std::vector slice_volumes_inner( params_base.extra_offset = 0; params_base.trafo = object_trafo; if (print_config.belt_printer.value) { + // --- Pre-slice axis remap --- + // Permutes/negates model axes before slicing so the slicer's coordinate + // system matches the physical bed orientation (e.g. XZ bed instead of XY). + int pre_rx = int(print_config.belt_preslice_remap_x.value); + int pre_ry = int(print_config.belt_preslice_remap_y.value); + int pre_rz = int(print_config.belt_preslice_remap_z.value); + + bool has_preslice_remap = (pre_rx != int(BeltRemapAxis::PosX) || + pre_ry != int(BeltRemapAxis::PosY) || + pre_rz != int(BeltRemapAxis::PosZ)); + + if (has_preslice_remap) { + // Build volume extents for Rev mode. + BoundingBoxf bbox_bed(print_config.printable_area.values); + Vec3d vol_max(bbox_bed.max.x(), bbox_bed.max.y(), + print_config.printable_height.value); + + // Each remap value selects a source axis and sign. + // The column vector tells the matrix which input axis feeds this output. + auto remap_column = [](int r) -> Vec3d { + int axis = r % 3; + Vec3d col = Vec3d::Zero(); + if (r < 3) col[axis] = 1.0; // +axis + else if (r < 6) col[axis] = -1.0; // -axis + else col[axis] = -1.0; // Rev: max - pos = -(pos - max) + return col; + }; + + Matrix3d remap_lin; + remap_lin.col(0) = remap_column(pre_rx); + remap_lin.col(1) = remap_column(pre_ry); + remap_lin.col(2) = remap_column(pre_rz); + + // Translation for Rev modes: output = max[src] - input[src]. + Vec3d remap_trans = Vec3d::Zero(); + auto add_rev_offset = [&](int r, int out_axis) { + if (r >= 6) { + int src_axis = r % 3; + remap_trans[out_axis] = vol_max[src_axis]; + } + }; + add_rev_offset(pre_rx, 0); + add_rev_offset(pre_ry, 1); + add_rev_offset(pre_rz, 2); + + Transform3d pre_remap = Transform3d::Identity(); + pre_remap.linear() = remap_lin; + pre_remap.translation() = remap_trans; + + params_base.trafo = pre_remap * params_base.trafo; + } + // Build per-axis shear matrix from 3 independent axis configs. auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { double angle_rad = Geometry::deg2rad(angle_deg); @@ -204,11 +256,12 @@ static std::vector slice_volumes_inner( } // Apply: scale * shear * trafo (shear first, then scale). - if (has_shear || has_scale) { + if (has_shear || has_scale) params_base.trafo = belt_scale * belt_shear * params_base.trafo; - // After the shear/scale transform, the mesh may clip through the - // build plate (Z < 0). Detect this and shift the mesh up. + // After pre-remap/shear/scale, the mesh may clip through the build + // plate (Z < 0). Detect this and shift the mesh up along slicer Z. + if (has_preslice_remap || has_shear || has_scale) { Transform3d combined = params_base.trafo; double min_z = std::numeric_limits::max(); for (const ModelVolume *mv : model_volumes) { @@ -893,17 +946,34 @@ void PrintObject::slice() this->slice_volumes(); m_print->throw_if_canceled(); - // After slicing, m_belt_min_z holds the exact post-shear minimum Z - // in trafo_centered space (which includes the ensure_on_bed Z offset). - // The belt surface is at Z=0 in trafo_centered space; after shear it - // becomes Z = sf*Y, and after the z-shift that keeps the mesh above - // Z=0 it becomes Z = sf*Y + z_shift_val. So belt_floor_z_shift is - // simply the z-shift applied, i.e. max(0, -m_belt_min_z). - // NOTE: do NOT add raw_bounding_box().min.z() here — m_belt_min_z - // already includes the ensure_on_bed offset, unlike the min_rz used - // in update_slicing_parameters() which needs that compensation. + // Belt floor Z-shift: where is the belt surface in final slicer space? + // + // The belt surface is at model_Y=0 (XZ belt plane). After the full + // pipeline (trafo_centered → pre_remap → shear → z_shift), the belt + // surface equation in slicer space is: + // Z_belt = sf * from_axis + belt_surface_z_centered + z_shift_val + // + // belt_surface_z_centered = remapped_bbox.min.z() (the Z position of + // the belt surface in centered-pre-shear slicer space, which is 0 + // without pre-remap but nonzero when e.g. Y↔Z swap shifts the belt + // surface away from Z=0 by the centering offset). + // + // z_shift_val = max(0, -m_belt_min_z) (lifts mesh above Z=0). + // + // So: belt_floor_z_shift = remapped_bb.min.z() + z_shift_val if (std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON) { - m_slicing_params.belt_floor_z_shift = (m_belt_min_z < 0.) ? -m_belt_min_z : 0.; + double z_shift_val = (m_belt_min_z < 0.) ? -m_belt_min_z : 0.; + // With pre-remap, the belt surface (model_Y=0) may not be at Z=0 in + // centered slicer space — add the remapped bbox min Z to compensate. + // Without pre-remap, the belt surface IS at Z=0 and bb.min.z() is + // already folded into m_belt_min_z, so use 0. + const auto &pcfg = this->print()->config(); + bool has_preslice_remap = (int(pcfg.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || + int(pcfg.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || + int(pcfg.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ)); + double belt_surface_z = has_preslice_remap + ? belt_remapped_bbox(*this->model_object(), pcfg).min.z() : 0.; + m_slicing_params.belt_floor_z_shift = belt_surface_z + z_shift_val; } int firstLayerReplacedBy = 0; @@ -989,14 +1059,23 @@ void PrintObject::slice() const auto &za = gaxes[2]; // Z row if (za.global && za.mode != BeltShearMode::None && za.from < 2) { double factor = compute_shear_factor(za.mode, za.angle); - // The Z-shift brought the mesh's lowest sheared vertex to - // Z=0. That vertex's physical Y determines the belt contact - // point. With trafo_z preserved (ensure_on_bed offset), - // min_z = Y_at_contact * factor for bottom-face vertices, - // so: z_offset = center_Y * factor + min_z. + // The global Z offset accounts for the instance's position- + // dependent shear contribution. m_belt_min_z is the minimum Z + // of the mesh after pre_remap + shear + trafo_centered, which + // includes the centering offset on the remapped Z axis. + // Subtract the belt surface's centered Z position so we get + // only the shear-induced contribution (same correction as the + // belt_floor_z_shift fix). + // Same pre-remap guard as belt_floor_z_shift above. + bool has_preslice_remap2 = (int(pcfg.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || + int(pcfg.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || + int(pcfg.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ)); + double belt_surface_z = has_preslice_remap2 + ? belt_remapped_bbox(*this->model_object(), this->print()->config()).min.z() : 0.; + double shear_min_z = m_belt_min_z - belt_surface_z; Point phys = inst_shift; // already has center_offset subtracted double center_on_axis = (za.from == 0) ? unscale(phys.x()) : unscale(phys.y()); - global_z_offset += center_on_axis * factor + m_belt_min_z; + global_z_offset += center_on_axis * factor + shear_min_z; } BOOST_LOG_TRIVIAL(warning) << "Belt global: z_offset=" << global_z_offset diff --git a/src/libslic3r/Support/TreeModelVolumes.cpp b/src/libslic3r/Support/TreeModelVolumes.cpp index e270b62aab..ec38ffed73 100644 --- a/src/libslic3r/Support/TreeModelVolumes.cpp +++ b/src/libslic3r/Support/TreeModelVolumes.cpp @@ -151,7 +151,7 @@ TreeModelVolumes::TreeModelVolumes( double belt_sf = sp2.belt_floor_shear_factor; if (std::abs(belt_sf) > EPSILON && std::abs(print_object.belt_global_z_offset()) > EPSILON && pcfg2.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { - double bb_min_z = std::abs(print_object.model_object()->raw_bounding_box().min.z()); + double bb_min_z = std::abs(belt_remapped_bbox(*print_object.model_object(), pcfg2).min.z()); double extra_depth = bb_min_z + 10.; int num_extra = std::max(0, (int)std::ceil(extra_depth / sp2.layer_height)); if (num_extra > 0) { diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 4966a5a139..60e681add9 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -1733,6 +1733,99 @@ void TreeSupport::generate() + // Belt floor: extend support below the object's first layer by creating + // additional support layers with geometry copied from the lowest content + // layer and clipped at the belt surface. These layers bypass the tree + // algorithm entirely — they're pure geometry added after draw_circles(). + { + const auto &sp = m_slicing_params; + const auto &pcfg = *m_print_config; + const double sf = sp.belt_floor_shear_factor; + if (std::abs(sf) > EPSILON + && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly + && m_object->support_layer_count() > 0) { + const int from_axis = sp.belt_floor_from_axis; + const double floor_off = pcfg.belt_support_floor_offset.value; + // Support layer print_z values are in GLOBAL Z (non-organic inherits + // from object layers which include global_z_offset). Use the GLOBAL + // belt_floor_z_shift to match. + const double z_shift = sp.belt_floor_z_shift; + // Find the lowest non-empty, non-brim support layer. + ExPolygons source_areas; + double source_z = 0; + int layers_with_content = 0; + for (size_t i = 0; i < m_object->support_layer_count(); ++i) { + SupportLayer *sl = m_object->get_support_layer(i); + if (sl && !sl->base_areas.empty()) { + layers_with_content++; + if (layers_with_content >= 2) { + source_areas = sl->base_areas; + source_z = sl->print_z; + break; + } + } + } + // Fallback to first content layer. + if (source_areas.empty()) { + for (size_t i = 0; i < m_object->support_layer_count(); ++i) { + SupportLayer *sl = m_object->get_support_layer(i); + if (sl && !sl->base_areas.empty()) { + source_areas = sl->base_areas; + source_z = sl->print_z; + break; + } + } + } + if (!source_areas.empty()) { + BoundingBoxf3 bb = belt_remapped_bbox(*m_object->model_object(), m_object->print()->config()); + double from_extent = std::abs(bb.min(from_axis)); + double bb_min_z = std::abs(bb.min.z()); + double first_z = m_object->get_support_layer(0)->print_z; + // Depth = from-axis extent + pre-shear bbox Z offset (ensure_on_bed + // distance) + 10mm safety margin. The 10mm is a bodge to avoid + // small cutoff artifacts — ideally computed exactly from belt geometry. + double extra_depth = std::min(from_extent + bb_min_z + 10., std::max(0., first_z)); + int num_extra = std::max(0, (int)std::ceil(extra_depth / sp.layer_height)); + ExPolygons prev_areas = source_areas; + // Build belt extension layers (lowest Z first). + SupportLayerPtrs belt_ext_layers; + for (int i = num_extra; i >= 1 && !prev_areas.empty(); --i) { + double print_z = first_z - i * sp.layer_height; + if (print_z < -sp.layer_height) continue; + double cutoff = (print_z - z_shift - floor_off) / sf; + coord_t cutoff_sc = scale_(cutoff); + coord_t big = scale_(1e3); + Polygon belt_poly; + if (from_axis == 0) { + if (sf > 0) belt_poly.points = {{cutoff_sc,-big},{big,-big},{big,big},{cutoff_sc,big}}; + else belt_poly.points = {{-big,-big},{cutoff_sc,-big},{cutoff_sc,big},{-big,big}}; + } else { + if (sf > 0) belt_poly.points = {{-big,cutoff_sc},{big,cutoff_sc},{big,big},{-big,big}}; + else belt_poly.points = {{-big,-big},{big,-big},{big,cutoff_sc},{-big,cutoff_sc}}; + } + ExPolygons clipped = diff_ex(source_areas, Polygons{belt_poly}); + if (clipped.empty()) continue; + SupportLayer *sl = new SupportLayer(0, 0, m_object, sp.layer_height, print_z, -1); + sl->base_areas = clipped; + // Populate area_groups — generate_toolpaths() iterates these, + // not base_areas directly. + for (auto &expoly : sl->base_areas) + sl->area_groups.emplace_back(&expoly, SupportLayer::BaseType, 0); + sl->lslices = clipped; + sl->lslices_bboxes.reserve(clipped.size()); + for (const ExPolygon &ep : clipped) + sl->lslices_bboxes.emplace_back(get_extents(ep)); + belt_ext_layers.push_back(sl); + } + // Insert at the front of support_layers (they're already in Z order). + if (!belt_ext_layers.empty()) { + auto &sl_vec = m_object->support_layers(); + sl_vec.insert(sl_vec.begin(), belt_ext_layers.begin(), belt_ext_layers.end()); + } + } + } + } + profiler.stage_start(STAGE_GENERATE_TOOLPATHS); m_object->print()->set_status(70, _u8L("Generating support")); generate_toolpaths(); diff --git a/src/libslic3r/Support/TreeSupport3D.cpp b/src/libslic3r/Support/TreeSupport3D.cpp index b551a42ece..9d35cb2dd8 100644 --- a/src/libslic3r/Support/TreeSupport3D.cpp +++ b/src/libslic3r/Support/TreeSupport3D.cpp @@ -3381,7 +3381,7 @@ static void generate_support_areas(Print &print, TreeSupport* tree_support, cons // termination happen inside the belt region and get clipped. // Use the distance from the pre-shear bbox min Z to the part's // post-shear min Z, plus 10mm for base expansion headroom. - double bb_min_z = std::abs(po.model_object()->raw_bounding_box().min.z()); + double bb_min_z = std::abs(belt_remapped_bbox(*po.model_object(), pcfg).min.z()); double extra_depth = bb_min_z + 10.; int num_extra = std::max(0, (int)std::ceil(extra_depth / sp.layer_height)); if (num_extra > 0) { diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index e6de38df78..d5a6b38ebc 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4431,12 +4431,41 @@ void TabPrinter::build_fff() optgroup->append_line(line); } { - Line line = { L("G-code axis remap"), L("Remap slicing-frame axes to machine axes in G-code output") }; + Line line = { L("Pre-slice axis remap"), + L("Remap model axes before slicing so the slicer's coordinate system matches " + "the physical bed orientation. For belt printers whose bed is NOT in the XY plane, " + "use this to swap axes so layers are stacked in the correct physical direction.") }; + line.append_option(optgroup->get_option("belt_preslice_remap_x")); + line.append_option(optgroup->get_option("belt_preslice_remap_y")); + line.append_option(optgroup->get_option("belt_preslice_remap_z")); + optgroup->append_line(line); + } + { + Line line = { L("G-code axis remap (post-slice)"), L("Remap slicing-frame axes to machine axes in G-code output. Applied AFTER slicing, during G-code generation.") }; line.append_option(optgroup->get_option("belt_gcode_remap_x")); line.append_option(optgroup->get_option("belt_gcode_remap_y")); line.append_option(optgroup->get_option("belt_gcode_remap_z")); optgroup->append_line(line); } + optgroup->append_single_option_line("belt_gcode_back_transform"); + { + Line line = { L("Origin snap X"), L("Snap object bbox min X to offset in G-code output") }; + line.append_option(optgroup->get_option("belt_origin_snap_x")); + line.append_option(optgroup->get_option("belt_origin_offset_x")); + optgroup->append_line(line); + } + { + Line line = { L("Origin snap Y"), L("Snap object bbox min Y to offset in G-code output") }; + line.append_option(optgroup->get_option("belt_origin_snap_y")); + line.append_option(optgroup->get_option("belt_origin_offset_y")); + optgroup->append_line(line); + } + { + Line line = { L("Origin snap Z"), L("Snap object bbox min Z to offset in G-code output") }; + line.append_option(optgroup->get_option("belt_origin_snap_z")); + line.append_option(optgroup->get_option("belt_origin_offset_z")); + optgroup->append_line(line); + } { Line line = { L("Support floor"), L("Belt floor awareness for support generation and clipping") }; line.append_option(optgroup->get_option("belt_support_floor_mode")); @@ -5291,7 +5320,9 @@ void TabPrinter::toggle_options() toggle_line("belt_printer_infinite_y", is_belt); for (auto el : {"belt_shear_x", "belt_shear_y", "belt_shear_z", "belt_scale_x", "belt_scale_y", "belt_scale_z", - "belt_gcode_remap_x"}) + "belt_preslice_remap_x", + "belt_gcode_remap_x", "belt_gcode_back_transform", + "belt_origin_snap_x", "belt_origin_snap_y", "belt_origin_snap_z"}) toggle_line(el, is_belt); // Gray out angle/from sub-options when their parent shear/scale mode is None. @@ -5319,6 +5350,10 @@ void TabPrinter::toggle_options() auto scz = m_config->option>("belt_scale_z")->value; toggle_option("belt_scale_z_angle", is_belt && scz != BeltScaleMode::None); + toggle_option("belt_origin_offset_x", is_belt && m_config->opt_bool("belt_origin_snap_x")); + toggle_option("belt_origin_offset_y", is_belt && m_config->opt_bool("belt_origin_snap_y")); + toggle_option("belt_origin_offset_z", is_belt && m_config->opt_bool("belt_origin_snap_z")); + toggle_line("belt_support_floor_mode", is_belt); } From c7aa4ca3eff1ae459c7017b3d32a8e03b68d6966 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 27 Mar 2026 17:34:19 -0500 Subject: [PATCH 007/434] Part 3.1: refactor BeltTransform pipeline add BeltGCodeWriter add BeltGCode consolidate changes into shared classes for BeltGcode --- src/libslic3r/BeltGCode.cpp | 119 +++++ src/libslic3r/BeltGCode.hpp | 28 ++ src/libslic3r/BeltGCodeWriter.cpp | 243 +++++++++ src/libslic3r/BeltGCodeWriter.hpp | 50 ++ src/libslic3r/BeltSliceStrategy.cpp | 62 +++ src/libslic3r/BeltSliceStrategy.hpp | 45 ++ src/libslic3r/BeltTransform.cpp | 246 ++++++++++ src/libslic3r/BeltTransform.hpp | 146 ++++++ src/libslic3r/CMakeLists.txt | 10 + src/libslic3r/GCode.cpp | 542 +++++++-------------- src/libslic3r/GCode.hpp | 34 +- src/libslic3r/GCode/BeltBackTransform.cpp | 124 +---- src/libslic3r/GCodeWriter.cpp | 127 +---- src/libslic3r/GCodeWriter.hpp | 74 +-- src/libslic3r/Print.cpp | 14 +- src/libslic3r/Print.hpp | 26 +- src/libslic3r/PrintObject.cpp | 143 +----- src/libslic3r/PrintObjectSlice.cpp | 177 +------ src/libslic3r/Support/BeltFloorContext.cpp | 124 +++++ src/libslic3r/Support/BeltFloorContext.hpp | 74 +++ src/libslic3r/Support/SupportMaterial.cpp | 131 +---- src/libslic3r/Support/TreeModelVolumes.cpp | 74 +-- src/libslic3r/Support/TreeSupport.cpp | 102 ++-- src/libslic3r/Support/TreeSupport3D.cpp | 36 +- 24 files changed, 1522 insertions(+), 1229 deletions(-) create mode 100644 src/libslic3r/BeltGCode.cpp create mode 100644 src/libslic3r/BeltGCode.hpp create mode 100644 src/libslic3r/BeltGCodeWriter.cpp create mode 100644 src/libslic3r/BeltGCodeWriter.hpp create mode 100644 src/libslic3r/BeltSliceStrategy.cpp create mode 100644 src/libslic3r/BeltSliceStrategy.hpp create mode 100644 src/libslic3r/BeltTransform.cpp create mode 100644 src/libslic3r/BeltTransform.hpp create mode 100644 src/libslic3r/Support/BeltFloorContext.cpp create mode 100644 src/libslic3r/Support/BeltFloorContext.hpp diff --git a/src/libslic3r/BeltGCode.cpp b/src/libslic3r/BeltGCode.cpp new file mode 100644 index 0000000000..dc1b6ca439 --- /dev/null +++ b/src/libslic3r/BeltGCode.cpp @@ -0,0 +1,119 @@ +#include "BeltGCode.hpp" +#include "BeltGCodeWriter.hpp" +#include "BeltTransform.hpp" +#include "Print.hpp" + +#include + +namespace Slic3r { + +void BeltGCode::init_belt_writer(Print &print, bool is_bbl_printers) +{ + if (!print.config().belt_printer.value) + return; + + auto belt_writer = std::make_unique(); + belt_writer->set_is_bbl_machine(is_bbl_printers); + belt_writer->set_belt_angle(print.config().belt_printer_angle.value); + belt_writer->set_axis_remap( + int(print.config().belt_gcode_remap_x.value), + int(print.config().belt_gcode_remap_y.value), + int(print.config().belt_gcode_remap_z.value)); + BoundingBoxf bbox_bed(print.config().printable_area.values); + belt_writer->set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(), + print.config().printable_height.value)); + belt_writer->set_belt_back_transform(print.config()); + m_writer = std::move(belt_writer); + + // Per-axis origin snap config. + m_origin_snap[0] = print.config().belt_origin_snap_x.value; + m_origin_snap[1] = print.config().belt_origin_snap_y.value; + m_origin_snap[2] = print.config().belt_origin_snap_z.value; + m_origin_snap_offset[0] = print.config().belt_origin_offset_x.value; + m_origin_snap_offset[1] = print.config().belt_origin_offset_y.value; + m_origin_snap_offset[2] = print.config().belt_origin_offset_z.value; +} + +void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print) +{ + if (!print.config().belt_printer.value) + return; + + file.write_format("; belt_printer_angle = %.1f\n", print.config().belt_printer_angle.value); + // Shear configs + const auto &full_cfg = print.full_print_config(); + file.write_format("; belt_shear_x = %s\n", full_cfg.opt_serialize("belt_shear_x").c_str()); + file.write_format("; belt_shear_x_angle = %.1f\n", print.config().belt_shear_x_angle.value); + file.write_format("; belt_shear_x_from = %s\n", full_cfg.opt_serialize("belt_shear_x_from").c_str()); + file.write_format("; belt_shear_y = %s\n", full_cfg.opt_serialize("belt_shear_y").c_str()); + file.write_format("; belt_shear_y_angle = %.1f\n", print.config().belt_shear_y_angle.value); + file.write_format("; belt_shear_y_from = %s\n", full_cfg.opt_serialize("belt_shear_y_from").c_str()); + file.write_format("; belt_shear_z = %s\n", full_cfg.opt_serialize("belt_shear_z").c_str()); + file.write_format("; belt_shear_z_angle = %.1f\n", print.config().belt_shear_z_angle.value); + file.write_format("; belt_shear_z_from = %s\n", full_cfg.opt_serialize("belt_shear_z_from").c_str()); + // Scale configs + file.write_format("; belt_scale_x = %s\n", full_cfg.opt_serialize("belt_scale_x").c_str()); + file.write_format("; belt_scale_x_angle = %.1f\n", print.config().belt_scale_x_angle.value); + file.write_format("; belt_scale_y = %s\n", full_cfg.opt_serialize("belt_scale_y").c_str()); + file.write_format("; belt_scale_y_angle = %.1f\n", print.config().belt_scale_y_angle.value); + file.write_format("; belt_scale_z = %s\n", full_cfg.opt_serialize("belt_scale_z").c_str()); + file.write_format("; belt_scale_z_angle = %.1f\n", print.config().belt_scale_z_angle.value); + // Pre-slice remap configs + file.write_format("; belt_preslice_remap_x = %s\n", full_cfg.opt_serialize("belt_preslice_remap_x").c_str()); + file.write_format("; belt_preslice_remap_y = %s\n", full_cfg.opt_serialize("belt_preslice_remap_y").c_str()); + file.write_format("; belt_preslice_remap_z = %s\n", full_cfg.opt_serialize("belt_preslice_remap_z").c_str()); +} + +void BeltGCode::on_set_origin(const PrintObject *obj, const Point &inst_shift) +{ + if (!m_origin_snap[0] && !m_origin_snap[1] && !m_origin_snap[2]) + return; + + auto *belt_writer = dynamic_cast(m_writer.get()); + if (!belt_writer) + return; + + // Clear existing snap so to_machine_coords gives raw machine coords for bbox computation. + for (int a = 0; a < 3; ++a) + belt_writer->set_origin_snap(a, false, 0., 0.); + + // Reconstruct the belt pipeline transform for this object. + Transform3d belt = BeltTransformPipeline::build_forward_transform(m_config); + + // Z-shift + double zs = (obj->belt_min_z() < 0.) ? -obj->belt_min_z() : 0.; + if (zs > 0.) { + Transform3d zsh = Transform3d::Identity(); + zsh.matrix()(2, 3) = zs; + belt = zsh * belt; + } + + // Full transform: belt * trafo_centered + Transform3d full = belt * obj->trafo_centered(); + + // Instance shift in slicer space + global Z offset + Vec3d shift(unscale(inst_shift.x()), + unscale(inst_shift.y()), + obj->belt_global_z_offset()); + + // Compute this instance's machine-space bbox min + BoundingBoxf3 bb = obj->model_object()->raw_bounding_box(); + Vec3d mn = bb.min.cast(), mx = bb.max.cast(); + Vec3d inst_min(std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max()); + for (int i = 0; i < 8; ++i) { + Vec3d c((i & 1) ? mx.x() : mn.x(), + (i & 2) ? mx.y() : mn.y(), + (i & 4) ? mx.z() : mn.z()); + Vec3d mc = belt_writer->to_machine_coords(full * c + shift); + for (int a = 0; a < 3; ++a) + inst_min[a] = std::min(inst_min[a], mc[a]); + } + + // Update writer snap for each enabled axis + for (int a = 0; a < 3; ++a) + belt_writer->set_origin_snap(a, m_origin_snap[a], m_origin_snap_offset[a], inst_min[a]); +} + +} // namespace Slic3r diff --git a/src/libslic3r/BeltGCode.hpp b/src/libslic3r/BeltGCode.hpp new file mode 100644 index 0000000000..682deb7d65 --- /dev/null +++ b/src/libslic3r/BeltGCode.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "GCode.hpp" + +namespace Slic3r { + +// Belt-printer-specific GCode export. +// +// Inherits from GCode and overrides virtual hooks to: +// - Create a BeltGCodeWriter instead of a plain GCodeWriter +// - Write belt configuration to the G-code header +// - Update per-axis origin snap when switching instances +// - Disable arc fitting (G2/G3 not supported on belt printers) +class BeltGCode : public GCode +{ +protected: + void init_belt_writer(Print &print, bool is_bbl_printers) override; + void write_belt_header(GCodeOutputStream &file, const Print &print) override; + void on_set_origin(const PrintObject *obj, const Point &inst_shift) override; + bool should_disable_arc_fitting() const override { return true; } + +private: + // Per-axis origin snap config (set during init, used in on_set_origin). + bool m_origin_snap[3] = {false, false, false}; + double m_origin_snap_offset[3] = {0., 0., 0.}; +}; + +} // namespace Slic3r diff --git a/src/libslic3r/BeltGCodeWriter.cpp b/src/libslic3r/BeltGCodeWriter.cpp new file mode 100644 index 0000000000..645bf85a5b --- /dev/null +++ b/src/libslic3r/BeltGCodeWriter.cpp @@ -0,0 +1,243 @@ +#include "BeltGCodeWriter.hpp" +#include "Geometry.hpp" + +namespace Slic3r { + +// ---- Belt configuration --------------------------------------------------- + +void BeltGCodeWriter::set_belt_angle(double angle_deg) +{ + m_belt_angle_rad = Geometry::deg2rad(angle_deg); +} + +void BeltGCodeWriter::set_axis_remap(int rx, int ry, int rz) +{ + m_remap_x = rx; + m_remap_y = ry; + m_remap_z = rz; +} + +void BeltGCodeWriter::set_build_volume_max(const Vec3d &max) +{ + m_build_vol_max = max; +} + +void BeltGCodeWriter::set_belt_back_transform(const PrintConfig &config) +{ + m_belt_back_transform.init_from_config(config); +} + +void BeltGCodeWriter::set_origin_snap(int axis, bool enable, double offset, double bbox_min) +{ + if (axis >= 0 && axis < 3) { + m_origin_snap[axis] = enable; + m_origin_offset[axis] = offset; + m_origin_bbox_min[axis] = bbox_min; + } +} + +Vec3d BeltGCodeWriter::to_machine_coords(const Vec3d &pos) const +{ + // Step 1: Undo the shear/scale applied during slicing. + Vec3d p = m_belt_back_transform.apply(pos); + // Step 2: Apply axis remapping for the machine's coordinate convention. + // BeltRemapAxis: 0-2 = +X/+Y/+Z, 3-5 = -X/-Y/-Z, 6-8 = Rev X/Y/Z + auto remap = [this, &p](int r) -> double { + int axis = r % 3; + if (r < 3) return p[axis]; + if (r < 6) return -p[axis]; + return m_build_vol_max[axis] - p[axis]; + }; + Vec3d result = { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) }; + // Per-axis origin snap: shift so bbox min on each enabled axis = offset. + for (int i = 0; i < 3; ++i) + if (m_origin_snap[i]) + result[i] -= (m_origin_bbox_min[i] - m_origin_offset[i]); + return result; +} + +// ---- Overridden movement methods ------------------------------------------ + +std::string BeltGCodeWriter::travel_to_xy(const Vec2d &point, const std::string &comment) +{ + m_pos(0) = point(0); + m_pos(1) = point(1); + + this->set_current_position_clear(true); + Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; + + // Belt printer: transform to machine coordinates (XY travel also needs Z due to YZ rotation) + Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); + + GCodeG1Formatter w; + w.emit_xyz(machine); + auto speed = m_is_first_layer + ? this->config.get_abs_value("initial_layer_travel_speed") : this->config.travel_speed.value; + w.emit_f(speed * 60.0); + w.emit_comment(GCodeWriter::full_gcode_comment, comment); + return w.string(); +} + +std::string BeltGCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase) +{ + // Belt printer: force NormalLift since SpiralLift and SlopeLift compute + // slope angles that don't account for the YZ coordinate rotation. + return GCodeWriter::lazy_lift(LiftType::NormalLift, spiral_vase); +} + +std::string BeltGCodeWriter::eager_lift(const LiftType type) +{ + // Belt printer: force NormalLift (SpiralLift/SlopeLift don't account for YZ rotation). + return GCodeWriter::eager_lift(LiftType::NormalLift); +} + +std::string BeltGCodeWriter::_travel_to_z(double z, const std::string &comment) +{ + m_pos(2) = z; + + double speed = this->config.travel_speed_z.value; + if (speed == 0.) { + speed = m_is_first_layer ? this->config.get_abs_value("initial_layer_travel_speed") + : this->config.travel_speed.value; + } + + // Belt printer: a Z-only move in slicing frame needs to emit both Y and Z in machine coords. + Vec3d machine = to_machine_coords(Vec3d(m_pos.x() - m_x_offset, m_pos.y() - m_y_offset, z)); + + GCodeG1Formatter w; + w.emit_xyz(machine); + w.emit_f(speed * 60.0); + w.emit_comment(GCodeWriter::full_gcode_comment, comment); + return w.string(); +} + +std::string BeltGCodeWriter::extrude_to_xy(const Vec2d &point, double dE, const std::string &comment, bool force_no_extrusion) +{ + m_pos(0) = point(0); + m_pos(1) = point(1); + if (std::abs(dE) <= std::numeric_limits::epsilon()) + force_no_extrusion = true; + + if (!force_no_extrusion) + filament()->extrude(dE); + + Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; + + // Belt printer: transform and emit XYZ (Y and Z are coupled) + Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); + + GCodeG1Formatter w; + w.emit_xyz(machine); + if (!force_no_extrusion) + w.emit_e(filament()->E()); + w.emit_comment(GCodeWriter::full_gcode_comment, comment); + return w.string(); +} + +std::string BeltGCodeWriter::extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment, bool force_no_extrusion) +{ + m_pos = point; + m_lifted = 0; + if (!force_no_extrusion) + filament()->extrude(dE); + + Vec3d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset, point(2) }; + point_on_plate = to_machine_coords(point_on_plate); + + GCodeG1Formatter w; + w.emit_xyz(point_on_plate); + if (!force_no_extrusion) + w.emit_e(filament()->E()); + w.emit_comment(GCodeWriter::full_gcode_comment, comment); + return w.string(); +} + +std::string BeltGCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &comment, bool force_z) +{ + // Belt-specific override of travel_to_xyz. + // Key differences from base: + // 1. All coordinates go through to_machine_coords() + // 2. Always emit full XYZ (can't split XY and Z due to coupling) + // 3. Lift type forced to NormalLift (handled by lazy_lift/eager_lift overrides) + + Vec3d dest_point = point; + auto travel_speed = + m_is_first_layer ? this->config.get_abs_value("initial_layer_travel_speed") : this->config.travel_speed.value; + + // Handle pending z_hop + if (std::abs(m_to_lift) > EPSILON) { + assert(std::abs(m_lifted) < EPSILON); + if ((!this->is_current_position_clear() || m_pos != dest_point) && + m_to_lift + m_pos(2) > point(2)) { + m_lifted = m_to_lift + m_pos(2) - point(2); + dest_point(2) = m_to_lift + m_pos(2); + } + m_to_lift = 0.; + + std::string slop_move; + Vec3d source = { m_pos(0) - m_x_offset, m_pos(1) - m_y_offset, m_pos(2) }; + Vec3d target = { dest_point(0) - m_x_offset, dest_point(1) - m_y_offset, dest_point(2) }; + Vec3d delta = target - source; + Vec2d delta_no_z = { delta(0), delta(1) }; + + if (delta(2) > 0 && delta_no_z.norm() != 0.0f) { + // Belt: SpiralLift and SlopeLift are disabled (lazy_lift forces NormalLift), + // but handle NormalLift and fallthrough. + if (m_to_lift_type == LiftType::SlopeLift && + this->is_current_position_clear() && + atan2(delta(2), delta_no_z.norm()) < this->filament()->travel_slope()) { + Vec2d temp = delta_no_z.normalized() * delta(2) / tan(this->filament()->travel_slope()); + Vec3d slope_top_point = Vec3d(temp(0), temp(1), delta(2)) + source; + slope_top_point = to_machine_coords(slope_top_point); + GCodeG1Formatter w0; + w0.emit_xyz(slope_top_point); + w0.emit_f(travel_speed * 60.0); + w0.emit_comment(GCodeWriter::full_gcode_comment, comment); + slop_move = w0.string(); + } + else if (m_to_lift_type == LiftType::NormalLift) { + slop_move = _travel_to_z(target.z(), "normal lift Z"); + } + } + + std::string xy_z_move; + { + Vec3d emit_target = to_machine_coords(target); + GCodeG1Formatter w0; + // Belt mode: always emit full XYZ since Y and Z are coupled + w0.emit_xyz(emit_target); + w0.emit_f(travel_speed * 60.0); + w0.emit_comment(GCodeWriter::full_gcode_comment, comment); + xy_z_move = w0.string(); + } + m_pos = dest_point; + this->set_current_position_clear(true); + return slop_move + xy_z_move; + } + else if (!force_z && !this->will_move_z(point(2))) { + double nominal_z = m_pos(2) - m_lifted; + m_lifted -= (point(2) - nominal_z); + if (std::abs(m_lifted) < EPSILON) + m_lifted = 0.; + this->set_current_position_clear(true); + return this->travel_to_xy(to_2d(point)); + } + else { + m_lifted = 0; + } + + Vec3d point_on_plate = { dest_point(0) - m_x_offset, dest_point(1) - m_y_offset, dest_point(2) }; + point_on_plate = to_machine_coords(point_on_plate); + + // Belt mode: always emit full XYZ + GCodeG1Formatter w; + w.emit_xyz(point_on_plate); + w.emit_f(this->config.travel_speed.value * 60.0); + w.emit_comment(GCodeWriter::full_gcode_comment, comment); + + m_pos = dest_point; + this->set_current_position_clear(true); + return w.string(); +} + +} // namespace Slic3r diff --git a/src/libslic3r/BeltGCodeWriter.hpp b/src/libslic3r/BeltGCodeWriter.hpp new file mode 100644 index 0000000000..727135b578 --- /dev/null +++ b/src/libslic3r/BeltGCodeWriter.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include "GCodeWriter.hpp" +#include "GCode/BeltBackTransform.hpp" + +namespace Slic3r { + +// Belt-printer-specific GCode writer. +// +// Inherits from GCodeWriter and overrides movement methods to apply +// coordinate transformation (back-transform, axis remap, origin snap) +// and emit coupled XYZ moves (Y and Z are coupled due to belt tilt). +class BeltGCodeWriter : public GCodeWriter +{ +public: + BeltGCodeWriter() : GCodeWriter() {} + + // Belt configuration + void set_belt_angle(double angle_deg); + bool is_belt_printer() const { return m_belt_angle_rad != 0.; } + void set_axis_remap(int rx, int ry, int rz); + void set_build_volume_max(const Vec3d &max); + void set_belt_back_transform(const PrintConfig &config); + void set_origin_snap(int axis, bool enable, double offset, double bbox_min); + Vec3d to_machine_coords(const Vec3d &pos) const; + + // Overridden movement methods + std::string travel_to_xy(const Vec2d &point, const std::string &comment = std::string()) override; + std::string travel_to_xyz(const Vec3d &point, const std::string &comment = std::string(), bool force_z = false) override; + std::string extrude_to_xy(const Vec2d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false) override; + std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false) override; + std::string lazy_lift(LiftType lift_type = LiftType::NormalLift, bool spiral_vase = false) override; + std::string eager_lift(const LiftType type) override; + +protected: + std::string _travel_to_z(double z, const std::string &comment) override; + +private: + double m_belt_angle_rad = 0.; + int m_remap_x = 0; + int m_remap_y = 1; + int m_remap_z = 2; + Vec3d m_build_vol_max = Vec3d::Zero(); + BeltBackTransform m_belt_back_transform; + bool m_origin_snap[3] = {false, false, false}; + double m_origin_offset[3] = {0., 0., 0.}; + double m_origin_bbox_min[3] = {0., 0., 0.}; +}; + +} // namespace Slic3r diff --git a/src/libslic3r/BeltSliceStrategy.cpp b/src/libslic3r/BeltSliceStrategy.cpp new file mode 100644 index 0000000000..b3adeec64a --- /dev/null +++ b/src/libslic3r/BeltSliceStrategy.cpp @@ -0,0 +1,62 @@ +#include "BeltSliceStrategy.hpp" + +#include + +namespace Slic3r { + +std::unique_ptr BeltSliceStrategy::create(const PrintConfig &config) +{ + if (!config.belt_printer.value) + return nullptr; + return std::unique_ptr(new BeltSliceStrategy(config)); +} + +BeltSliceStrategy::BeltSliceStrategy(const PrintConfig &config) +{ + m_has_remap = BeltTransformPipeline::has_preslice_remap(config); + if (m_has_remap) + m_pre_remap = BeltTransformPipeline::build_preslice_remap(config); + + m_shear = BeltTransformPipeline::build_shear_matrix(config, &m_has_shear); + m_scale = BeltTransformPipeline::build_scale_matrix(config, &m_has_scale); +} + +void BeltSliceStrategy::apply_to_trafo(Transform3d &trafo, + const ModelVolumePtrs &model_volumes, + double *out_belt_min_z) const +{ + // Step 1: Pre-slice axis remap. + if (m_has_remap) + trafo = m_pre_remap * trafo; + + // Step 2: Shear + scale. + if (m_has_shear || m_has_scale) { + Transform3d belt_xform = Transform3d::Identity(); + belt_xform.linear() = m_scale * m_shear; + trafo = belt_xform * trafo; + } + + // Step 3: Z-shift — detect if mesh clips below build plate after transforms. + if (m_has_remap || m_has_shear || m_has_scale) { + double min_z = std::numeric_limits::max(); + for (const ModelVolume *mv : model_volumes) { + if (!mv->is_model_part()) continue; + for (const stl_vertex &v : mv->mesh().its.vertices) { + Vec3d pt = trafo * v.cast(); + min_z = std::min(min_z, pt.z()); + } + } + double belt_z_shift_val = (min_z < 0. && min_z != std::numeric_limits::max()) ? -min_z : 0.; + BOOST_LOG_TRIVIAL(warning) << "Belt Z-shift: min_z=" << min_z + << " z_shift=" << belt_z_shift_val; + if (belt_z_shift_val > 0.) { + Transform3d z_shift = Transform3d::Identity(); + z_shift.matrix()(2, 3) = belt_z_shift_val; + trafo = z_shift * trafo; + } + if (out_belt_min_z) + *out_belt_min_z = (min_z != std::numeric_limits::max()) ? min_z : 0.; + } +} + +} // namespace Slic3r diff --git a/src/libslic3r/BeltSliceStrategy.hpp b/src/libslic3r/BeltSliceStrategy.hpp new file mode 100644 index 0000000000..bf19243fd2 --- /dev/null +++ b/src/libslic3r/BeltSliceStrategy.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include "libslic3r.h" +#include "Point.hpp" +#include "BeltTransform.hpp" +#include "PrintConfig.hpp" +#include "Model.hpp" + +#include +#include + +namespace Slic3r { + +// Belt printer pre-slice transform strategy. +// +// Encapsulates the pre-remap, shear, scale, and Z-shift transforms +// that are applied to model geometry before slicing on belt printers. +// Used by PrintObjectSlice.cpp to isolate belt-specific logic from +// the slicing pipeline. +class BeltSliceStrategy +{ +public: + // Create a strategy if belt_printer is enabled; returns nullptr otherwise. + static std::unique_ptr create(const PrintConfig &config); + + // Apply belt transforms to the slicing trafo. + // Modifies trafo in-place: trafo = z_shift * scale * shear * pre_remap * trafo + // Scans all model_part volumes to detect minimum Z and add z-shift if needed. + // Sets *out_belt_min_z to the minimum Z of the mesh after transforms. + void apply_to_trafo(Transform3d &trafo, + const ModelVolumePtrs &model_volumes, + double *out_belt_min_z) const; + +private: + explicit BeltSliceStrategy(const PrintConfig &config); + + bool m_has_remap = false; + bool m_has_shear = false; + bool m_has_scale = false; + Transform3d m_pre_remap = Transform3d::Identity(); + Matrix3d m_shear = Matrix3d::Identity(); + Matrix3d m_scale = Matrix3d::Identity(); +}; + +} // namespace Slic3r diff --git a/src/libslic3r/BeltTransform.cpp b/src/libslic3r/BeltTransform.cpp new file mode 100644 index 0000000000..db7eea7d80 --- /dev/null +++ b/src/libslic3r/BeltTransform.cpp @@ -0,0 +1,246 @@ +#include "BeltTransform.hpp" +#include "Model.hpp" + +#include + +namespace Slic3r { + +// ---- Matrix builders ------------------------------------------------------ + +Transform3d BeltTransformPipeline::build_preslice_remap(const PrintConfig &config) +{ + Transform3d pre_remap = Transform3d::Identity(); + if (!has_preslice_remap(config)) + return pre_remap; + + int pre_rx = int(config.belt_preslice_remap_x.value); + int pre_ry = int(config.belt_preslice_remap_y.value); + int pre_rz = int(config.belt_preslice_remap_z.value); + + // Each remap value selects a source axis and sign. + auto remap_column = [](int r) -> Vec3d { + int axis = r % 3; + Vec3d col = Vec3d::Zero(); + if (r < 3) col[axis] = 1.0; // +axis + else if (r < 6) col[axis] = -1.0; // -axis + else col[axis] = -1.0; // Rev: max - pos = -(pos - max) + return col; + }; + + Matrix3d remap_lin; + remap_lin.col(0) = remap_column(pre_rx); + remap_lin.col(1) = remap_column(pre_ry); + remap_lin.col(2) = remap_column(pre_rz); + pre_remap.linear() = remap_lin; + + // Translation for Rev modes (needs build volume extents). + if (pre_rx >= 6 || pre_ry >= 6 || pre_rz >= 6) { + BoundingBoxf bbox_bed(config.printable_area.values); + Vec3d vol_max(bbox_bed.max.x(), bbox_bed.max.y(), + config.printable_height.value); + Vec3d remap_trans = Vec3d::Zero(); + auto add_rev = [&](int r, int out) { + if (r >= 6) remap_trans[out] = vol_max[r % 3]; + }; + add_rev(pre_rx, 0); + add_rev(pre_ry, 1); + add_rev(pre_rz, 2); + pre_remap.translation() = remap_trans; + } + + return pre_remap; +} + +Matrix3d BeltTransformPipeline::build_shear_matrix(const PrintConfig &config, bool *has_shear_out) +{ + struct AxisShear { BeltShearMode mode; double angle; int from; }; + AxisShear axes[3] = { + { config.belt_shear_x.value, config.belt_shear_x_angle.value, int(config.belt_shear_x_from.value) }, + { config.belt_shear_y.value, config.belt_shear_y_angle.value, int(config.belt_shear_y_from.value) }, + { config.belt_shear_z.value, config.belt_shear_z_angle.value, int(config.belt_shear_z_from.value) }, + }; + + Matrix3d shear = Matrix3d::Identity(); + bool active = false; + for (int row = 0; row < 3; ++row) { + if (axes[row].mode != BeltShearMode::None) { + double factor = compute_shear_factor(axes[row].mode, axes[row].angle); + if (std::abs(factor) > EPSILON) { + shear(row, axes[row].from) += factor; + active = true; + } + } + } + if (has_shear_out) *has_shear_out = active; + return shear; +} + +Matrix3d BeltTransformPipeline::build_scale_matrix(const PrintConfig &config, bool *has_scale_out) +{ + double sx = compute_scale_factor(config.belt_scale_x.value, config.belt_scale_x_angle.value); + double sy = compute_scale_factor(config.belt_scale_y.value, config.belt_scale_y_angle.value); + double sz = compute_scale_factor(config.belt_scale_z.value, config.belt_scale_z_angle.value); + + bool active = (std::abs(sx - 1.) > EPSILON || + std::abs(sy - 1.) > EPSILON || + std::abs(sz - 1.) > EPSILON); + + Matrix3d scale = Matrix3d::Identity(); + if (active) { + scale(0, 0) = sx; + scale(1, 1) = sy; + scale(2, 2) = sz; + } + if (has_scale_out) *has_scale_out = active; + return scale; +} + +Transform3d BeltTransformPipeline::build_forward_transform(const PrintConfig &config) +{ + Transform3d pre_remap = build_preslice_remap(config); + bool shear_active = false; + Matrix3d shear = build_shear_matrix(config, &shear_active); + bool scale_active = false; + Matrix3d scale = build_scale_matrix(config, &scale_active); + + // Pipeline: scale * shear * pre_remap + Transform3d combined = Transform3d::Identity(); + combined.linear() = scale * shear; + combined = combined * pre_remap; + return combined; +} + +// ---- Bounding box remap --------------------------------------------------- + +BoundingBoxf3 BeltTransformPipeline::remap_bbox(const BoundingBoxf3 &bb, const PrintConfig &config) +{ + int pre_rx = int(config.belt_preslice_remap_x.value); + int pre_ry = int(config.belt_preslice_remap_y.value); + int pre_rz = int(config.belt_preslice_remap_z.value); + + if (pre_rx == int(BeltRemapAxis::PosX) && + pre_ry == int(BeltRemapAxis::PosY) && + pre_rz == int(BeltRemapAxis::PosZ)) + return bb; // Identity remap. + + auto remap_coord = [](int r, const Vec3d &v) -> double { + int axis = r % 3; + if (r < 3) return v[axis]; + return -v[axis]; + }; + + Vec3d mn = bb.min.cast(), mx = bb.max.cast(); + BoundingBoxf3 rbb; + for (int i = 0; i < 8; ++i) { + Vec3d c((i & 1) ? mx.x() : mn.x(), + (i & 2) ? mx.y() : mn.y(), + (i & 4) ? mx.z() : mn.z()); + Vec3d rc(remap_coord(pre_rx, c), remap_coord(pre_ry, c), remap_coord(pre_rz, c)); + if (i == 0) rbb = BoundingBoxf3(rc, rc); + else rbb.merge(rc); + } + return rbb; +} + +BoundingBoxf3 BeltTransformPipeline::remap_bbox(const ModelObject &model_object, const PrintConfig &config) +{ + return remap_bbox(model_object.raw_bounding_box(), config); +} + +// ---- Belt floor parameters ------------------------------------------------ + +// Shared implementation for both PrintConfig and DynamicPrintConfig. +// Template avoids duplicating the math for the two config types. +namespace { + +template +BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl( + const Config &config, const BoundingBoxf3 &bb, double original_height) +{ + BeltTransformPipeline::BeltHeightResult result; + result.object_height = original_height; + + // Extract Z-axis shear/scale config. + BeltShearMode z_shear_mode; + double z_shear_angle; + BeltScaleMode z_scale_mode; + double z_scale_angle; + int z_shear_from; + + if constexpr (std::is_same_v) { + z_shear_mode = config.belt_shear_z.value; + z_shear_angle = config.belt_shear_z_angle.value; + z_scale_mode = config.belt_scale_z.value; + z_scale_angle = config.belt_scale_z_angle.value; + z_shear_from = int(config.belt_shear_z_from.value); + } else { + // DynamicPrintConfig path + auto get_shear = [&](const char *key) { + auto *opt = config.template option>(key); + return opt ? opt->value : BeltShearMode::None; + }; + auto get_scale = [&](const char *key) { + auto *opt = config.template option>(key); + return opt ? opt->value : BeltScaleMode::None; + }; + auto get_float = [&](const char *key) { + auto *opt = config.template option(key); + return opt ? opt->value : 45.0; + }; + auto get_axis = [&](const char *key) { + auto *opt = config.template option>(key); + return opt ? int(opt->value) : 1; + }; + z_shear_mode = get_shear("belt_shear_z"); + z_shear_angle = get_float("belt_shear_z_angle"); + z_scale_mode = get_scale("belt_scale_z"); + z_scale_angle = get_float("belt_scale_z_angle"); + z_shear_from = get_axis("belt_shear_z_from"); + } + + bool has_z_shear = z_shear_mode != BeltShearMode::None; + bool has_z_scale = z_scale_mode != BeltScaleMode::None; + + if (!has_z_shear && !has_z_scale) + return result; + + double shear_factor = has_z_shear + ? BeltTransformPipeline::compute_shear_factor(z_shear_mode, z_shear_angle) : 0.; + double scale_z = BeltTransformPipeline::compute_scale_factor(z_scale_mode, z_scale_angle); + + if (has_z_shear && std::abs(shear_factor) > EPSILON) { + int from = z_shear_from; + double min_rz = std::numeric_limits::max(); + double max_rz = std::numeric_limits::lowest(); + for (double vz : {bb.min.z(), bb.max.z()}) + for (double vs : {bb.min(from), bb.max(from)}) { + double new_z = scale_z * (vz + shear_factor * vs); + min_rz = std::min(min_rz, new_z); + max_rz = std::max(max_rz, new_z); + } + result.object_height = max_rz - min_rz; + result.floor_params.shear_factor = shear_factor; + result.floor_params.from_axis = from; + result.floor_params.z_shift = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.); + } else { + result.object_height = original_height * scale_z; + } + + return result; +} + +} // anonymous namespace + +BeltTransformPipeline::BeltHeightResult BeltTransformPipeline::compute_belt_height_and_floor( + const PrintConfig &config, const BoundingBoxf3 &remapped_bbox, double original_height) +{ + return compute_belt_height_and_floor_impl(config, remapped_bbox, original_height); +} + +BeltTransformPipeline::BeltHeightResult BeltTransformPipeline::compute_belt_height_and_floor( + const DynamicPrintConfig &config, const BoundingBoxf3 &remapped_bbox, double original_height) +{ + return compute_belt_height_and_floor_impl(config, remapped_bbox, original_height); +} + +} // namespace Slic3r diff --git a/src/libslic3r/BeltTransform.hpp b/src/libslic3r/BeltTransform.hpp new file mode 100644 index 0000000000..b9a60473e5 --- /dev/null +++ b/src/libslic3r/BeltTransform.hpp @@ -0,0 +1,146 @@ +#pragma once + +#include "libslic3r.h" +#include "Point.hpp" +#include "BoundingBox.hpp" +#include "PrintConfig.hpp" +#include "Geometry.hpp" + +#include + +namespace Slic3r { + +class ModelObject; + +// Shared belt-printer transform math. +// +// The pre-slice pipeline applied in PrintObjectSlice.cpp is: +// trafo_out = z_shift * scale * shear * pre_remap * trafo_in +// +// This class provides the building blocks so every call site uses the +// same implementation. z_shift is object-dependent (computed from mesh +// vertex bounds) and is NOT included in build_forward_transform(). +class BeltTransformPipeline +{ +public: + // ---- Pure math helpers ------------------------------------------------ + + static double compute_shear_factor(BeltShearMode mode, double angle_deg) + { + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad); + double cos_a = std::cos(angle_rad); + switch (mode) { + case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; + case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; + case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; + case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; + default: return 0.; + } + } + + static double compute_scale_factor(BeltScaleMode mode, double angle_deg) + { + if (mode == BeltScaleMode::None) return 1.; + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad); + double cos_a = std::cos(angle_rad); + switch (mode) { + case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; + case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; + case BeltScaleMode::Sin: return sin_a; + case BeltScaleMode::Cos: return cos_a; + default: return 1.; + } + } + + // ---- Identity checks -------------------------------------------------- + + static bool has_preslice_remap(const PrintConfig &config) + { + return int(config.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || + int(config.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || + int(config.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ); + } + + // Overload accepting DynamicPrintConfig (used in static slicing_parameters). + static bool has_preslice_remap(const DynamicPrintConfig &config) + { + auto get_int = [&](const char *key) -> int { + auto *opt = config.option>(key); + return opt ? int(opt->value) : 0; + }; + return get_int("belt_preslice_remap_x") != int(BeltRemapAxis::PosX) || + get_int("belt_preslice_remap_y") != int(BeltRemapAxis::PosY) || + get_int("belt_preslice_remap_z") != int(BeltRemapAxis::PosZ); + } + + static bool has_shear(const PrintConfig &config) + { + return config.belt_shear_x.value != BeltShearMode::None || + config.belt_shear_y.value != BeltShearMode::None || + config.belt_shear_z.value != BeltShearMode::None; + } + + static bool has_scale(const PrintConfig &config) + { + double sx = compute_scale_factor(config.belt_scale_x.value, config.belt_scale_x_angle.value); + double sy = compute_scale_factor(config.belt_scale_y.value, config.belt_scale_y_angle.value); + double sz = compute_scale_factor(config.belt_scale_z.value, config.belt_scale_z_angle.value); + return std::abs(sx - 1.) > EPSILON || + std::abs(sy - 1.) > EPSILON || + std::abs(sz - 1.) > EPSILON; + } + + // ---- Matrix builders -------------------------------------------------- + + // Build the pre-slice axis remap transform (includes Rev-mode translation). + static Transform3d build_preslice_remap(const PrintConfig &config); + + // Build the 3x3 shear matrix. Returns Identity if no shear is active. + // Also sets has_shear_out if non-null. + static Matrix3d build_shear_matrix(const PrintConfig &config, bool *has_shear_out = nullptr); + + // Build the 3x3 diagonal scale matrix. Returns Identity if no scale. + // Also sets has_scale_out if non-null. + static Matrix3d build_scale_matrix(const PrintConfig &config, bool *has_scale_out = nullptr); + + // Combined forward transform: scale * shear * pre_remap. + // Does NOT include the per-object Z-shift. + static Transform3d build_forward_transform(const PrintConfig &config); + + // ---- Bounding box remap ----------------------------------------------- + + // Remap a bounding box through the pre-slice axis remap. + // Returns the original bbox if remap is identity. + static BoundingBoxf3 remap_bbox(const BoundingBoxf3 &bb, const PrintConfig &config); + static BoundingBoxf3 remap_bbox(const ModelObject &model_object, const PrintConfig &config); + + // ---- Belt floor parameters -------------------------------------------- + + struct BeltFloorParams { + double shear_factor = 0.0; + int from_axis = 1; + double z_shift = 0.0; + }; + + // Result of computing belt height + floor params. + struct BeltHeightResult { + double object_height; // Effective object height after shear/scale + BeltFloorParams floor_params; + }; + + // Compute effective object height and belt floor parameters from config + // and pre-remapped bounding box. original_height is the input height + // (bb.size().z() or model_object.max_z()). + static BeltHeightResult compute_belt_height_and_floor( + const PrintConfig &config, const BoundingBoxf3 &remapped_bbox, + double original_height); + + // Overload for DynamicPrintConfig (used by static slicing_parameters). + static BeltHeightResult compute_belt_height_and_floor( + const DynamicPrintConfig &config, const BoundingBoxf3 &remapped_bbox, + double original_height); +}; + +} // namespace Slic3r diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index c2d92ee800..a3e813b1da 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -79,6 +79,14 @@ set(lisbslic3r_sources BoundingBox.hpp BridgeDetector.cpp BridgeDetector.hpp + BeltGCode.cpp + BeltGCode.hpp + BeltGCodeWriter.cpp + BeltGCodeWriter.hpp + BeltSliceStrategy.cpp + BeltSliceStrategy.hpp + BeltTransform.cpp + BeltTransform.hpp Brim.cpp BrimEarsPoint.hpp Brim.hpp @@ -417,6 +425,8 @@ set(lisbslic3r_sources SlicingAdaptive.hpp Slicing.cpp Slicing.hpp + Support/BeltFloorContext.cpp + Support/BeltFloorContext.hpp Support/SupportCommon.cpp Support/SupportCommon.hpp Support/SupportLayer.hpp diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 8391f9b820..c8b6e540cf 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -8,6 +8,7 @@ #include "Exception.hpp" #include "ExtrusionEntity.hpp" #include "EdgeGrid.hpp" +#include "BeltTransform.hpp" #include "Geometry.hpp" #include "Geometry/ConvexHull.hpp" #include "GCode/PrintExtents.hpp" @@ -795,7 +796,7 @@ static std::vector get_path_of_change_filament(const Print& print) //BBS: if needed, write the gcode_label_objects_end then priming tower, if the retract, didn't did it. std::string object_end_label_temp; - gcodegen.m_writer.add_object_end_labels(object_end_label_temp); + gcodegen.m_writer->add_object_end_labels(object_end_label_temp); // Process the custom change_filament_gcode. If it is empty, provide a simple Tn command to change the filament. // Otherwise, leave control to the user completely. @@ -839,7 +840,7 @@ static std::vector get_path_of_change_filament(const Print& print) // config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); // BBS { - GCodeWriter& gcode_writer = gcodegen.m_writer; + GCodeWriter& gcode_writer = *gcodegen.m_writer; FullPrintConfig& full_config = gcodegen.m_config; // set volumetric speed of outer wall ,ignore per obejct,just use default setting @@ -1573,8 +1574,8 @@ static std::vector get_path_of_change_filament(const Print& print) const std::vector ColorPrintColors::Colors = { "#C0392B", "#E67E22", "#F1C40F", "#27AE60", "#1ABC9C", "#2980B9", "#9B59B6" }; -#define EXTRUDER_CONFIG(OPT) m_config.OPT.get_at(m_writer.filament()->extruder_id()) -#define FILAMENT_CONFIG(OPT) m_config.OPT.get_at(m_writer.filament()->id()) +#define EXTRUDER_CONFIG(OPT) m_config.OPT.get_at(m_writer->filament()->extruder_id()) +#define FILAMENT_CONFIG(OPT) m_config.OPT.get_at(m_writer->filament()->id()) void GCode::PlaceholderParserIntegration::reset() { @@ -1687,7 +1688,7 @@ void GCode::PlaceholderParserIntegration::validate_output_vector_variables() // Collect pairs of object_layer + support_layer sorted by print_z. // object_layer & support_layer are considered to be on the same print_z, if they are not further than EPSILON. -std::vector GCode::collect_layers_to_print(const PrintObject& object) +std::vector GCode::collect_layers_to_print(const PrintObject& object, bool skip_empty_first_layer) { std::vector layers_to_print; layers_to_print.reserve(object.layers().size() + object.support_layers().size()); @@ -1753,8 +1754,7 @@ std::vector GCode::collect_layers_to_print(const PrintObjec // an error. In global shear mode the object may also start above // Z=0 on the tilted belt surface. if (layers_to_print.size() == 1u) { - bool skip_empty_check = object.print()->config().belt_printer.value; - if (!has_extrusions && !skip_empty_check) + if (!has_extrusions && !skip_empty_first_layer) throw Slic3r::SlicingError(_(L("One object has an empty first layer and can't be printed. Please Cut the bottom or enable supports.")), object.id().id); } @@ -1827,7 +1827,7 @@ std::vector>> GCode::collec for (size_t i = 0; i < print.objects().size(); ++i) { try { - per_object[i] = collect_layers_to_print(*print.objects()[i]); + per_object[i] = collect_layers_to_print(*print.objects()[i], print.config().belt_printer.value); } catch (const Slic3r::SlicingError &e) { errors.push_back(e); continue; @@ -2016,7 +2016,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu BOOST_LOG_TRIVIAL(info) << boost::format("Will export G-code to %1% soon")%path; GCodeProcessor::s_IsBBLPrinter = print->is_BBL_printer(); - m_writer.set_is_bbl_machine(print->is_BBL_printer()); + m_writer->set_is_bbl_machine(print->is_BBL_printer()); print->set_started(psGCodeExport); // check if any custom gcode contains keywords used by the gcode processor to @@ -2113,7 +2113,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu m_processor.result().support_traditional_timelapse = m_support_traditional_timelapse; bool activate_long_retraction_when_cut = false; - for (const auto& filament : m_writer.extruders()) + for (const auto& filament : m_writer->extruders()) activate_long_retraction_when_cut |= ( m_config.long_retractions_when_cut.get_at(filament.id()) && m_config.retraction_distances_when_cut.get_at(filament.id()) > 0 @@ -2144,7 +2144,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu m_processor.finalize(true); // DoExport::update_print_estimated_times_stats(m_processor, print->m_print_statistics); - DoExport::update_print_estimated_stats(m_processor, m_writer.extruders(), print->m_print_statistics, print->config()); + DoExport::update_print_estimated_stats(m_processor, m_writer->extruders(), print->m_print_statistics, print->config()); if (result != nullptr) { *result = std::move(m_processor.extract_result()); // set the filename to the correct value @@ -2404,7 +2404,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato PROFILE_FUNC(); m_print = &print; - m_timelapse_pos_picker.init(&print,m_writer.get_xy_offset().cast()); + m_timelapse_pos_picker.init(&print,m_writer->get_xy_offset().cast()); // modifies m_silent_time_estimator_enabled DoExport::init_gcode_processor(print.config(), m_processor, m_silent_time_estimator_enabled); @@ -2420,29 +2420,10 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_fan_mover.release(); - m_writer.set_is_bbl_machine(is_bbl_printers); + m_writer->set_is_bbl_machine(is_bbl_printers); - // Belt printer: initialize coordinate transformation and axis remap on the writer. - if (print.config().belt_printer.value) { - m_writer.set_belt_angle(print.config().belt_printer_angle.value); - m_writer.set_axis_remap( - int(print.config().belt_gcode_remap_x.value), - int(print.config().belt_gcode_remap_y.value), - int(print.config().belt_gcode_remap_z.value)); - // Build volume extents for Rev remap mode. - BoundingBoxf bbox_bed(print.config().printable_area.values); - m_writer.set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(), print.config().printable_height.value)); - // Initialize the back-transform that undoes slicing shear/scale. - m_writer.set_belt_back_transform(print.config()); - // Per-axis origin snap: store config; actual snap is computed - // per-instance in update_origin_snap() called from set_origin(). - m_origin_snap[0] = print.config().belt_origin_snap_x.value; - m_origin_snap[1] = print.config().belt_origin_snap_y.value; - m_origin_snap[2] = print.config().belt_origin_snap_z.value; - m_origin_snap_offset[0] = print.config().belt_origin_offset_x.value; - m_origin_snap_offset[1] = print.config().belt_origin_offset_y.value; - m_origin_snap_offset[2] = print.config().belt_origin_offset_z.value; - } + // Belt printer: initialize belt-specific writer via virtual hook. + this->init_belt_writer(print, is_bbl_printers); // How many times will be change_layer() called? // change_layer() in turn increments the progress bar status. @@ -2533,31 +2514,8 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.write_format("; HEADER_BLOCK_START\n"); // Write information on the generator. file.write_format("; generated by %s on %s\n", Slic3r::header_slic3r_generated().c_str(), Slic3r::Utils::local_timestamp().c_str()); - // Belt printer: embed angle and transform configs in header for G-code processor detection. - if (print.config().belt_printer.value) { - file.write_format("; belt_printer_angle = %.1f\n", print.config().belt_printer_angle.value); - // Shear configs - const auto &full_cfg = print.full_print_config(); - file.write_format("; belt_shear_x = %s\n", full_cfg.opt_serialize("belt_shear_x").c_str()); - file.write_format("; belt_shear_x_angle = %.1f\n", print.config().belt_shear_x_angle.value); - file.write_format("; belt_shear_x_from = %s\n", full_cfg.opt_serialize("belt_shear_x_from").c_str()); - file.write_format("; belt_shear_y = %s\n", full_cfg.opt_serialize("belt_shear_y").c_str()); - file.write_format("; belt_shear_y_angle = %.1f\n", print.config().belt_shear_y_angle.value); - file.write_format("; belt_shear_y_from = %s\n", full_cfg.opt_serialize("belt_shear_y_from").c_str()); - file.write_format("; belt_shear_z = %s\n", full_cfg.opt_serialize("belt_shear_z").c_str()); - file.write_format("; belt_shear_z_angle = %.1f\n", print.config().belt_shear_z_angle.value); - file.write_format("; belt_shear_z_from = %s\n", full_cfg.opt_serialize("belt_shear_z_from").c_str()); - // Scale configs - file.write_format("; belt_scale_x = %s\n", full_cfg.opt_serialize("belt_scale_x").c_str()); - file.write_format("; belt_scale_x_angle = %.1f\n", print.config().belt_scale_x_angle.value); - file.write_format("; belt_scale_y = %s\n", full_cfg.opt_serialize("belt_scale_y").c_str()); - file.write_format("; belt_scale_y_angle = %.1f\n", print.config().belt_scale_y_angle.value); - file.write_format("; belt_scale_z = %s\n", full_cfg.opt_serialize("belt_scale_z").c_str()); - file.write_format("; belt_scale_z_angle = %.1f\n", print.config().belt_scale_z_angle.value); - file.write_format("; belt_preslice_remap_x = %s\n", full_cfg.opt_serialize("belt_preslice_remap_x").c_str()); - file.write_format("; belt_preslice_remap_y = %s\n", full_cfg.opt_serialize("belt_preslice_remap_y").c_str()); - file.write_format("; belt_preslice_remap_z = %s\n", full_cfg.opt_serialize("belt_preslice_remap_z").c_str()); - } + // Belt printer: embed angle and transform configs in header via virtual hook. + this->write_belt_header(file, print); if (is_bbl_printers) file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str()); //BBS: total layer number @@ -2804,13 +2762,13 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Disable fan. if (m_config.auxiliary_fan.value && print.config().close_fan_the_first_x_layers.get_at(initial_extruder_id)) { - file.write(m_writer.set_fan(0)); + file.write(m_writer->set_fan(0)); //BBS: disable additional fan - file.write(m_writer.set_additional_fan(0)); + file.write(m_writer->set_additional_fan(0)); } // Update output variables after the extruders were initialized. - m_placeholder_parser_integration.init(m_writer); + m_placeholder_parser_integration.init(*m_writer); // Let the start-up script prime the 1st printing tool. @@ -2883,7 +2841,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato { BoundingBoxf bbox_bed(print.config().printable_area.values); - Vec2f plate_offset = m_writer.get_xy_offset(); + Vec2f plate_offset = m_writer->get_xy_offset(); this->placeholder_parser().set("print_bed_min", new ConfigOptionFloats({ bbox_bed.min.x(), bbox_bed.min.y()})); this->placeholder_parser().set("print_bed_max", new ConfigOptionFloats({ bbox_bed.max.x(), bbox_bed.max.y()})); this->placeholder_parser().set("print_bed_size", new ConfigOptionFloats({ bbox_bed.size().x(), bbox_bed.size().y() })); @@ -2985,7 +2943,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } bool activate_chamber_temp_control = false; auto max_chamber_temp = 0; - for (const auto &extruder : m_writer.extruders()) { + for (const auto &extruder : m_writer->extruders()) { activate_chamber_temp_control |= m_config.activate_chamber_temp_control.get_at(extruder.id()); max_chamber_temp = std::max(max_chamber_temp, m_config.chamber_temperature.get_at(extruder.id())); } @@ -2993,7 +2951,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato BedType curr_bed_type = m_config.curr_bed_type; int min_temperature_vitrification = std::numeric_limits::max(); - for (const auto& extruder : m_writer.extruders()) + for (const auto& extruder : m_writer->extruders()) min_temperature_vitrification = std::min(min_temperature_vitrification, m_config.temperature_vitrification.get_at(extruder.id())); @@ -3107,18 +3065,18 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato if (activate_chamber_temp_control && max_chamber_temp > 0){ int temp_out =0; if(!custom_gcode_sets_temperature(machine_start_gcode,141,191,false,temp_out)) - file.write(m_writer.set_chamber_temperature(max_chamber_temp, true)); // set chamber_temperature + file.write(m_writer->set_chamber_temperature(max_chamber_temp, true)); // set chamber_temperature } // Write the custom start G-code file.writeln(machine_start_gcode); //BBS: gcode writer doesn't know where the real position of extruder is after inserting custom gcode - m_writer.set_current_position_clear(false); + m_writer->set_current_position_clear(false); m_start_gcode_filament = GCodeProcessor::get_gcode_last_filament(machine_start_gcode); if (is_bbl_printers) { - m_writer.init_extruder(initial_non_support_extruder_id); + m_writer->init_extruder(initial_non_support_extruder_id); // add the missing filament start gcode in machine start gcode { DynamicConfig config; @@ -3131,7 +3089,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } // Orca: add missing PA settings for initial filament if (m_config.enable_pressure_advance.get_at(initial_non_support_extruder_id)) { - file.write(m_writer.set_pressure_advance(m_config.pressure_advance.get_at(initial_non_support_extruder_id))); + file.write(m_writer->set_pressure_advance(m_config.pressure_advance.get_at(initial_non_support_extruder_id))); // Orca: Adaptive PA // Reset Adaptive PA processor last PA value m_pa_processor->resetPreviousPA(m_config.pressure_advance.get_at(initial_non_support_extruder_id)); @@ -3157,14 +3115,14 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Orca: when activate_air_filtration is set on any extruder, find and set the highest during_print_exhaust_fan_speed bool activate_air_filtration = false; int during_print_exhaust_fan_speed = 0; - for (const auto &extruder : m_writer.extruders()) { + for (const auto &extruder : m_writer->extruders()) { activate_air_filtration |= m_config.activate_air_filtration.get_at(extruder.id()); if (m_config.activate_air_filtration.get_at(extruder.id())) during_print_exhaust_fan_speed = std::max(during_print_exhaust_fan_speed, m_config.during_print_exhaust_fan_speed.get_at(extruder.id())); } if (activate_air_filtration) - file.write(m_writer.set_exhaust_fan(during_print_exhaust_fan_speed, true)); + file.write(m_writer->set_exhaust_fan(during_print_exhaust_fan_speed, true)); print.throw_if_canceled(); @@ -3180,7 +3138,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_seam_placer.init(print, throw_if_canceled_func); // BBS: get path for change filament - if (m_writer.multiple_extruders) { + if (m_writer->multiple_extruders) { std::vector points = get_path_of_change_filament(print); if (points.size() == 3) { travel_point_1 = points[0]; @@ -3212,12 +3170,12 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato std::string gcode; gcode += ";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Layer_Change) + "\n"; if ((print.default_object_config().outer_wall_acceleration.value > 0 && print.default_object_config().outer_wall_acceleration.value > 0)) { - gcode += m_writer.set_print_acceleration((unsigned int)floor(print.default_object_config().outer_wall_acceleration.value + 0.5)); + gcode += m_writer->set_print_acceleration((unsigned int)floor(print.default_object_config().outer_wall_acceleration.value + 0.5)); } if (print.default_object_config().outer_wall_jerk.value > 0) { double jerk = print.default_object_config().outer_wall_jerk.value; - gcode += m_writer.set_jerk_xy(jerk); + gcode += m_writer->set_jerk_xy(jerk); } auto params = print.calib_params(); @@ -3261,8 +3219,8 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } print.throw_if_canceled(); this->set_origin(unscale((*print_object_instance_sequential_active)->shift)); - this->update_origin_snap((*print_object_instance_sequential_active)->print_object, - (*print_object_instance_sequential_active)->shift); + this->on_set_origin((*print_object_instance_sequential_active)->print_object, + (*print_object_instance_sequential_active)->shift); // BBS: prime extruder if extruder change happens before this object instance bool prime_extruder = false; @@ -3272,7 +3230,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_enable_cooling_markers = false; // we're not filtering these moves through CoolingBuffer m_avoid_crossing_perimeters.use_external_mp_once(); // BBS. change tool before moving to origin point. - if (m_writer.need_toolchange(initial_extruder_id)) { + if (m_writer->need_toolchange(initial_extruder_id)) { const PrintObjectConfig& object_config = object.config(); coordf_t initial_layer_print_height = print.config().initial_layer_print_height.value; @@ -3285,7 +3243,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } else { file.write(this->retract()); } - file.write(m_writer.travel_to_z(m_max_layer_z + m_writer.config.z_hop.get_at(initial_extruder_id))); + file.write(m_writer->travel_to_z(m_max_layer_z + m_writer->config.z_hop.get_at(initial_extruder_id))); file.write(this->travel_to(Point(0, 0), erNone, "move to origin position for next object")); m_enable_cooling_markers = true; // Disable motion planner when traveling to first object point. @@ -3308,7 +3266,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // and export G-code into file. tool_ordering.cal_most_used_extruder(print.config()); m_printed_objects.emplace_back(&object); - this->process_layers(print, tool_ordering, collect_layers_to_print(object), *print_object_instance_sequential_active - object.instances().data(), file, + this->process_layers(print, tool_ordering, collect_layers_to_print(object, print.config().belt_printer.value), *print_object_instance_sequential_active - object.instances().data(), file, prime_extruder); { // save the flush statitics stored in tool ordering by object @@ -3325,7 +3283,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato { const auto plr_mode = print.config().enable_power_loss_recovery.value; if (m_second_layer_things_done && plr_mode == PowerLossRecoveryMode::Enable) { - file.write(m_writer.enable_power_loss_recovery(PowerLossRecoveryMode::Disable)); + file.write(m_writer->enable_power_loss_recovery(PowerLossRecoveryMode::Disable)); } } ++ finished_objects; @@ -3346,7 +3304,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_wipe_tower->set_wipe_tower_bbx(print.get_wipe_tower_bbx()); m_wipe_tower->set_rib_offset(print.get_rib_offset()); //BBS - file.write(m_writer.travel_to_z(initial_layer_print_height + m_config.z_offset.value, "Move to the first layer height")); + file.write(m_writer->travel_to_z(initial_layer_print_height + m_config.z_offset.value, "Move to the first layer height")); if (wipe_tower_type == WipeTowerType::Type2 && print.config().single_extruder_multi_material_priming) { file.write(m_wipe_tower->prime(*this)); @@ -3405,7 +3363,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Orca: disable power loss recovery if (m_second_layer_things_done && print.config().enable_power_loss_recovery.value == PowerLossRecoveryMode::Enable) { - file.write(m_writer.enable_power_loss_recovery(PowerLossRecoveryMode::Disable)); + file.write(m_writer->enable_power_loss_recovery(PowerLossRecoveryMode::Disable)); } if (m_wipe_tower) // Purge the extruder, pull out the active filament. @@ -3419,14 +3377,14 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // if needed, write the gcode_label_objects_end { std::string gcode; - m_writer.add_object_change_labels(gcode); + m_writer->add_object_change_labels(gcode); file.write(gcode); } - file.write(m_writer.set_fan(0)); + file.write(m_writer->set_fan(0)); //BBS: make sure the additional fan is closed when end if(m_config.auxiliary_fan.value) - file.write(m_writer.set_additional_fan(0)); + file.write(m_writer->set_additional_fan(0)); if (is_bbl_printers) { //BBS: close spaghetti detector //Note: M981 is also used to tell xcam the last layer is finished, so we need always send it even if spaghetti option is disabled. @@ -3442,12 +3400,12 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato DynamicConfig config; config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index)); //BBS - config.set_key_value("layer_z", new ConfigOptionFloat(m_writer.get_position()(2) - m_config.z_offset.value)); + config.set_key_value("layer_z", new ConfigOptionFloat(m_writer->get_position()(2) - m_config.z_offset.value)); config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); if (print.config().single_extruder_multi_material) { // Process the filament_end_gcode for the active filament only. - int extruder_id = m_writer.filament()->id(); + int extruder_id = m_writer->filament()->id(); config.set_key_value("filament_extruder_id", new ConfigOptionInt(extruder_id)); file.writeln(this->placeholder_parser_process("filament_end_gcode", print.config().filament_end_gcode.get_at(extruder_id), extruder_id, &config)); } else { @@ -3457,20 +3415,20 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.writeln(this->placeholder_parser_process("filament_end_gcode", end_gcode, extruder_id, &config)); } } - file.writeln(this->placeholder_parser_process("machine_end_gcode", print.config().machine_end_gcode, m_writer.filament()->id(), &config)); + file.writeln(this->placeholder_parser_process("machine_end_gcode", print.config().machine_end_gcode, m_writer->filament()->id(), &config)); } - file.write(m_writer.update_progress(m_layer_count, m_layer_count, true)); // 100% - file.write(m_writer.postamble()); + file.write(m_writer->update_progress(m_layer_count, m_layer_count, true)); // 100% + file.write(m_writer->postamble()); if (activate_chamber_temp_control && max_chamber_temp > 0) - file.write(m_writer.set_chamber_temperature(0, false)); //close chamber_temperature + file.write(m_writer->set_chamber_temperature(0, false)); //close chamber_temperature if (activate_air_filtration) { int complete_print_exhaust_fan_speed = 0; - for (const auto& extruder : m_writer.extruders()) + for (const auto& extruder : m_writer->extruders()) if (m_config.activate_air_filtration.get_at(extruder.id())) complete_print_exhaust_fan_speed = std::max(complete_print_exhaust_fan_speed, m_config.complete_print_exhaust_fan_speed.get_at(extruder.id())); - file.write(m_writer.set_exhaust_fan(complete_print_exhaust_fan_speed, true)); + file.write(m_writer->set_exhaust_fan(complete_print_exhaust_fan_speed, true)); } // adds tags for time estimators file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Last_Line_M73_Placeholder).c_str()); @@ -3482,7 +3440,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.write(DoExport::update_print_stats_and_format_filament_stats( // Const inputs has_wipe_tower, print.wipe_tower_data(), - m_writer.extruders(), + m_writer->extruders(), // Modifies print.m_print_statistics)); print.m_print_statistics.initial_tool = initial_extruder_id; @@ -3656,7 +3614,7 @@ void GCode::process_layers( ); const auto fan_mover = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, - [&fan_mover = this->m_fan_mover, &config = this->config(), &writer = this->m_writer](std::string in)->std::string { + [&fan_mover = this->m_fan_mover, &config = this->config(), &writer = *this->m_writer](std::string in)->std::string { CNumericLocalesSetter locales_setter; @@ -3756,7 +3714,7 @@ void GCode::process_layers( ); const auto fan_mover = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, - [&fan_mover = this->m_fan_mover, &config = this->config(), &writer = this->m_writer](std::string in)->std::string { + [&fan_mover = this->m_fan_mover, &config = this->config(), &writer = *this->m_writer](std::string in)->std::string { if (config.fan_speedup_time.value != 0 || config.fan_kickstart.value > 0) { if (fan_mover.get() == nullptr) @@ -3824,21 +3782,21 @@ std::string GCode::placeholder_parser_process(const std::string &name, const std PlaceholderParserIntegration &ppi = m_placeholder_parser_integration; try { - ppi.update_from_gcodewriter(m_writer); + ppi.update_from_gcodewriter(*m_writer); std::string output = ppi.parser.process(templ, current_filament_id, config_override, &ppi.output_config, &ppi.context); ppi.validate_output_vector_variables(); if (const std::vector &pos = ppi.opt_position->values; ppi.position != pos) { // Update G-code writer. - m_writer.set_position({ pos[0], pos[1], pos[2] }); + m_writer->set_position({ pos[0], pos[1], pos[2] }); this->set_last_pos(this->gcode_to_point({ pos[0], pos[1] })); } - for (const Extruder &e : m_writer.extruders()) { + for (const Extruder &e : m_writer->extruders()) { unsigned int eid = e.id(); assert(eid < ppi.num_extruders); if ( eid < ppi.num_extruders) { - if (! m_writer.config.use_relative_e_distances && ! is_approx(ppi.e_position[eid], ppi.opt_e_position->values[eid])) + if (! m_writer->config.use_relative_e_distances && ! is_approx(ppi.e_position[eid], ppi.opt_e_position->values[eid])) const_cast(e).set_position(ppi.opt_e_position->values[eid]); if (! is_approx(ppi.e_retracted[eid], ppi.opt_e_retracted->values[eid]) || ! is_approx(ppi.e_restart_extra[eid], ppi.opt_e_restart_extra->values[eid])) @@ -3963,9 +3921,9 @@ void GCode::_print_first_layer_bed_temperature(GCodeOutputStream &file, Print &p temp = temp_by_gcode; #endif - // Always call m_writer.set_bed_temperature() so it will set the internal "current" state of the bed temp as if + // Always call m_writer->set_bed_temperature() so it will set the internal "current" state of the bed temp as if // the custom start G-code emited these. - std::string set_temp_gcode = m_writer.set_bed_temperature(bed_temp, wait); + std::string set_temp_gcode = m_writer->set_bed_temperature(bed_temp, wait); if (! temp_set_by_gcode) file.write(set_temp_gcode); } @@ -3985,14 +3943,14 @@ void GCode::_print_first_layer_extruder_temperatures(GCodeOutputStream &file, Pr int temp = print.config().nozzle_temperature_initial_layer.get_at(first_printing_extruder_id); if (temp_by_gcode >= 0 && temp_by_gcode < 1000) temp = temp_by_gcode; - m_writer.set_temperature(temp, wait, first_printing_extruder_id); + m_writer->set_temperature(temp, wait, first_printing_extruder_id); } else { // Custom G-code does not set the extruder temperature. Do it now. if (print.config().single_extruder_multi_material.value) { // Set temperature of the first printing extruder only. int temp = print.config().nozzle_temperature_initial_layer.get_at(first_printing_extruder_id); if (temp > 0) - file.write(m_writer.set_temperature(temp, wait, first_printing_extruder_id)); + file.write(m_writer->set_temperature(temp, wait, first_printing_extruder_id)); } else { // Set temperatures of all the printing extruders. for (unsigned int tool_id : print.extruders()) { @@ -4004,7 +3962,7 @@ void GCode::_print_first_layer_extruder_temperatures(GCodeOutputStream &file, Pr temp = print.config().idle_temperature.get_at(tool_id); } if (temp > 0) - file.write(m_writer.set_temperature(temp, wait, tool_id)); + file.write(m_writer->set_temperature(temp, wait, tool_id)); } } } @@ -4427,7 +4385,7 @@ LayerResult GCode::process_layer( //support is attached above the object, and support layers has independent layer height, then the lowest support //interface layer id is 0. bool first_layer = (layer.id() == 0 && abs(layer.bottom_z()) < EPSILON); - m_writer.set_is_first_layer(first_layer); + m_writer->set_is_first_layer(first_layer); unsigned int first_extruder_id = layer_tools.extruders.front(); // Initialize config with the 1st object to be printed at this layer. @@ -4477,7 +4435,7 @@ LayerResult GCode::process_layer( config.set_key_value("layer_z", new ConfigOptionFloat(print_z)); config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); gcode += this->placeholder_parser_process("before_layer_change_gcode", - print.config().before_layer_change_gcode.value, m_writer.filament()->id(), &config) + print.config().before_layer_change_gcode.value, m_writer->filament()->id(), &config) + "\n"; } @@ -4512,7 +4470,7 @@ LayerResult GCode::process_layer( config.set_key_value("layer_z", new ConfigOptionFloat(print_z)); config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); gcode += this->placeholder_parser_process("timelapse_gcode", - print.config().time_lapse_gcode.value, m_writer.filament()->id(), &config) + print.config().time_lapse_gcode.value, m_writer->filament()->id(), &config) + "\n"; } @@ -4522,7 +4480,7 @@ LayerResult GCode::process_layer( config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index)); config.set_key_value("layer_z", new ConfigOptionFloat(print_z)); gcode += this->placeholder_parser_process("layer_change_gcode", - print.config().layer_change_gcode.value, m_writer.filament()->id(), &config) + print.config().layer_change_gcode.value, m_writer->filament()->id(), &config) + "\n"; config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); } @@ -4562,7 +4520,7 @@ LayerResult GCode::process_layer( case CalibMode::Calib_Input_shaping_freq: { if (m_layer_index == 1){ gcode += writer().set_input_shaping('A', print.calib_params().start, 0.f, print.calib_params().shaper_type); - if (m_writer.get_gcode_flavor() == gcfKlipper) { + if (m_writer->get_gcode_flavor() == gcfKlipper) { // Disable minimum cruise ratio to ensure consistent motion for calibration gcode += "SET_VELOCITY_LIMIT MINIMUM_CRUISE_RATIO=0\n"; } @@ -4578,7 +4536,7 @@ LayerResult GCode::process_layer( } case CalibMode::Calib_Input_shaping_damp: { if (m_layer_index == 1){ - if (m_writer.get_gcode_flavor() == gcfKlipper) { + if (m_writer->get_gcode_flavor() == gcfKlipper) { // Disable minimum cruise ratio to ensure consistent motion for calibration gcode += "SET_VELOCITY_LIMIT MINIMUM_CRUISE_RATIO=0\n"; } @@ -4590,7 +4548,7 @@ LayerResult GCode::process_layer( break; } case CalibMode::Calib_Cornering: { - if (m_writer.get_gcode_flavor() == gcfMarlinFirmware && + if (m_writer->get_gcode_flavor() == gcfMarlinFirmware && !m_config.machine_max_junction_deviation.values.empty() && m_config.machine_max_junction_deviation.values.front() > 0) { gcode += writer().set_junction_deviation(this->interpolate_value_across_layers(print.calib_params().start, print.calib_params().end)); @@ -4605,22 +4563,22 @@ LayerResult GCode::process_layer( if (first_layer) { // Orca: we don't need to optimize the Klipper as only set once if (m_config.default_acceleration.value > 0 && m_config.initial_layer_acceleration.value > 0) { - gcode += m_writer.set_print_acceleration((unsigned int)floor(m_config.initial_layer_acceleration.value + 0.5)); + gcode += m_writer->set_print_acceleration((unsigned int)floor(m_config.initial_layer_acceleration.value + 0.5)); } if (m_config.default_jerk.value > 0 && m_config.initial_layer_jerk.value > 0) { - gcode += m_writer.set_jerk_xy(m_config.initial_layer_jerk.value); + gcode += m_writer->set_jerk_xy(m_config.initial_layer_jerk.value); } - if (m_writer.get_gcode_flavor() == gcfMarlinFirmware && m_config.default_junction_deviation.value > 0) { - gcode += m_writer.set_junction_deviation(m_config.default_junction_deviation.value); + if (m_writer->get_gcode_flavor() == gcfMarlinFirmware && m_config.default_junction_deviation.value > 0) { + gcode += m_writer->set_junction_deviation(m_config.default_junction_deviation.value); } } if (!first_layer && !m_second_layer_things_done) { // Orca: set power loss recovery const auto plr_mode = print.config().enable_power_loss_recovery.value; - gcode += m_writer.enable_power_loss_recovery(plr_mode); + gcode += m_writer->enable_power_loss_recovery(plr_mode); if (print.is_BBL_printer()) { // BBS: open first layer inspection at second layer @@ -4635,23 +4593,23 @@ LayerResult GCode::process_layer( // Reset acceleration at sencond layer // Orca: only set once, don't need to call set_accel_and_jerk if (m_config.default_acceleration.value > 0 && m_config.initial_layer_acceleration.value > 0) { - gcode += m_writer.set_print_acceleration((unsigned int) floor(m_config.default_acceleration.value + 0.5)); + gcode += m_writer->set_print_acceleration((unsigned int) floor(m_config.default_acceleration.value + 0.5)); } if (m_config.default_jerk.value > 0 && m_config.initial_layer_jerk.value > 0) { - gcode += m_writer.set_jerk_xy(m_config.default_jerk.value); + gcode += m_writer->set_jerk_xy(m_config.default_jerk.value); } // Transition from 1st to 2nd layer. Adjust nozzle temperatures as prescribed by the nozzle dependent // nozzle_temperature_initial_layer vs. temperature settings. - for (const Extruder& extruder : m_writer.extruders()) { + for (const Extruder& extruder : m_writer->extruders()) { if ((print.config().single_extruder_multi_material.value || m_ooze_prevention.enable) && - extruder.id() != m_writer.filament()->id()) + extruder.id() != m_writer->filament()->id()) // In single extruder multi material mode, set the temperature for the current extruder only. continue; int temperature = print.config().nozzle_temperature.get_at(extruder.id()); if (temperature > 0 && temperature != print.config().nozzle_temperature_initial_layer.get_at(extruder.id())) - gcode += m_writer.set_temperature(temperature, false, extruder.id()); + gcode += m_writer->set_temperature(temperature, false, extruder.id()); } // BBS @@ -4660,14 +4618,14 @@ LayerResult GCode::process_layer( bed_temp = get_highest_bed_temperature(false,print); else bed_temp = get_bed_temperature(first_extruder_id, false, m_config.curr_bed_type); - gcode += m_writer.set_bed_temperature(bed_temp); + gcode += m_writer->set_bed_temperature(bed_temp); // Mark the temperature transition from 1st to 2nd layer to be finished. m_second_layer_things_done = true; } if (single_object_instance_idx == size_t(-1)) { // Normal (non-sequential) print. - gcode += ProcessLayer::emit_custom_gcode_per_print_z(*this, layer_tools.custom_gcode, m_writer.filament()->id(), first_extruder_id, print.config()); + gcode += ProcessLayer::emit_custom_gcode_per_print_z(*this, layer_tools.custom_gcode, m_writer->filament()->id(), first_extruder_id, print.config()); } // BBS: get next extruder according to flush and soluble @@ -4958,9 +4916,9 @@ LayerResult GCode::process_layer( auto insert_timelapse_gcode = [this, print_z, &print, &most_used_extruder, &layer_object_label_ids,&printed_objects = std::as_const(m_printed_objects)]() -> std::string { PosPickCtx ctx; - ctx.curr_pos = { (coord_t)(scale_(m_writer.get_position().x())),(coord_t)(scale_(m_writer.get_position().y())) }; + ctx.curr_pos = { (coord_t)(scale_(m_writer->get_position().x())),(coord_t)(scale_(m_writer->get_position().y())) }; ctx.curr_layer = this->layer(); - ctx.curr_extruder_id = m_writer.filament()->extruder_id(); + ctx.curr_extruder_id = m_writer->filament()->extruder_id(); ctx.picture_extruder_id = most_used_extruder; if (m_config.print_sequence == PrintSequence::ByObject) ctx.printed_objects = printed_objects; @@ -4978,17 +4936,17 @@ LayerResult GCode::process_layer( config.set_key_value("timelapse_pos_x", new ConfigOptionInt((int)timelapse_pos.x())); config.set_key_value("timelapse_pos_y", new ConfigOptionInt((int)timelapse_pos.y())); config.set_key_value("has_timelapse_safe_pos", new ConfigOptionBool(timelapse_pos != DefaultTimelapsePos)); - timelapse_gcode = this->placeholder_parser_process("timelapse_gcode", print.config().time_lapse_gcode.value, m_writer.filament()->id(), &config) + "\n"; + timelapse_gcode = this->placeholder_parser_process("timelapse_gcode", print.config().time_lapse_gcode.value, m_writer->filament()->id(), &config) + "\n"; } if (!timelapse_gcode.empty()) { - m_writer.set_current_position_clear(false); + m_writer->set_current_position_clear(false); double temp_z_after_tool_change; if (GCodeProcessor::get_last_z_from_gcode(timelapse_gcode, temp_z_after_tool_change)) { - Vec3d pos = m_writer.get_position(); + Vec3d pos = m_writer->get_position(); pos(2) = temp_z_after_tool_change; - m_writer.set_position(pos); + m_writer->set_position(pos); } } @@ -5034,16 +4992,16 @@ LayerResult GCode::process_layer( config.set_key_value("layer_z", new ConfigOptionFloat(print_z)); config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); config.set_key_value("most_used_physical_extruder_id", new ConfigOptionInt(m_config.physical_extruder_map.get_at(most_used_extruder))); - config.set_key_value("curr_physical_extruder_id", new ConfigOptionInt(m_config.physical_extruder_map.get_at(m_writer.filament()->extruder_id()))); - wrapping_gcode = this->placeholder_parser_process("wrapping_detection_gcode", print.config().wrapping_detection_gcode.value, m_writer.filament()->id(), &config) +"\n"; + config.set_key_value("curr_physical_extruder_id", new ConfigOptionInt(m_config.physical_extruder_map.get_at(m_writer->filament()->extruder_id()))); + wrapping_gcode = this->placeholder_parser_process("wrapping_detection_gcode", print.config().wrapping_detection_gcode.value, m_writer->filament()->id(), &config) +"\n"; } - m_writer.set_current_position_clear(false); + m_writer->set_current_position_clear(false); double temp_z_after_tool_change; if (GCodeProcessor::get_last_z_from_gcode(wrapping_gcode, temp_z_after_tool_change)) { - Vec3d pos = m_writer.get_position(); + Vec3d pos = m_writer->get_position(); pos(2) = temp_z_after_tool_change; - m_writer.set_position(pos); + m_writer->set_position(pos); } return wrapping_gcode; }; @@ -5076,7 +5034,7 @@ LayerResult GCode::process_layer( if (should_insert) { gcode += this->retract(false, false, auto_lift_type, true); - m_writer.add_object_change_labels(gcode); + m_writer->add_object_change_labels(gcode); gcode += insert_timelapse_gcode(); has_insert_timelapse_gcode = true; @@ -5093,12 +5051,12 @@ LayerResult GCode::process_layer( } else { if (need_insert_timelapse_gcode_for_traditional && !has_insert_timelapse_gcode && - m_writer.need_toolchange(extruder_id) && + m_writer->need_toolchange(extruder_id) && m_config.nozzle_diameter.values.size() == 2 && writer().filament() && (get_extruder_id(writer().filament()->id()) == most_used_extruder)) { gcode += this->retract(false, false, auto_lift_type, true); - m_writer.add_object_change_labels(gcode); + m_writer->add_object_change_labels(gcode); gcode += insert_timelapse_gcode(); has_insert_timelapse_gcode = true; @@ -5197,19 +5155,19 @@ LayerResult GCode::process_layer( // exclude objects if (m_enable_exclude_object) { if (is_BBL_Printer()) { - m_writer.set_object_start_str( + m_writer->set_object_start_str( std::string("; start printing object, unique label id: ") + std::to_string(instance_to_print.label_object_id) + "\n" + "M624 " + _encode_label_ids_to_base64({instance_to_print.label_object_id}) + "\n"); } else { const auto gflavor = print.config().gcode_flavor.value; if (gflavor == gcfKlipper) { - m_writer.set_object_start_str(std::string("EXCLUDE_OBJECT_START NAME=") + + m_writer->set_object_start_str(std::string("EXCLUDE_OBJECT_START NAME=") + get_instance_name(&instance_to_print.print_object, inst.id) + "\n"); } else if (gflavor == gcfMarlinLegacy || gflavor == gcfMarlinFirmware || gflavor == gcfRepRapFirmware) { std::string str = std::string("M486 S") + std::to_string(inst.unique_id) + "\n"; - m_writer.set_object_start_str(str); + m_writer->set_object_start_str(str); } } } @@ -5227,7 +5185,7 @@ LayerResult GCode::process_layer( m_avoid_crossing_perimeters.use_external_mp_once(); m_last_obj_copy = this_object_copy; this->set_origin(unscale(offset)); - this->update_origin_snap(&instance_to_print.print_object, offset); + this->on_set_origin(&instance_to_print.print_object, offset); if (instance_to_print.object_by_extruder.support != nullptr) { m_layer = layers[instance_to_print.layer_id].support_layer; m_object_layer_over_raft = false; @@ -5251,7 +5209,7 @@ LayerResult GCode::process_layer( m_avoid_crossing_perimeters.use_external_mp_once(); m_last_obj_copy = this_object_copy; this->set_origin(unscale(offset)); - this->update_origin_snap(&instance_to_print.print_object, offset); + this->on_set_origin(&instance_to_print.print_object, offset); ExtrusionEntityCollection support_eec; // BBS @@ -5300,7 +5258,7 @@ LayerResult GCode::process_layer( m_avoid_crossing_perimeters.use_external_mp_once(); m_last_obj_copy = this_object_copy; this->set_origin(unscale(offset)); - this->update_origin_snap(&instance_to_print.print_object, offset); + this->on_set_origin(&instance_to_print.print_object, offset); //FIXME the following code prints regions in the order they are defined, the path is not optimized in any way. auto has_infill = [](const std::vector &by_region) { @@ -5337,20 +5295,20 @@ LayerResult GCode::process_layer( } // exclude objects // Don't set m_gcode_label_objects_end if you don't had to write the m_gcode_label_objects_start. - if (!m_writer.is_object_start_str_empty()) { - m_writer.set_object_start_str(""); + if (!m_writer->is_object_start_str_empty()) { + m_writer->set_object_start_str(""); } else if (m_enable_exclude_object) { if (is_BBL_Printer()) { - m_writer.set_object_end_str(std::string("; stop printing object, unique label id: ") + + m_writer->set_object_end_str(std::string("; stop printing object, unique label id: ") + std::to_string(instance_to_print.label_object_id) + "\n" + "M625\n"); } else { const auto gflavor = print.config().gcode_flavor.value; if (gflavor == gcfKlipper) { - m_writer.set_object_end_str(std::string("EXCLUDE_OBJECT_END NAME=") + + m_writer->set_object_end_str(std::string("EXCLUDE_OBJECT_END NAME=") + get_instance_name(&instance_to_print.print_object, inst.id) + "\n"); } else if (gflavor == gcfMarlinLegacy || gflavor == gcfMarlinFirmware || gflavor == gcfRepRapFirmware) { - m_writer.set_object_end_str(std::string("M486 S-1\n")); + m_writer->set_object_end_str(std::string("M486 S-1\n")); } } } @@ -5402,7 +5360,7 @@ LayerResult GCode::process_layer( if (FILAMENT_CONFIG(retract_when_changing_layer)) { gcode += this->retract(false, false, auto_lift_type, true); } - m_writer.add_object_change_labels(gcode); + m_writer->add_object_change_labels(gcode); gcode += insert_timelapse_gcode(); } @@ -5414,7 +5372,7 @@ LayerResult GCode::process_layer( void GCode::apply_print_config(const PrintConfig &print_config) { - m_writer.apply_print_config(print_config); + m_writer->apply_print_config(print_config); m_config.apply(print_config); m_scaled_resolution = scaled(print_config.resolution.value); m_enable_exclude_object = m_config.exclude_object; @@ -5504,7 +5462,7 @@ void GCode::append_full_config(const Print &print, std::string &str) void GCode::set_extruders(const std::vector &extruder_ids) { - m_writer.set_extruders(extruder_ids); + m_writer->set_extruders(extruder_ids); // enable wipe path generation if any extruder has wipe enabled m_wipe.enable = false; @@ -5527,139 +5485,15 @@ void GCode::set_origin(const Vec2d &pointf) m_origin = pointf; } -void GCode::update_origin_snap(const PrintObject *obj, const Point &inst_shift) -{ - if (!m_origin_snap[0] && !m_origin_snap[1] && !m_origin_snap[2]) - return; - - // Clear existing snap so to_machine_coords gives raw machine coords for bbox computation. - for (int a = 0; a < 3; ++a) - m_writer.set_origin_snap(a, false, 0., 0.); - - // Reconstruct the belt pipeline transform for this object (same as - // PrintObjectSlice.cpp: z_shift * scale * shear * pre_remap). - const auto &cfg = m_config; - Transform3d belt = Transform3d::Identity(); - - // Pre-slice remap - int pre_rx = int(cfg.belt_preslice_remap_x.value); - int pre_ry = int(cfg.belt_preslice_remap_y.value); - int pre_rz = int(cfg.belt_preslice_remap_z.value); - if (pre_rx != int(BeltRemapAxis::PosX) || - pre_ry != int(BeltRemapAxis::PosY) || - pre_rz != int(BeltRemapAxis::PosZ)) { - auto remap_col = [](int r) -> Vec3d { - int a = r % 3; Vec3d c = Vec3d::Zero(); - c[a] = (r < 3) ? 1.0 : -1.0; - return c; - }; - Matrix3d lin; - lin.col(0) = remap_col(pre_rx); - lin.col(1) = remap_col(pre_ry); - lin.col(2) = remap_col(pre_rz); - Transform3d pre = Transform3d::Identity(); - pre.linear() = lin; - if (pre_rx >= 6 || pre_ry >= 6 || pre_rz >= 6) { - BoundingBoxf bb(cfg.printable_area.values); - Vec3d vm(bb.max.x(), bb.max.y(), cfg.printable_height.value); - Vec3d tr = Vec3d::Zero(); - if (pre_rx >= 6) tr[0] = vm[pre_rx % 3]; - if (pre_ry >= 6) tr[1] = vm[pre_ry % 3]; - if (pre_rz >= 6) tr[2] = vm[pre_rz % 3]; - pre.translation() = tr; - } - belt = pre * belt; - } - - // Shear - auto shear_f = [](BeltShearMode m, double a) -> double { - double r = Geometry::deg2rad(a), s = std::sin(r), c = std::cos(r); - switch (m) { - case BeltShearMode::PosCot: return (s > EPSILON) ? c/s : 0.; - case BeltShearMode::NegCot: return (s > EPSILON) ? -c/s : 0.; - case BeltShearMode::PosTan: return (c > EPSILON) ? s/c : 0.; - case BeltShearMode::NegTan: return (c > EPSILON) ? -s/c : 0.; - default: return 0.; - } - }; - struct AS { BeltShearMode m; double a; int f; }; - AS axes[3] = { - {cfg.belt_shear_x.value, cfg.belt_shear_x_angle.value, int(cfg.belt_shear_x_from.value)}, - {cfg.belt_shear_y.value, cfg.belt_shear_y_angle.value, int(cfg.belt_shear_y_from.value)}, - {cfg.belt_shear_z.value, cfg.belt_shear_z_angle.value, int(cfg.belt_shear_z_from.value)}, - }; - Transform3d shear = Transform3d::Identity(); - for (int i = 0; i < 3; ++i) - if (axes[i].m != BeltShearMode::None) { - double f = shear_f(axes[i].m, axes[i].a); - if (std::abs(f) > EPSILON) - shear.matrix()(i, axes[i].f) += f; - } - - // Scale - auto scale_f = [](BeltScaleMode m, double a) -> double { - if (m == BeltScaleMode::None) return 1.; - double r = Geometry::deg2rad(a), s = std::sin(r), c = std::cos(r); - switch (m) { - case BeltScaleMode::InvSin: return (s > EPSILON) ? 1./s : 1.; - case BeltScaleMode::InvCos: return (c > EPSILON) ? 1./c : 1.; - case BeltScaleMode::Sin: return s; - case BeltScaleMode::Cos: return c; - default: return 1.; - } - }; - Transform3d sc = Transform3d::Identity(); - sc.matrix()(0,0) = scale_f(cfg.belt_scale_x.value, cfg.belt_scale_x_angle.value); - sc.matrix()(1,1) = scale_f(cfg.belt_scale_y.value, cfg.belt_scale_y_angle.value); - sc.matrix()(2,2) = scale_f(cfg.belt_scale_z.value, cfg.belt_scale_z_angle.value); - - belt = sc * shear * belt; - - // Z-shift - double zs = (obj->belt_min_z() < 0.) ? -obj->belt_min_z() : 0.; - if (zs > 0.) { - Transform3d zsh = Transform3d::Identity(); - zsh.matrix()(2, 3) = zs; - belt = zsh * belt; - } - - // Full transform: belt * trafo_centered - Transform3d full = belt * obj->trafo_centered(); - - // Instance shift in slicer space + global Z offset - Vec3d shift(unscale(inst_shift.x()), - unscale(inst_shift.y()), - obj->belt_global_z_offset()); - - // Compute this instance's machine-space bbox min - BoundingBoxf3 bb = obj->model_object()->raw_bounding_box(); - Vec3d mn = bb.min.cast(), mx = bb.max.cast(); - Vec3d inst_min(std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max()); - for (int i = 0; i < 8; ++i) { - Vec3d c((i & 1) ? mx.x() : mn.x(), - (i & 2) ? mx.y() : mn.y(), - (i & 4) ? mx.z() : mn.z()); - Vec3d mc = m_writer.to_machine_coords(full * c + shift); - for (int a = 0; a < 3; ++a) - inst_min[a] = std::min(inst_min[a], mc[a]); - } - - // Update writer snap for each enabled axis - for (int a = 0; a < 3; ++a) - m_writer.set_origin_snap(a, m_origin_snap[a], m_origin_snap_offset[a], inst_min[a]); -} - std::string GCode::preamble() { - std::string gcode = m_writer.preamble(); + std::string gcode = m_writer->preamble(); /* Perform a *silent* move to z_offset: we need this to initialize the Z position of our writer object so that any initial lift taking place before the first layer change will raise the extruder from the correct initial Z instead of 0. */ - m_writer.travel_to_z(m_config.z_offset.value); + m_writer->travel_to_z(m_config.z_offset.value); return gcode; } @@ -5670,28 +5504,28 @@ std::string GCode::change_layer(coordf_t print_z) std::string gcode; if (m_layer_count > 0) // Increment a progress bar indicator. - gcode += m_writer.update_progress(++ m_layer_index, m_layer_count); + gcode += m_writer->update_progress(++ m_layer_index, m_layer_count); //BBS coordf_t z = print_z + m_config.z_offset.value; // in unscaled coordinates - if (FILAMENT_CONFIG(retract_when_changing_layer) && m_writer.will_move_z(z)) { + if (FILAMENT_CONFIG(retract_when_changing_layer) && m_writer->will_move_z(z)) { LiftType lift_type = this->to_lift_type(ZHopType(FILAMENT_CONFIG(z_hop_types))); //BBS: force to use SpiralLift when change layer if lift type is auto gcode += this->retract(false, false, ZHopType(FILAMENT_CONFIG(z_hop_types)) == ZHopType::zhtAuto ? LiftType::SpiralLift : lift_type); } - m_writer.add_object_change_labels(gcode); + m_writer->add_object_change_labels(gcode); if (m_spiral_vase) { //BBS: force to normal lift immediately in spiral vase mode std::ostringstream comment; comment << "move to next layer (" << m_layer_index << ")"; - gcode += m_writer.travel_to_z(z, comment.str()); + gcode += m_writer->travel_to_z(z, comment.str()); } m_need_change_layer_lift_z = true; m_nominal_z = z; - m_writer.get_position().z() = z; + m_writer->get_position().z() = z; // forget last wiping path as wiping after raising Z is pointless // BBS. Dont forget wiping path to reduce stringing. @@ -5990,7 +5824,7 @@ std::string GCode::extrude_loop(ExtrusionLoop loop, std::string description, dou //Point pt = ((nd * nd >= l2) ? (p1+v*0.4): (p1 + 0.2 * v * (nd / sqrt(l2)))).cast(); pt.rotate(angle, paths.front().polyline.points.front()); // generate the travel move - gcode += m_writer.extrude_to_xy(this->point_to_gcode(pt), 0,"move inwards before travel",true); + gcode += m_writer->extrude_to_xy(this->point_to_gcode(pt), 0,"move inwards before travel",true); } return gcode; @@ -6313,7 +6147,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, bool slope_need_z_travel = false; if (sloped != nullptr && !sloped->is_flat()) { auto target_z = get_sloped_z(sloped->slope_begin.z_ratio); - slope_need_z_travel = m_writer.will_move_z(target_z); + slope_need_z_travel = m_writer->will_move_z(target_z); } // Move to first point of extrusion path // path is 2D. But in slope lift case, lift z is done in travel_to function. @@ -6336,7 +6170,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // if needed, write the gcode_label_objects_end then gcode_label_objects_start // should be already done by travel_to, but just in case - m_writer.add_object_change_labels(gcode); + m_writer->add_object_change_labels(gcode); // compensate retraction gcode += this->unretract(); @@ -6390,12 +6224,12 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } } - if (m_writer.get_gcode_flavor() == gcfKlipper) { - gcode += m_writer.set_accel_and_jerk(acceleration_i, jerk); + if (m_writer->get_gcode_flavor() == gcfKlipper) { + gcode += m_writer->set_accel_and_jerk(acceleration_i, jerk); } else { - gcode += m_writer.set_print_acceleration(acceleration_i); - gcode += m_writer.set_jerk_xy(jerk); + gcode += m_writer->set_print_acceleration(acceleration_i); + gcode += m_writer->set_jerk_xy(jerk); } // calculate effective extrusion length per distance unit (e_per_mm) @@ -6440,8 +6274,8 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } // Effective extrusion length per distance unit = (filament_flow_ratio/cross_section) * mm3_per_mm / print flow ratio - // m_writer.extruder()->e_per_mm3() below is (filament flow ratio / cross-sectional area) - double e_per_mm = m_writer.filament()->e_per_mm3() * _mm3_per_mm; + // m_writer->extruder()->e_per_mm3() below is (filament flow ratio / cross-sectional area) + double e_per_mm = m_writer->filament()->e_per_mm3() * _mm3_per_mm; e_per_mm /= filament_flow_ratio; // set speed @@ -6667,7 +6501,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index + 1)); config.set_key_value("layer_z", new ConfigOptionFloat(m_layer == nullptr ? m_last_height : m_layer->print_z)); gcode += this->placeholder_parser_process("change_extrusion_role_gcode", - m_config.change_extrusion_role_gcode.value, m_writer.filament()->id(), &config) + m_config.change_extrusion_role_gcode.value, m_writer->filament()->id(), &config) + "\n"; } @@ -6722,7 +6556,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, if (m_multi_flow_segment_path_average_mm3_per_mm > 0) { sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer.filament()->id(), + m_writer->filament()->id(), m_multi_flow_segment_path_average_mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -6735,7 +6569,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // to issue a zero flow PA change command for this sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer.filament()->id(), + m_writer->filament()->id(), _mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -6841,7 +6675,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer.filament()->id(), + m_writer->filament()->id(), _mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -6856,7 +6690,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer.filament()->id(), + m_writer->filament()->id(), _mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -6871,7 +6705,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // ORCA: End of adaptive PA code segment } - gcode += m_writer.set_speed(F, "", comment); + gcode += m_writer->set_speed(F, "", comment); { if (m_enable_cooling_markers) { if (enable_overhang_bridge_fan) { @@ -6889,7 +6723,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } // BBS: use G1 if not enable arc fitting or has no arc fitting result or in spiral_mode mode or we are doing sloped extrusion // Attention: G2 and G3 is not supported in spiral_mode mode - if (!m_config.enable_arc_fitting || path.polyline.fitting_result.empty() || m_config.spiral_mode || sloped != nullptr || m_config.belt_printer) { + if (!m_config.enable_arc_fitting || path.polyline.fitting_result.empty() || m_config.spiral_mode || sloped != nullptr || this->should_disable_arc_fitting()) { double path_length = 0.; double total_length = sloped == nullptr ? 0. : path.polyline.length() * SCALING_FACTOR; for (const Line& line : path.polyline.lines()) { @@ -6909,7 +6743,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } if (sloped == nullptr) { // Normal extrusion - gcode += m_writer.extrude_to_xy( + gcode += m_writer->extrude_to_xy( this->point_to_gcode(line.b), dE, GCodeWriter::full_gcode_comment ? tempDescription : "", path.is_force_no_extrusion()); @@ -6918,7 +6752,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, const auto [z_ratio, e_ratio] = sloped->interpolate(path_length / total_length); Vec2d dest2d = this->point_to_gcode(line.b); Vec3d dest3d(dest2d(0), dest2d(1), get_sloped_z(z_ratio)); - gcode += m_writer.extrude_to_xyz( + gcode += m_writer->extrude_to_xyz( dest3d, dE * e_ratio, GCodeWriter::full_gcode_comment ? tempDescription : "", path.is_force_no_extrusion()); @@ -6948,7 +6782,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, tempDescription += Slic3r::format(" | Old Flow Value: %0.5f Length: %0.5f",oldE, line_length); } } - gcode += m_writer.extrude_to_xy( + gcode += m_writer->extrude_to_xy( this->point_to_gcode(line.b), dE, GCodeWriter::full_gcode_comment ? tempDescription : "", path.is_force_no_extrusion()); @@ -6971,7 +6805,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, tempDescription += Slic3r::format(" | Old Flow Value: %0.5f Length: %0.5f",oldE, arc_length); } } - gcode += m_writer.extrude_arc_to_xy( + gcode += m_writer->extrude_arc_to_xy( this->point_to_gcode(arc.end_point), center_offset, dE, @@ -6999,7 +6833,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, Polyline l(p); total_length = l.length() * SCALING_FACTOR; } - gcode += m_writer.set_speed(last_set_speed, "", comment); + gcode += m_writer->set_speed(last_set_speed, "", comment); Vec2d prev = this->point_to_gcode_quantized(new_points[0].p); bool pre_fan_enabled = false; bool cur_fan_enabled = false; @@ -7049,7 +6883,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer.filament()->id(), + m_writer->filament()->id(), _mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -7064,7 +6898,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer.filament()->id(), + m_writer->filament()->id(), _mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -7081,10 +6915,10 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // Ignore small speed variations - emit speed change if the delta between current and new is greater than 60mm/min / 1mm/sec // Reset speed to F if delta to F is less than 1mm/sec if ((std::abs(last_set_speed - new_speed) > 60)) { - gcode += m_writer.set_speed(new_speed, "", comment); + gcode += m_writer->set_speed(new_speed, "", comment); last_set_speed = new_speed; } else if ((std::abs(F - new_speed) <= 60)) { - gcode += m_writer.set_speed(F, "", comment); + gcode += m_writer->set_speed(F, "", comment); last_set_speed = F; } auto dE = e_per_mm * line_length; @@ -7098,12 +6932,12 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } if (sloped == nullptr) { // Normal extrusion - gcode += m_writer.extrude_to_xy(p, dE, GCodeWriter::full_gcode_comment ? tempDescription : ""); + gcode += m_writer->extrude_to_xy(p, dE, GCodeWriter::full_gcode_comment ? tempDescription : ""); } else { // Sloped extrusion const auto [z_ratio, e_ratio] = sloped->interpolate(path_length / total_length); Vec3d dest3d(p(0), p(1), get_sloped_z(z_ratio)); - gcode += m_writer.extrude_to_xyz(dest3d, dE * e_ratio, GCodeWriter::full_gcode_comment ? tempDescription : ""); + gcode += m_writer->extrude_to_xyz(dest3d, dE * e_ratio, GCodeWriter::full_gcode_comment ? tempDescription : ""); } prev = p; @@ -7244,18 +7078,18 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string } } - if (m_writer.get_gcode_flavor() == gcfKlipper) { - gcode += m_writer.set_accel_and_jerk(acceleration_to_set, jerk_to_set); + if (m_writer->get_gcode_flavor() == gcfKlipper) { + gcode += m_writer->set_accel_and_jerk(acceleration_to_set, jerk_to_set); } else { - gcode += m_writer.set_travel_acceleration(acceleration_to_set); - gcode += m_writer.set_jerk_xy(jerk_to_set); + gcode += m_writer->set_travel_acceleration(acceleration_to_set); + gcode += m_writer->set_jerk_xy(jerk_to_set); } // if a retraction would be needed, try to use reduce_crossing_wall to plan a // multi-hop travel path inside the configuration space if (m_config.reduce_crossing_wall && !m_avoid_crossing_perimeters.disabled_once() - && m_writer.is_current_position_clear()) + && m_writer->is_current_position_clear()) //BBS: don't generate detour travel paths when current position is unclea { travel = m_avoid_crossing_perimeters.travel_to(*this, point, &could_be_wipe_disabled); @@ -7302,7 +7136,7 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string // if needed, write the gcode_label_objects_end then gcode_label_objects_start - m_writer.add_object_change_labels(gcode); + m_writer->add_object_change_labels(gcode); // use G1 because we rely on paths being straight (G0 may make round paths) if (travel.size() >= 2) { @@ -7310,14 +7144,14 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string if (false/*m_spiral_vase*/) { // No lazy z lift for spiral vase mode for (size_t i = 1; i < travel.size(); ++i) { - gcode += m_writer.travel_to_xy(this->point_to_gcode(travel.points[i]), comment); + gcode += m_writer->travel_to_xy(this->point_to_gcode(travel.points[i]), comment); } } else { if (travel.size() == 2) { // No extra movements emitted by avoid_crossing_perimeters, simply move to the end point with z change const auto& dest2d = this->point_to_gcode(travel.points.back()); Vec3d dest3d(dest2d(0), dest2d(1), z == DBL_MAX ? m_nominal_z : z); - gcode += m_writer.travel_to_xyz(dest3d, comment, m_need_change_layer_lift_z); + gcode += m_writer->travel_to_xyz(dest3d, comment, m_need_change_layer_lift_z); m_need_change_layer_lift_z = false; } else { // Extra movements emitted by avoid_crossing_perimeters, lift the z to normal height at the beginning, then apply the z @@ -7327,16 +7161,16 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string // Lift to normal z at beginning Vec2d dest2d = this->point_to_gcode(travel.points[i]); Vec3d dest3d(dest2d(0), dest2d(1), m_nominal_z); - gcode += m_writer.travel_to_xyz(dest3d, comment, m_need_change_layer_lift_z); + gcode += m_writer->travel_to_xyz(dest3d, comment, m_need_change_layer_lift_z); m_need_change_layer_lift_z = false; } else if (z != DBL_MAX && i == travel.size() - 1) { // Apply z_ratio for the very last point Vec2d dest2d = this->point_to_gcode(travel.points[i]); Vec3d dest3d(dest2d(0), dest2d(1), z); - gcode += m_writer.travel_to_xyz(dest3d, comment); + gcode += m_writer->travel_to_xyz(dest3d, comment); } else { // For all points in between, no z change - gcode += m_writer.travel_to_xy(this->point_to_gcode(travel.points[i]), comment); + gcode += m_writer->travel_to_xy(this->point_to_gcode(travel.points[i]), comment); } } } @@ -7439,7 +7273,7 @@ bool GCode::needs_retraction(const Polyline &travel, ExtrusionRole role, LiftTyp if (clipped_travel.length() > travel_len_thresh) clipped_travel.points.back() = clipped_travel.points.front()+(clipped_travel.points.back() - clipped_travel.points.front()) * (travel_len_thresh / clipped_travel.length()); //BBS: translate to current plate coordinate system - clipped_travel.translate(Point::new_scale(double(m_origin.x() - m_writer.get_xy_offset().x()), double(m_origin.y() - m_writer.get_xy_offset().y()))); + clipped_travel.translate(Point::new_scale(double(m_origin.x() - m_writer->get_xy_offset().x()), double(m_origin.y() - m_writer->get_xy_offset().y()))); //BBS: force to retract when leave from external perimeter for a long travel //Better way is judging whether the travel move direction is same with last extrusion move. @@ -7491,13 +7325,13 @@ std::string GCode::retract(bool toolchange, bool is_last_retraction, LiftType li { std::string gcode; - if (m_writer.filament() == nullptr) + if (m_writer->filament() == nullptr) return gcode; // wipe (if it's enabled for this extruder and we have a stored wipe path and no-zero wipe distance) if (FILAMENT_CONFIG(wipe) && m_wipe.has_path() && scale_(FILAMENT_CONFIG(wipe_distance)) > SCALED_EPSILON) { Wipe::RetractionValues wipeRetractions = m_wipe.calculateWipeRetractionLengths(*this, toolchange); - gcode += toolchange ? m_writer.retract_for_toolchange(true,wipeRetractions.retractLengthBeforeWipe) : m_writer.retract(true, wipeRetractions.retractLengthBeforeWipe); + gcode += toolchange ? m_writer->retract_for_toolchange(true,wipeRetractions.retractLengthBeforeWipe) : m_writer->retract(true, wipeRetractions.retractLengthBeforeWipe); gcode += m_wipe.wipe(*this,wipeRetractions.retractLengthDuringWipe, toolchange, is_last_retraction); } @@ -7507,14 +7341,14 @@ std::string GCode::retract(bool toolchange, bool is_last_retraction, LiftType li length is honored in case wipe path was too short. */ if ((!this->on_first_layer() || this->config().bottom_surface_pattern != InfillPattern::ipHilbertCurve) && (role != erTopSolidInfill || this->config().top_surface_pattern != InfillPattern::ipHilbertCurve)) - gcode += toolchange ? m_writer.retract_for_toolchange() : m_writer.retract(); + gcode += toolchange ? m_writer->retract_for_toolchange() : m_writer->retract(); - gcode += m_writer.reset_e(); + gcode += m_writer->reset_e(); // Orca: check if should + can lift (roughly from SuperSlicer) RetractLiftEnforceType retract_lift_type = RetractLiftEnforceType(EXTRUDER_CONFIG(retract_lift_enforce)); bool needs_lift = toolchange - || m_writer.filament()->retraction_length() > 0 + || m_writer->filament()->retraction_length() > 0 || m_config.use_firmware_retraction; bool last_fill_extrusion_role_top_infill = (this->m_last_notgapfill_extrusion_role == ExtrusionRole::erTopSolidInfill || this->m_last_notgapfill_extrusion_role == ExtrusionRole::erIroning); @@ -7537,9 +7371,9 @@ std::string GCode::retract(bool toolchange, bool is_last_retraction, LiftType li if (needs_lift && can_lift) { if (apply_instantly) - gcode += m_writer.eager_lift(lift_type); + gcode += m_writer->eager_lift(lift_type); else - gcode += m_writer.lazy_lift(lift_type, m_spiral_vase != nullptr); + gcode += m_writer->lazy_lift(lift_type, m_spiral_vase != nullptr); } return gcode; @@ -7548,11 +7382,11 @@ std::string GCode::retract(bool toolchange, bool is_last_retraction, LiftType li std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bool by_object, int toolchange_temp_override) { int new_extruder_id = get_extruder_id(new_filament_id); - if (!m_writer.need_toolchange(new_filament_id)) + if (!m_writer->need_toolchange(new_filament_id)) return ""; // if we are running a single-extruder setup, just set the extruder and return nothing - if (!m_writer.multiple_extruders) { + if (!m_writer->multiple_extruders) { this->placeholder_parser().set("current_extruder", new_filament_id); this->placeholder_parser().set("retraction_distance_when_ec", m_config.retraction_distances_when_ec.get_at(new_filament_id)); this->placeholder_parser().set("long_retraction_when_ec", m_config.long_retractions_when_ec.get_at(new_filament_id)); @@ -7575,13 +7409,13 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo check_add_eol(gcode); } if (m_config.enable_pressure_advance.get_at(new_filament_id)) { - gcode += m_writer.set_pressure_advance(m_config.pressure_advance.get_at(new_filament_id)); + gcode += m_writer->set_pressure_advance(m_config.pressure_advance.get_at(new_filament_id)); // Orca: Adaptive PA // Reset Adaptive PA processor last PA value m_pa_processor->resetPreviousPA(m_config.pressure_advance.get_at(new_filament_id)); } - gcode += m_writer.toolchange(new_filament_id); + gcode += m_writer->toolchange(new_filament_id); return gcode; } @@ -7596,18 +7430,18 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo // BBS: insert skip object label before change filament while by object if (by_object) - m_writer.add_object_change_labels(gcode); + m_writer->add_object_change_labels(gcode); bool add_change_filament_624 = false; - if (m_writer.filament() != nullptr) { + if (m_writer->filament() != nullptr) { // Process the custom filament_end_gcode. set_extruder() is only called if there is no wipe tower // so it should not be injected twice. - unsigned int old_filament_id = m_writer.filament()->id(); + unsigned int old_filament_id = m_writer->filament()->id(); const std::string &filament_end_gcode = m_config.filament_end_gcode.get_at(old_filament_id); if (! filament_end_gcode.empty()) { DynamicConfig config; config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index)); - config.set_key_value("layer_z", new ConfigOptionFloat(m_writer.get_position().z() - m_config.z_offset.value)); + config.set_key_value("layer_z", new ConfigOptionFloat(m_writer->get_position().z() - m_config.z_offset.value)); config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); config.set_key_value("filament_extruder_id", new ConfigOptionInt(int(get_extruder_id(old_filament_id)))); if (!m_filament_instances_code.empty()) { @@ -7623,7 +7457,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo // If ooze prevention is enabled, park current extruder in the nearest // standby point and set it to the standby temperature. - if (m_ooze_prevention.enable && m_writer.filament() != nullptr) + if (m_ooze_prevention.enable && m_writer->filament() != nullptr) gcode += m_ooze_prevention.pre_toolchange(*this); // BBS @@ -7636,23 +7470,23 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo if (toolchange_temp_override > 0) new_filament_temp = toolchange_temp_override; - Vec3d nozzle_pos = m_writer.get_position(); + Vec3d nozzle_pos = m_writer->get_position(); float old_retract_length, old_retract_length_toolchange, wipe_volume; int old_filament_temp, old_filament_e_feedrate; float filament_area = float((M_PI / 4.f) * pow(m_config.filament_diameter.get_at(new_filament_id), 2)); //BBS: add handling for filament change in start gcode int old_filament_id = -1; - if (m_writer.filament() != nullptr || m_start_gcode_filament != -1) { + if (m_writer->filament() != nullptr || m_start_gcode_filament != -1) { std::vector flush_matrix(cast(get_flush_volumes_matrix(m_config.flush_volumes_matrix.values, new_extruder_id, m_config.nozzle_diameter.values.size()))); const unsigned int number_of_extruders = (unsigned int) (m_config.filament_colour.values.size()); // if is multi_extruder only use the fist extruder matrix - if (m_writer.filament() != nullptr) - assert(m_writer.filament()->id() < number_of_extruders); + if (m_writer->filament() != nullptr) + assert(m_writer->filament()->id() < number_of_extruders); else assert(m_start_gcode_filament < number_of_extruders); - old_filament_id = m_writer.filament() != nullptr ? m_writer.filament()->id() : m_start_gcode_filament; - int old_extruder_id = m_writer.filament() != nullptr ? m_writer.filament()->extruder_id() : get_extruder_id(m_start_gcode_filament); + old_filament_id = m_writer->filament() != nullptr ? m_writer->filament()->id() : m_start_gcode_filament; + int old_extruder_id = m_writer->filament() != nullptr ? m_writer->filament()->extruder_id() : get_extruder_id(m_start_gcode_filament); old_retract_length = m_config.retraction_length.get_at(old_filament_id); old_retract_length_toolchange = m_config.retract_length_toolchange.get_at(old_filament_id); @@ -7662,7 +7496,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo float grab_purge_volume = m_config.grab_length.get_at(new_extruder_id) * 2.4; if (old_extruder_id != new_extruder_id) { //calc flush volume between the same extruder id - int old_filament_id_in_new_extruder = m_writer.filament(new_extruder_id) != nullptr ? m_writer.filament(new_extruder_id)->id() : -1; + int old_filament_id_in_new_extruder = m_writer->filament(new_extruder_id) != nullptr ? m_writer->filament(new_extruder_id)->id() : -1; if (old_filament_id_in_new_extruder == -1) wipe_volume = 0; else { @@ -7798,13 +7632,13 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo //BBS: gcode writer doesn't know where the extruder is and whether fan speed is changed after inserting tool change gcode //Set this flag so that normal lift will be used the first time after tool change. gcode += ";_FORCE_RESUME_FAN_SPEED\n"; - m_writer.set_current_position_clear(false); + m_writer->set_current_position_clear(false); //BBS: check whether custom gcode changes the z position. Update if changed double temp_z_after_tool_change; if (GCodeProcessor::get_last_z_from_gcode(toolchange_gcode_parsed, temp_z_after_tool_change)) { - Vec3d pos = m_writer.get_position(); + Vec3d pos = m_writer->get_position(); pos(2) = temp_z_after_tool_change; - m_writer.set_position(pos); + m_writer->set_position(pos); } } } @@ -7812,13 +7646,13 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo // BBS. Reset old extruder E-value. // Keep retract length because Custom GCode will guarantee retract length be the same as toolchange if (m_config.single_extruder_multi_material) { - m_writer.reset_e(); + m_writer->reset_e(); } //BBS: don't add T[next extruder] if there is no T cmd on filament change //We inform the writer about what is happening, but we may not use the resulting gcode. - std::string toolchange_command = m_writer.toolchange(new_filament_id); - if (!custom_gcode_changes_tool(toolchange_gcode_parsed, m_writer.toolchange_prefix(), new_filament_id)) + std::string toolchange_command = m_writer->toolchange(new_filament_id); + if (!custom_gcode_changes_tool(toolchange_gcode_parsed, m_writer->toolchange_prefix(), new_filament_id)) gcode += toolchange_command; else { // user provided his own toolchange gcode, no need to do anything @@ -7829,7 +7663,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo int temp = (m_layer_index <= 0 ? m_config.nozzle_temperature_initial_layer.get_at(new_filament_id) : m_config.nozzle_temperature.get_at(new_filament_id)); - gcode += m_writer.set_temperature(temp, false); + gcode += m_writer->set_temperature(temp, false); } this->placeholder_parser().set("current_extruder", new_filament_id); @@ -7873,7 +7707,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo gcode += m_ooze_prevention.post_toolchange(*this); if (m_config.enable_pressure_advance.get_at(new_filament_id)) { - gcode += m_writer.set_pressure_advance(m_config.pressure_advance.get_at(new_filament_id)); + gcode += m_writer->set_pressure_advance(m_config.pressure_advance.get_at(new_filament_id)); // Orca: Adaptive PA // Reset Adaptive PA processor last PA value m_pa_processor->resetPreviousPA(m_config.pressure_advance.get_at(new_filament_id)); @@ -7959,7 +7793,7 @@ Vec2d GCode::point_to_gcode(const Point &point) const Point GCode::gcode_to_point(const Vec2d &point) const { Vec2d pt = point - m_origin; - if (const Extruder *extruder = m_writer.filament(); extruder) + if (const Extruder *extruder = m_writer->filament(); extruder) // This function may be called at the very start from toolchange G-code when the extruder is not assigned yet. pt += m_config.extruder_offset.get_at(extruder->extruder_id()); return scaled(pt); diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index dbec7d0315..9004f30fd3 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -4,6 +4,7 @@ #include "libslic3r.h" #include "ExPolygon.hpp" #include "GCodeWriter.hpp" +#include "BeltGCodeWriter.hpp" #include "Layer.hpp" #include "Point.hpp" #include "PlaceholderParser.hpp" @@ -206,16 +207,18 @@ public: m_last_obj_copy(nullptr, Point(std::numeric_limits::max(), std::numeric_limits::max())), // BBS m_toolchange_count(0), - m_nominal_z(0.) + m_nominal_z(0.), + m_writer(std::make_unique()) {} - ~GCode() = default; + virtual ~GCode() = default; +public: // throws std::runtime_exception on error, // throws CanceledException through print->throw_if_canceled(). void do_export(Print* print, const char* path, GCodeProcessorResult* result = nullptr, ThumbnailsGeneratorCallback thumbnail_cb = nullptr); void export_layer_filaments(GCodeProcessorResult* result); //BBS: set offset for gcode writer - void set_gcode_offset(double x, double y) { m_writer.set_xy_offset(x, y); m_processor.set_xy_offset(x, y);} + void set_gcode_offset(double x, double y) { m_writer->set_xy_offset(x, y); m_processor.set_xy_offset(x, y);} // Exported for the helper classes (OozePrevention, Wipe) and for the Perl binding for unit tests. const Vec2d& origin() const { return m_origin; } @@ -227,8 +230,8 @@ public: Vec2d point_to_gcode_quantized(const Point& point) const; const FullPrintConfig &config() const { return m_config; } const Layer* layer() const { return m_layer; } - GCodeWriter& writer() { return m_writer; } - const GCodeWriter& writer() const { return m_writer; } + GCodeWriter& writer() { return *m_writer; } + const GCodeWriter& writer() const { return *m_writer; } PlaceholderParser& placeholder_parser() { return m_placeholder_parser_integration.parser; } const PlaceholderParser& placeholder_parser() const { return m_placeholder_parser_integration.parser; } // Process a template through the placeholder parser, collect error messages to be reported @@ -248,7 +251,7 @@ public: std::string travel_to(const Point& point, ExtrusionRole role, std::string comment, double z = DBL_MAX); bool needs_retraction(const Polyline& travel, ExtrusionRole role, LiftType& lift_type); std::string retract(bool toolchange = false, bool is_last_retraction = false, LiftType lift_type = LiftType::NormalLift, bool apply_instantly = false, ExtrusionRole role = erNone); - std::string unretract() { return m_writer.unlift() + m_writer.unretract(); } + std::string unretract() { return m_writer->unlift() + m_writer->unretract(); } std::string set_extruder(unsigned int extruder_id, double print_z, bool by_object=false, int toolchange_temp_override = -1); bool is_BBL_Printer(); WipeTowerType wipe_tower_type(); @@ -300,7 +303,7 @@ public: } }; -private: +protected: class GCodeOutputStream { public: GCodeOutputStream(FILE *f, GCodeProcessor &processor) : f(f), m_processor(processor) {} @@ -328,9 +331,17 @@ private: FILE *f = nullptr; GCodeProcessor &m_processor; }; + + // Virtual hooks for belt printer subclass (BeltGCode). + // No-ops in base GCode; overridden in BeltGCode. + virtual void init_belt_writer(Print &print, bool is_bbl_printers) {} + virtual void write_belt_header(GCodeOutputStream &file, const Print &print) {} + virtual void on_set_origin(const PrintObject *obj, const Point &inst_shift) {} + virtual bool should_disable_arc_fitting() const { return false; } + void _do_export(Print &print, GCodeOutputStream &file, ThumbnailsGeneratorCallback thumbnail_cb); - static std::vector collect_layers_to_print(const PrintObject &object); + static std::vector collect_layers_to_print(const PrintObject &object, bool skip_empty_first_layer = false); static std::vector>> collect_layers_to_print(const Print &print); std::string generate_skirt(const Print &print, @@ -493,16 +504,11 @@ private: This affects the input arguments supplied to the extrude*() and travel_to() methods. */ Vec2d m_origin; - // Per-axis origin snap: shift G-code so each object's bbox min = offset. - bool m_origin_snap[3] = {false, false, false}; - double m_origin_snap_offset[3] = {0., 0., 0.}; - // Called when switching instances to recompute the writer's snap for this instance. - void update_origin_snap(const PrintObject *obj, const Point &inst_shift); FullPrintConfig m_config; DynamicConfig m_calib_config; // scaled G-code resolution double m_scaled_resolution; - GCodeWriter m_writer; + std::unique_ptr m_writer; struct PlaceholderParserIntegration { void reset(); diff --git a/src/libslic3r/GCode/BeltBackTransform.cpp b/src/libslic3r/GCode/BeltBackTransform.cpp index 7cd5bc6b53..2e2cd26f75 100644 --- a/src/libslic3r/GCode/BeltBackTransform.cpp +++ b/src/libslic3r/GCode/BeltBackTransform.cpp @@ -1,40 +1,8 @@ #include "BeltBackTransform.hpp" -#include "../Geometry.hpp" -#include +#include "../BeltTransform.hpp" namespace Slic3r { -// Keep in sync with PrintObjectSlice.cpp compute_shear_factor (lines ~147-157). -static double compute_shear_factor(BeltShearMode mode, double angle_deg) -{ - double angle_rad = Geometry::deg2rad(angle_deg); - double sin_a = std::sin(angle_rad); - double cos_a = std::cos(angle_rad); - switch (mode) { - case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; - case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; - case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; - case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; - default: return 0.; - } -} - -// Keep in sync with PrintObjectSlice.cpp compute_scale_factor (lines ~180-192). -static double compute_scale_factor(BeltScaleMode mode, double angle_deg) -{ - if (mode == BeltScaleMode::None) return 1.; - double angle_rad = Geometry::deg2rad(angle_deg); - double sin_a = std::sin(angle_rad); - double cos_a = std::cos(angle_rad); - switch (mode) { - case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; - case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; - case BeltScaleMode::Sin: return sin_a; - case BeltScaleMode::Cos: return cos_a; - default: return 1.; - } -} - bool BeltBackTransform::init_from_config(const PrintConfig &config) { m_active = false; @@ -43,99 +11,19 @@ bool BeltBackTransform::init_from_config(const PrintConfig &config) if (!config.belt_printer.value || !config.belt_gcode_back_transform.value) return false; - // --- Pre-slice axis remap (same as PrintObjectSlice.cpp) --- - int pre_rx = int(config.belt_preslice_remap_x.value); - int pre_ry = int(config.belt_preslice_remap_y.value); - int pre_rz = int(config.belt_preslice_remap_z.value); - - bool has_preslice_remap = (pre_rx != int(BeltRemapAxis::PosX) || - pre_ry != int(BeltRemapAxis::PosY) || - pre_rz != int(BeltRemapAxis::PosZ)); - // Require at least one active transform to proceed. bool has_global_shear = config.belt_shear_x_global.value || config.belt_shear_y_global.value || config.belt_shear_z_global.value; - if (!has_global_shear && !has_preslice_remap) + if (!has_global_shear && !BeltTransformPipeline::has_preslice_remap(config)) return false; - // Build pre-slice remap matrix. - Transform3d pre_remap = Transform3d::Identity(); - if (has_preslice_remap) { - auto remap_column = [](int r) -> Vec3d { - int axis = r % 3; - Vec3d col = Vec3d::Zero(); - if (r < 3) col[axis] = 1.0; - else if (r < 6) col[axis] = -1.0; - else col[axis] = -1.0; // Rev: max - pos - return col; - }; - - Matrix3d remap_lin; - remap_lin.col(0) = remap_column(pre_rx); - remap_lin.col(1) = remap_column(pre_ry); - remap_lin.col(2) = remap_column(pre_rz); - pre_remap.linear() = remap_lin; - - // Rev mode translation (needs build volume extents). - Vec3d remap_trans = Vec3d::Zero(); - if (pre_rx >= 6 || pre_ry >= 6 || pre_rz >= 6) { - BoundingBoxf bbox_bed(config.printable_area.values); - Vec3d vol_max(bbox_bed.max.x(), bbox_bed.max.y(), - config.printable_height.value); - auto add_rev = [&](int r, int out) { - if (r >= 6) remap_trans[out] = vol_max[r % 3]; - }; - add_rev(pre_rx, 0); - add_rev(pre_ry, 1); - add_rev(pre_rz, 2); - } - pre_remap.translation() = remap_trans; - } - - // Build per-axis shear matrix (same as PrintObjectSlice.cpp). - struct AxisShear { BeltShearMode mode; double angle; int from; }; - AxisShear axes[3] = { - { config.belt_shear_x.value, config.belt_shear_x_angle.value, int(config.belt_shear_x_from.value) }, - { config.belt_shear_y.value, config.belt_shear_y_angle.value, int(config.belt_shear_y_from.value) }, - { config.belt_shear_z.value, config.belt_shear_z_angle.value, int(config.belt_shear_z_from.value) }, - }; - - Matrix3d shear = Matrix3d::Identity(); - bool has_shear = false; - for (int row = 0; row < 3; ++row) { - if (axes[row].mode != BeltShearMode::None) { - double factor = compute_shear_factor(axes[row].mode, axes[row].angle); - if (std::abs(factor) > EPSILON) { - shear(row, axes[row].from) += factor; - has_shear = true; - } - } - } - - // Build per-axis scale diagonal matrix (same as PrintObjectSlice.cpp). - double sx = compute_scale_factor(config.belt_scale_x.value, config.belt_scale_x_angle.value); - double sy = compute_scale_factor(config.belt_scale_y.value, config.belt_scale_y_angle.value); - double sz = compute_scale_factor(config.belt_scale_z.value, config.belt_scale_z_angle.value); - - Matrix3d scale = Matrix3d::Identity(); - bool has_scale = (std::abs(sx - 1.) > EPSILON || - std::abs(sy - 1.) > EPSILON || - std::abs(sz - 1.) > EPSILON); - if (has_scale) { - scale(0, 0) = sx; - scale(1, 1) = sy; - scale(2, 2) = sz; - } - - if (!has_shear && !has_scale && !has_preslice_remap) + // Build the forward pipeline (scale * shear * pre_remap) and store its inverse. + Transform3d forward = BeltTransformPipeline::build_forward_transform(config); + if (forward.isApprox(Transform3d::Identity())) return false; - // Forward pipeline: scale * shear * pre_remap (same order as PrintObjectSlice.cpp). - Transform3d combined = Transform3d::Identity(); - combined.linear() = scale * shear; - combined = combined * pre_remap; - m_inverse = combined.inverse(); + m_inverse = forward.inverse(); m_active = true; return true; } diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index 8c2f7122b8..fea5509bc3 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -20,59 +20,6 @@ namespace Slic3r { bool GCodeWriter::full_gcode_comment = true; -void GCodeWriter::set_belt_angle(double angle_deg) -{ - m_belt_angle_rad = Geometry::deg2rad(angle_deg); -} - -void GCodeWriter::set_axis_remap(int rx, int ry, int rz) -{ - m_remap_x = rx; - m_remap_y = ry; - m_remap_z = rz; -} - -void GCodeWriter::set_build_volume_max(const Vec3d &max) -{ - m_build_vol_max = max; -} - -void GCodeWriter::set_belt_back_transform(const PrintConfig &config) -{ - m_belt_back_transform.init_from_config(config); -} - -void GCodeWriter::set_origin_snap(int axis, bool enable, double offset, double bbox_min) -{ - if (axis >= 0 && axis < 3) { - m_origin_snap[axis] = enable; - m_origin_offset[axis] = offset; - m_origin_bbox_min[axis] = bbox_min; - } -} - -Vec3d GCodeWriter::to_machine_coords(const Vec3d &pos) const -{ - if (!is_belt_printer()) - return pos; - // Step 1: Undo the shear/scale applied during slicing. - Vec3d p = m_belt_back_transform.apply(pos); - // Step 2: Apply axis remapping for the machine's coordinate convention. - // BeltRemapAxis: 0-2 = +X/+Y/+Z, 3-5 = -X/-Y/-Z, 6-8 = Rev X/Y/Z - auto remap = [this, &p](int r) -> double { - int axis = r % 3; - if (r < 3) return p[axis]; - if (r < 6) return -p[axis]; - return m_build_vol_max[axis] - p[axis]; - }; - Vec3d result = { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) }; - // Per-axis origin snap: shift so bbox min on each enabled axis = offset. - for (int i = 0; i < 3; ++i) - if (m_origin_snap[i]) - result[i] -= (m_origin_bbox_min[i] - m_origin_offset[i]); - return result; -} - bool GCodeWriter::supports_separate_travel_acceleration(GCodeFlavor flavor) { return (flavor == gcfRepetier || flavor == gcfMarlinFirmware || flavor == gcfRepRapFirmware); @@ -601,13 +548,7 @@ std::string GCodeWriter::travel_to_xy(const Vec2d &point, const std::string &com Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; GCodeG1Formatter w; - if (is_belt_printer()) { - // Belt printer: transform to machine coordinates (XY travel also needs Z due to YZ rotation) - Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); - w.emit_xyz(machine); - } else { - w.emit_xy(point_on_plate); - } + w.emit_xy(point_on_plate); auto speed = m_is_first_layer ? this->config.get_abs_value("initial_layer_travel_speed") : this->config.travel_speed.value; w.emit_f(speed * 60.0); @@ -621,11 +562,6 @@ it will not perform subsequent lifts, even if Z was raised manually (i.e. with travel_to_z()) and thus _lifted was reduced. */ std::string GCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase) { - // Belt printer: force NormalLift since SpiralLift and SlopeLift compute slope angles - // that don't account for the YZ coordinate rotation. - if (is_belt_printer()) - lift_type = LiftType::NormalLift; - // check whether the above/below conditions are met double target_lift = 0; { @@ -654,8 +590,7 @@ std::string GCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase) // BBS: immediately execute an undelayed lift move with a spiral lift pattern // designed specifically for subsequent gcode injection (e.g. timelapse) std::string GCodeWriter::eager_lift(const LiftType type) { - // Belt printer: force NormalLift (SpiralLift/SlopeLift don't account for YZ rotation). - const LiftType effective_type = is_belt_printer() ? LiftType::NormalLift : type; + const LiftType effective_type = type; std::string lift_move; double target_lift = 0; { @@ -742,8 +677,6 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co // / to make the z list early to avoid to hit some warping place when travel is long. Vec2d temp = delta_no_z.normalized() * delta(2) / tan(this->filament()->travel_slope()); Vec3d slope_top_point = Vec3d(temp(0), temp(1), delta(2)) + source; - if (is_belt_printer()) - slope_top_point = to_machine_coords(slope_top_point); GCodeG1Formatter w0; w0.emit_xyz(slope_top_point); w0.emit_f(travel_speed * 60.0); @@ -758,27 +691,18 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co std::string xy_z_move; { - Vec3d emit_target = is_belt_printer() ? to_machine_coords(target) : target; GCodeG1Formatter w0; if (this->is_current_position_clear()) { - w0.emit_xyz(emit_target); + w0.emit_xyz(target); w0.emit_f(travel_speed * 60.0); w0.emit_comment(GCodeWriter::full_gcode_comment, comment); xy_z_move = w0.string(); } else { - if (is_belt_printer()) { - // Belt mode: can't split XY and Z moves independently, emit full XYZ - w0.emit_xyz(emit_target); - w0.emit_f(travel_speed * 60.0); - w0.emit_comment(GCodeWriter::full_gcode_comment, comment); - xy_z_move = w0.string(); - } else { - w0.emit_xy(Vec2d(target.x(), target.y())); - w0.emit_f(travel_speed * 60.0); - w0.emit_comment(GCodeWriter::full_gcode_comment, comment); - xy_z_move = w0.string() + _travel_to_z(target.z(), comment); - } + w0.emit_xy(Vec2d(target.x(), target.y())); + w0.emit_f(travel_speed * 60.0); + w0.emit_comment(GCodeWriter::full_gcode_comment, comment); + xy_z_move = w0.string() + _travel_to_z(target.z(), comment); } } m_pos = dest_point; @@ -804,25 +728,15 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co //BBS: take plate offset into consider Vec3d point_on_plate = { dest_point(0) - m_x_offset, dest_point(1) - m_y_offset, dest_point(2) }; - if (is_belt_printer()) - point_on_plate = to_machine_coords(point_on_plate); std::string out_string; GCodeG1Formatter w; if (!this->is_current_position_clear()) { - if (is_belt_printer()) { - // Belt mode: emit full XYZ since Y and Z are coupled - w.emit_xyz(point_on_plate); - w.emit_f(this->config.travel_speed.value * 60.0); - w.emit_comment(GCodeWriter::full_gcode_comment, comment); - out_string = w.string(); - } else { - //force to move xy first then z after filament change - w.emit_xy(Vec2d(point_on_plate.x(), point_on_plate.y())); - w.emit_f(this->config.travel_speed.value * 60.0); - w.emit_comment(GCodeWriter::full_gcode_comment, comment); - out_string = w.string() + _travel_to_z(point_on_plate.z(), comment); - } + //force to move xy first then z after filament change + w.emit_xy(Vec2d(point_on_plate.x(), point_on_plate.y())); + w.emit_f(this->config.travel_speed.value * 60.0); + w.emit_comment(GCodeWriter::full_gcode_comment, comment); + out_string = w.string() + _travel_to_z(point_on_plate.z(), comment); } else { GCodeG1Formatter w; w.emit_xyz(point_on_plate); @@ -866,13 +780,7 @@ std::string GCodeWriter::_travel_to_z(double z, const std::string &comment) } GCodeG1Formatter w; - if (is_belt_printer()) { - // Belt printer: a Z-only move in slicing frame needs to emit both Y and Z in machine coords. - Vec3d machine = to_machine_coords(Vec3d(m_pos.x() - m_x_offset, m_pos.y() - m_y_offset, z)); - w.emit_xyz(machine); - } else { - w.emit_z(z); - } + w.emit_z(z); w.emit_f(speed * 60.0); //BBS w.emit_comment(GCodeWriter::full_gcode_comment, comment); @@ -977,12 +885,7 @@ std::string GCodeWriter::extrude_to_xy(const Vec2d &point, double dE, const std: Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; GCodeG1Formatter w; - if (is_belt_printer()) { - Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); - w.emit_xyz(machine); - } else { - w.emit_xy(point_on_plate); - } + w.emit_xy(point_on_plate); if (!force_no_extrusion) w.emit_e(filament()->E()); //BBS @@ -1023,8 +926,6 @@ std::string GCodeWriter::extrude_to_xyz(const Vec3d &point, double dE, const std Vec3d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset, point(2) }; GCodeG1Formatter w; - if (is_belt_printer()) - point_on_plate = to_machine_coords(point_on_plate); w.emit_xyz(point_on_plate); if (!force_no_extrusion) w.emit_e(filament()->E()); diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index 2334f5d416..c96c3ca2c1 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -8,12 +8,11 @@ #include "Point.hpp" #include "PrintConfig.hpp" #include "GCode/CoolingBuffer.hpp" -#include "GCode/BeltBackTransform.hpp" - namespace Slic3r { class GCodeWriter { public: + virtual ~GCodeWriter() = default; GCodeConfig config; bool multiple_extruders; @@ -74,21 +73,21 @@ public: std::string set_speed(double F, const std::string &comment = std::string(), const std::string &cooling_marker = std::string()); // SoftFever NOTE: the returned speed is mm/minute double get_current_speed() const { return m_current_speed;} - std::string travel_to_xy(const Vec2d &point, const std::string &comment = std::string()); - std::string travel_to_xyz(const Vec3d &point, const std::string &comment = std::string(), bool force_z = false); + virtual std::string travel_to_xy(const Vec2d &point, const std::string &comment = std::string()); + virtual std::string travel_to_xyz(const Vec3d &point, const std::string &comment = std::string(), bool force_z = false); std::string travel_to_z(double z, const std::string &comment = std::string(), bool force = false); bool will_move_z(double z) const; - std::string extrude_to_xy(const Vec2d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false); + virtual std::string extrude_to_xy(const Vec2d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false); //BBS: generate G2 or G3 extrude which moves by arc std::string extrude_arc_to_xy(const Vec2d &point, const Vec2d ¢er_offset, double dE, const bool is_ccw, const std::string &comment = std::string(), bool force_no_extrusion = false); - std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false); + virtual std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false); std::string retract(bool before_wipe = false, double retract_length = 0); std::string retract_for_toolchange(bool before_wipe = false, double retract_length = 0); std::string unretract(); // do lift instantly - std::string eager_lift(const LiftType type); + virtual std::string eager_lift(const LiftType type); // record a lift request, do realy lift in next travel - std::string lazy_lift(LiftType lift_type = LiftType::NormalLift, bool spiral_vase = false); + virtual std::string lazy_lift(LiftType lift_type = LiftType::NormalLift, bool spiral_vase = false); std::string unlift(); const Vec3d& get_position() const { return m_pos; } Vec3d& get_position() { return m_pos; } @@ -126,23 +125,23 @@ public: void set_is_first_layer(bool bval) { m_is_first_layer = bval; } GCodeFlavor get_gcode_flavor() const { return config.gcode_flavor; } - // Belt printer: set the belt angle and precompute sin/cos for coordinate transformation. - void set_belt_angle(double angle_deg); - bool is_belt_printer() const { return m_belt_angle_rad != 0.; } - // Set axis remap for G-code output (BeltRemapAxis enum values). - void set_axis_remap(int rx, int ry, int rz); - // Set build volume extents for Rev remap mode (max X, Y, Z). - void set_build_volume_max(const Vec3d &max); - // Initialize the belt back-transform that undoes slicing shear/scale. - void set_belt_back_transform(const PrintConfig &config); - // Set per-axis origin snap: shifts G-code so bbox min on this axis = offset. - void set_origin_snap(int axis, bool enable, double offset, double bbox_min); - // Transform a point from the slicing frame to machine coordinates. - Vec3d to_machine_coords(const Vec3d &pos) const; - // Returns whether this flavor supports separate print and travel acceleration. static bool supports_separate_travel_acceleration(GCodeFlavor flavor); - private: +protected: + // Position/lift/offset state — accessible to subclasses (e.g. BeltGCodeWriter) + Vec3d m_pos = Vec3d::Zero(); + double m_x_offset{ 0 }; + double m_y_offset{ 0 }; + double m_lifted; + double m_to_lift; + LiftType m_to_lift_type; + bool m_is_first_layer = true; + bool m_is_current_pos_clear = false; + double m_current_speed; + + virtual std::string _travel_to_z(double z, const std::string &comment); + +private: // Extruders are sorted by their ID, so that binary search is possible. std::vector m_filament_extruders; bool m_single_extruder_multi_material; @@ -170,48 +169,21 @@ public: unsigned int m_last_additional_fan_speed; int m_last_bed_temperature; bool m_last_bed_temperature_reached; - double m_lifted; - - // BBS - double m_to_lift; - LiftType m_to_lift_type; - Vec3d m_pos = Vec3d::Zero(); - //BBS: this flag is used to indicate whether the m_pos is real. - //A example that of the first move, the m_pos is zero, but the real position of extruder doesn't - //Pos must be clear after the first xyz travel move - bool m_is_current_pos_clear = false; - //BBS: x, y offset for gcode generated - double m_x_offset{ 0 }; - double m_y_offset{ 0 }; // Orca: slicing resolution in mm double m_resolution = 0.01; - + std::string m_gcode_label_objects_start; std::string m_gcode_label_objects_end; //SoftFever bool m_is_bbl_printers = false; - // Belt printer state - double m_belt_angle_rad = 0.; - int m_remap_x = 0; - int m_remap_y = 1; - int m_remap_z = 2; - Vec3d m_build_vol_max = Vec3d::Zero(); - BeltBackTransform m_belt_back_transform; - bool m_origin_snap[3] = {false, false, false}; - double m_origin_offset[3] = {0., 0., 0.}; // target coord for bbox min - double m_origin_bbox_min[3] = {0., 0., 0.}; // computed bbox min in machine space - double m_current_speed; - bool m_is_first_layer = true; - enum class Acceleration { Travel, Print }; - std::string _travel_to_z(double z, const std::string &comment); std::string _spiral_travel_to_z(double z, const Vec2d &ij_offset, const std::string &comment); std::string _retract(double length, double restart_extra, const std::string &comment); std::string set_acceleration_internal(Acceleration type, unsigned int acceleration); diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 0b7190e8d1..1f787a70cf 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -12,6 +12,7 @@ #include "Thread.hpp" #include "Time.hpp" #include "GCode.hpp" +#include "BeltGCode.hpp" #include "GCode/WipeTower.hpp" #include "GCode/WipeTower2.hpp" #include "Utils.hpp" @@ -2551,12 +2552,17 @@ std::string Print::export_gcode(const std::string& path_template, GCodeProcessor this->set_status(80, message); // The following line may die for multiple reasons. - GCode gcode; + // Factory: use BeltGCode for belt printers, plain GCode otherwise. + std::unique_ptr gcode; + if (m_config.belt_printer.value) + gcode = std::make_unique(); + else + gcode = std::make_unique(); //BBS: compute plate offset for gcode-generator const Vec3d origin = this->get_plate_origin(); - gcode.set_gcode_offset(origin(0), origin(1)); - gcode.do_export(this, path.c_str(), result, thumbnail_cb); - gcode.export_layer_filaments(result); + gcode->set_gcode_offset(origin(0), origin(1)); + gcode->do_export(this, path.c_str(), result, thumbnail_cb); + gcode->export_layer_filaments(result); //BBS result->conflict_result = m_conflict_result; return path.c_str(); diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index c13a1407c0..9a5d000e1c 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -17,6 +17,7 @@ #include "GCode/ThumbnailData.hpp" #include "GCode/GCodeProcessor.hpp" #include "MultiMaterialSegmentation.hpp" +#include "BeltTransform.hpp" #include "libslic3r.h" #include @@ -190,30 +191,7 @@ class ConstSupportLayerPtrsAdaptor : public ConstVectorOfPtrsAdaptor double { - int axis = r % 3; - if (r < 3) return v[axis]; - return -v[axis]; - }; - Vec3d mn = bb.min.cast(), mx = bb.max.cast(); - BoundingBoxf3 rbb; - for (int i = 0; i < 8; ++i) { - Vec3d c((i & 1) ? mx.x() : mn.x(), - (i & 2) ? mx.y() : mn.y(), - (i & 4) ? mx.z() : mn.z()); - Vec3d rc(remap_coord(pre_rx, c), remap_coord(pre_ry, c), remap_coord(pre_rz, c)); - if (i == 0) rbb = BoundingBoxf3(rc, rc); - else rbb.merge(rc); - } - return rbb; + return BeltTransformPipeline::remap_bbox(model_object, config); } // Single instance of a PrintObject. diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index ba2ed408ce..be65c21c59 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1,5 +1,6 @@ #include "Exception.hpp" #include "Print.hpp" +#include "BeltTransform.hpp" #include "BoundingBox.hpp" #include "ClipperUtils.hpp" #include "ElephantFootCompensation.hpp" @@ -3398,76 +3399,22 @@ void PrintObject::update_slicing_parameters() // Orca: updated function call for XYZ shrinkage compensation if (!m_slicing_params.valid) { coordf_t object_height = this->model_object()->max_z(); - // Belt floor parameters for support clipping (populated below if belt Z-shear is active). - double belt_floor_shear_factor_out = 0.0; - int belt_floor_from_axis_out = 1; - double belt_floor_z_shift_out = 0.0; - // Belt shear/scale/pre-remap may change the effective Z height. + BeltTransformPipeline::BeltFloorParams belt_floor; const auto &pcfg = this->print()->config(); if (pcfg.belt_printer.value) { - BoundingBoxf3 bb = belt_remapped_bbox(*this->model_object(), pcfg); - bool has_preslice_remap = (int(pcfg.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || - int(pcfg.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || - int(pcfg.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ)); - if (has_preslice_remap) + BoundingBoxf3 bb = BeltTransformPipeline::remap_bbox(*this->model_object(), pcfg); + if (BeltTransformPipeline::has_preslice_remap(pcfg)) object_height = bb.size().z(); - - bool has_z_shear = pcfg.belt_shear_z.value != BeltShearMode::None; - bool has_z_scale = pcfg.belt_scale_z.value != BeltScaleMode::None; - if (has_z_shear || has_z_scale) { - auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { - double angle_rad = Geometry::deg2rad(angle_deg); - double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad); - switch (mode) { - case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; - case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; - case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; - case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; - default: return 0.; - } - }; - auto compute_scale_factor = [](BeltScaleMode mode, double angle_deg) -> double { - if (mode == BeltScaleMode::None) return 1.; - double angle_rad = Geometry::deg2rad(angle_deg); - double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad); - switch (mode) { - case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; - case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; - case BeltScaleMode::Sin: return sin_a; - case BeltScaleMode::Cos: return cos_a; - default: return 1.; - } - }; - double shear_factor = has_z_shear ? compute_shear_factor(pcfg.belt_shear_z.value, pcfg.belt_shear_z_angle.value) : 0.; - double scale_z = compute_scale_factor(pcfg.belt_scale_z.value, pcfg.belt_scale_z_angle.value); - if (has_z_shear && std::abs(shear_factor) > EPSILON) { - int from = int(pcfg.belt_shear_z_from.value); - double min_rz = std::numeric_limits::max(); - double max_rz = std::numeric_limits::lowest(); - for (double vz : {bb.min.z(), bb.max.z()}) - for (double vs : {bb.min(from), bb.max(from)}) { - double new_z = scale_z * (vz + shear_factor * vs); - min_rz = std::min(min_rz, new_z); - max_rz = std::max(max_rz, new_z); - } - object_height = max_rz - min_rz; - belt_floor_shear_factor_out = shear_factor; - belt_floor_from_axis_out = from; - // Belt contact surface starts at bb.min.z() pre-shear; add the - // slicing Z-shift that keeps the mesh above Z=0. - // Exact value is patched after slice_volumes() in posSlice. - belt_floor_z_shift_out = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.); - } else { - object_height *= scale_z; - } - } + auto hr = BeltTransformPipeline::compute_belt_height_and_floor(pcfg, bb, object_height); + object_height = hr.object_height; + belt_floor = hr.floor_params; } m_slicing_params = SlicingParameters::create_from_config(pcfg, m_config, object_height, this->object_extruders(), this->print()->shrinkage_compensation()); // Populate belt floor parameters into slicing params for support clipping. - m_slicing_params.belt_floor_shear_factor = belt_floor_shear_factor_out; - m_slicing_params.belt_floor_from_axis = belt_floor_from_axis_out; - m_slicing_params.belt_floor_z_shift = belt_floor_z_shift_out; + m_slicing_params.belt_floor_shear_factor = belt_floor.shear_factor; + m_slicing_params.belt_floor_from_axis = belt_floor.from_axis; + m_slicing_params.belt_floor_z_shift = belt_floor.z_shift; } } @@ -3505,75 +3452,23 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full sort_remove_duplicates(object_extruders); //FIXME add painting extruders - // Belt floor parameters for support clipping (populated below if belt Z-shear is active). - double belt_floor_shear_factor_out = 0.0; - int belt_floor_from_axis_out = 1; - double belt_floor_z_shift_out = 0.0; - + BeltTransformPipeline::BeltFloorParams belt_floor; if (object_max_z <= 0.f) { BoundingBoxf3 bb = model_object.raw_bounding_box(); object_max_z = (float)bb.size().z(); - // Belt pre-remap/shear/scale may change the effective Z height. if (print_config.belt_printer.value) { - bb = belt_remapped_bbox(model_object, print_config); - bool has_preslice_remap = (int(print_config.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || - int(print_config.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || - int(print_config.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ)); - if (has_preslice_remap) + bb = BeltTransformPipeline::remap_bbox(model_object, print_config); + if (BeltTransformPipeline::has_preslice_remap(print_config)) object_max_z = (float)bb.size().z(); - - bool has_z_shear = print_config.belt_shear_z.value != BeltShearMode::None; - bool has_z_scale = print_config.belt_scale_z.value != BeltScaleMode::None; - if (has_z_shear || has_z_scale) { - auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { - double angle_rad = Geometry::deg2rad(angle_deg); - double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad); - switch (mode) { - case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; - case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; - case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; - case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; - default: return 0.; - } - }; - auto compute_scale_factor = [](BeltScaleMode mode, double angle_deg) -> double { - if (mode == BeltScaleMode::None) return 1.; - double angle_rad = Geometry::deg2rad(angle_deg); - double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad); - switch (mode) { - case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; - case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; - case BeltScaleMode::Sin: return sin_a; - case BeltScaleMode::Cos: return cos_a; - default: return 1.; - } - }; - double shear_factor = has_z_shear ? compute_shear_factor(print_config.belt_shear_z.value, print_config.belt_shear_z_angle.value) : 0.; - double scale_z = compute_scale_factor(print_config.belt_scale_z.value, print_config.belt_scale_z_angle.value); - if (has_z_shear && std::abs(shear_factor) > EPSILON) { - int from = int(print_config.belt_shear_z_from.value); - double min_rz = std::numeric_limits::max(); - double max_rz = std::numeric_limits::lowest(); - for (double vz : {bb.min.z(), bb.max.z()}) - for (double vs : {bb.min(from), bb.max(from)}) { - double new_z = scale_z * (vz + shear_factor * vs); - min_rz = std::min(min_rz, new_z); - max_rz = std::max(max_rz, new_z); - } - object_max_z = (float)(max_rz - min_rz); - belt_floor_shear_factor_out = shear_factor; - belt_floor_from_axis_out = from; - belt_floor_z_shift_out = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.); - } else { - object_max_z *= (float)scale_z; - } - } + auto hr = BeltTransformPipeline::compute_belt_height_and_floor(print_config, bb, object_max_z); + object_max_z = (float)hr.object_height; + belt_floor = hr.floor_params; } } SlicingParameters params = SlicingParameters::create_from_config(print_config, object_config, object_max_z, object_extruders, object_shrinkage_compensation); - params.belt_floor_shear_factor = belt_floor_shear_factor_out; - params.belt_floor_from_axis = belt_floor_from_axis_out; - params.belt_floor_z_shift = belt_floor_z_shift_out; + params.belt_floor_shear_factor = belt_floor.shear_factor; + params.belt_floor_from_axis = belt_floor.from_axis; + params.belt_floor_z_shift = belt_floor.z_shift; return params; } diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 4c767ea0ed..3067772f09 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -9,6 +9,8 @@ #include "Layer.hpp" #include "MultiMaterialSegmentation.hpp" #include "Print.hpp" +#include "BeltTransform.hpp" +#include "BeltSliceStrategy.hpp" #include "Geometry.hpp" //BBS #include "ShortestPath.hpp" @@ -142,147 +144,11 @@ static std::vector slice_volumes_inner( params_base.closing_radius = print_object_config.slice_closing_radius.value; params_base.extra_offset = 0; params_base.trafo = object_trafo; - if (print_config.belt_printer.value) { - // --- Pre-slice axis remap --- - // Permutes/negates model axes before slicing so the slicer's coordinate - // system matches the physical bed orientation (e.g. XZ bed instead of XY). - int pre_rx = int(print_config.belt_preslice_remap_x.value); - int pre_ry = int(print_config.belt_preslice_remap_y.value); - int pre_rz = int(print_config.belt_preslice_remap_z.value); - - bool has_preslice_remap = (pre_rx != int(BeltRemapAxis::PosX) || - pre_ry != int(BeltRemapAxis::PosY) || - pre_rz != int(BeltRemapAxis::PosZ)); - - if (has_preslice_remap) { - // Build volume extents for Rev mode. - BoundingBoxf bbox_bed(print_config.printable_area.values); - Vec3d vol_max(bbox_bed.max.x(), bbox_bed.max.y(), - print_config.printable_height.value); - - // Each remap value selects a source axis and sign. - // The column vector tells the matrix which input axis feeds this output. - auto remap_column = [](int r) -> Vec3d { - int axis = r % 3; - Vec3d col = Vec3d::Zero(); - if (r < 3) col[axis] = 1.0; // +axis - else if (r < 6) col[axis] = -1.0; // -axis - else col[axis] = -1.0; // Rev: max - pos = -(pos - max) - return col; - }; - - Matrix3d remap_lin; - remap_lin.col(0) = remap_column(pre_rx); - remap_lin.col(1) = remap_column(pre_ry); - remap_lin.col(2) = remap_column(pre_rz); - - // Translation for Rev modes: output = max[src] - input[src]. - Vec3d remap_trans = Vec3d::Zero(); - auto add_rev_offset = [&](int r, int out_axis) { - if (r >= 6) { - int src_axis = r % 3; - remap_trans[out_axis] = vol_max[src_axis]; - } - }; - add_rev_offset(pre_rx, 0); - add_rev_offset(pre_ry, 1); - add_rev_offset(pre_rz, 2); - - Transform3d pre_remap = Transform3d::Identity(); - pre_remap.linear() = remap_lin; - pre_remap.translation() = remap_trans; - - params_base.trafo = pre_remap * params_base.trafo; - } - - // Build per-axis shear matrix from 3 independent axis configs. - auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { - double angle_rad = Geometry::deg2rad(angle_deg); - double sin_a = std::sin(angle_rad); - double cos_a = std::cos(angle_rad); - switch (mode) { - case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; - case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; - case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; - case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; - default: return 0.; - } - }; - - struct AxisShear { BeltShearMode mode; double angle; int from; }; - AxisShear axes[3] = { - { print_config.belt_shear_x.value, print_config.belt_shear_x_angle.value, int(print_config.belt_shear_x_from.value) }, - { print_config.belt_shear_y.value, print_config.belt_shear_y_angle.value, int(print_config.belt_shear_y_from.value) }, - { print_config.belt_shear_z.value, print_config.belt_shear_z_angle.value, int(print_config.belt_shear_z_from.value) }, - }; - - Transform3d belt_shear = Transform3d::Identity(); - bool has_shear = false; - for (int row = 0; row < 3; ++row) { - if (axes[row].mode != BeltShearMode::None) { - double factor = compute_shear_factor(axes[row].mode, axes[row].angle); - if (std::abs(factor) > EPSILON) { - belt_shear.matrix()(row, axes[row].from) += factor; - has_shear = true; - } - } - } - - // Build per-axis scale matrix. - auto compute_scale_factor = [](BeltScaleMode mode, double angle_deg) -> double { - if (mode == BeltScaleMode::None) return 1.; - double angle_rad = Geometry::deg2rad(angle_deg); - double sin_a = std::sin(angle_rad); - double cos_a = std::cos(angle_rad); - switch (mode) { - case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; - case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; - case BeltScaleMode::Sin: return sin_a; - case BeltScaleMode::Cos: return cos_a; - default: return 1.; - } - }; - - Transform3d belt_scale = Transform3d::Identity(); - bool has_scale = false; - double sx = compute_scale_factor(print_config.belt_scale_x.value, print_config.belt_scale_x_angle.value); - double sy = compute_scale_factor(print_config.belt_scale_y.value, print_config.belt_scale_y_angle.value); - double sz = compute_scale_factor(print_config.belt_scale_z.value, print_config.belt_scale_z_angle.value); - if (std::abs(sx - 1.) > EPSILON || std::abs(sy - 1.) > EPSILON || std::abs(sz - 1.) > EPSILON) { - belt_scale.matrix()(0, 0) = sx; - belt_scale.matrix()(1, 1) = sy; - belt_scale.matrix()(2, 2) = sz; - has_scale = true; - } - - // Apply: scale * shear * trafo (shear first, then scale). - if (has_shear || has_scale) - params_base.trafo = belt_scale * belt_shear * params_base.trafo; - - // After pre-remap/shear/scale, the mesh may clip through the build - // plate (Z < 0). Detect this and shift the mesh up along slicer Z. - if (has_preslice_remap || has_shear || has_scale) { - Transform3d combined = params_base.trafo; - double min_z = std::numeric_limits::max(); - for (const ModelVolume *mv : model_volumes) { - if (!mv->is_model_part()) continue; - for (const stl_vertex &v : mv->mesh().its.vertices) { - Vec3d pt = combined * v.cast(); - min_z = std::min(min_z, pt.z()); - } - } - double belt_z_shift_val = (min_z < 0. && min_z != std::numeric_limits::max()) ? -min_z : 0.; - BOOST_LOG_TRIVIAL(warning) << "Belt Z-shift: min_z=" << min_z - << " z_shift=" << belt_z_shift_val - << " trafo_z=" << object_trafo.matrix()(2, 3); - if (belt_z_shift_val > 0.) { - Transform3d z_shift = Transform3d::Identity(); - z_shift.matrix()(2, 3) = belt_z_shift_val; - params_base.trafo = z_shift * params_base.trafo; - } - if (out_belt_min_z) - *out_belt_min_z = (min_z != std::numeric_limits::max()) ? min_z : 0.; - } + { + // Belt printer: apply pre-slice transforms (remap, shear, scale, z-shift) via strategy. + auto belt_strategy = BeltSliceStrategy::create(print_config); + if (belt_strategy) + belt_strategy->apply_to_trafo(params_base.trafo, model_volumes, out_belt_min_z); } //BBS: 0.0025mm is safe enough to simplify the data to speed slicing up for high-resolution model. //Also has on influence on arc fitting which has default resolution 0.0125mm. @@ -968,11 +834,8 @@ void PrintObject::slice() // Without pre-remap, the belt surface IS at Z=0 and bb.min.z() is // already folded into m_belt_min_z, so use 0. const auto &pcfg = this->print()->config(); - bool has_preslice_remap = (int(pcfg.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || - int(pcfg.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || - int(pcfg.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ)); - double belt_surface_z = has_preslice_remap - ? belt_remapped_bbox(*this->model_object(), pcfg).min.z() : 0.; + double belt_surface_z = BeltTransformPipeline::has_preslice_remap(pcfg) + ? BeltTransformPipeline::remap_bbox(*this->model_object(), pcfg).min.z() : 0.; m_slicing_params.belt_floor_z_shift = belt_surface_z + z_shift_val; } @@ -1025,18 +888,6 @@ void PrintObject::slice() << " belt_shear_z_global=" << pcfg.belt_shear_z_global.value << " object=" << this->model_object()->name; if (pcfg.belt_printer.value) { - auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { - double angle_rad = Geometry::deg2rad(angle_deg); - double sin_a = std::sin(angle_rad); - double cos_a = std::cos(angle_rad); - switch (mode) { - case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; - case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; - case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; - case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; - default: return 0.; - } - }; Point inst_shift = this->instances().empty() ? Point(0, 0) : this->instances().front().shift - this->center_offset(); @@ -1058,7 +909,7 @@ void PrintObject::slice() // PrintObjects so the lowest-positioned object stays at Z=0. const auto &za = gaxes[2]; // Z row if (za.global && za.mode != BeltShearMode::None && za.from < 2) { - double factor = compute_shear_factor(za.mode, za.angle); + double factor = BeltTransformPipeline::compute_shear_factor(za.mode, za.angle); // The global Z offset accounts for the instance's position- // dependent shear contribution. m_belt_min_z is the minimum Z // of the mesh after pre_remap + shear + trafo_centered, which @@ -1066,12 +917,8 @@ void PrintObject::slice() // Subtract the belt surface's centered Z position so we get // only the shear-induced contribution (same correction as the // belt_floor_z_shift fix). - // Same pre-remap guard as belt_floor_z_shift above. - bool has_preslice_remap2 = (int(pcfg.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || - int(pcfg.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || - int(pcfg.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ)); - double belt_surface_z = has_preslice_remap2 - ? belt_remapped_bbox(*this->model_object(), this->print()->config()).min.z() : 0.; + double belt_surface_z = BeltTransformPipeline::has_preslice_remap(pcfg) + ? BeltTransformPipeline::remap_bbox(*this->model_object(), pcfg).min.z() : 0.; double shear_min_z = m_belt_min_z - belt_surface_z; Point phys = inst_shift; // already has center_offset subtracted double center_on_axis = (za.from == 0) ? unscale(phys.x()) : unscale(phys.y()); diff --git a/src/libslic3r/Support/BeltFloorContext.cpp b/src/libslic3r/Support/BeltFloorContext.cpp new file mode 100644 index 0000000000..d2ffcfd397 --- /dev/null +++ b/src/libslic3r/Support/BeltFloorContext.cpp @@ -0,0 +1,124 @@ +#include "BeltFloorContext.hpp" + +#include +#include + +namespace Slic3r { + +bool BeltFloorContext::init(const SlicingParameters &sp, const PrintConfig &pcfg) +{ + m_active = false; + m_shear_factor = sp.belt_floor_shear_factor; + m_from_axis = sp.belt_floor_from_axis; + m_z_shift = sp.belt_floor_z_shift; + m_floor_offset = pcfg.belt_support_floor_offset.value; + + if (std::abs(m_shear_factor) < EPSILON) + return false; + + m_active = true; + return true; +} + +bool BeltFloorContext::init_local(const SlicingParameters &sp, const PrintConfig &pcfg, + double global_z_offset) +{ + if (!init(sp, pcfg)) + return false; + // Local Z: subtract the global Z offset so polygon computation + // works in the object's local coordinate space. + m_z_shift -= global_z_offset; + return true; +} + +Polygons BeltFloorContext::surface_polygon(coordf_t print_z) const +{ + return half_plane(print_z, /*belt_surface=*/true); +} + +Polygons BeltFloorContext::valid_region_polygon(coordf_t print_z) const +{ + return half_plane(print_z, /*belt_surface=*/false); +} + +double BeltFloorContext::floor_print_z(const Point &pos_slicing) const +{ + if (!m_active) + return -std::numeric_limits::infinity(); + double pos = unscale(m_from_axis == 0 ? pos_slicing.x() : pos_slicing.y()); + return m_shear_factor * pos + m_floor_offset + m_z_shift; +} + +std::vector BeltFloorContext::compute_per_layer_floors( + size_t num_layers, + const std::function &layer_print_z) const +{ + std::vector result(num_layers); + if (!m_active) + return result; + for (size_t i = 0; i < num_layers; ++i) + result[i] = surface_polygon(layer_print_z(i)); + return result; +} + +Polygons BeltFloorContext::half_plane(coordf_t print_z, bool belt_surface) const +{ + if (!m_active) + return {}; + + const double cutoff = (print_z - m_z_shift - m_floor_offset) / m_shear_factor; + const coord_t cutoff_scaled = scale_(cutoff); + const coord_t large_bound = scale_(1e3); + + // The belt surface is on one side of the cutoff line; the valid region + // is on the other side. Which side depends on shear_factor sign. + // + // belt_surface=true → the belt side (where support should NOT exist) + // belt_surface=false → the valid side (where support IS allowed) + // + // For shear_factor > 0: belt surface is from_axis >= cutoff + // For shear_factor < 0: belt surface is from_axis <= cutoff + bool high_side = (m_shear_factor > 0) == belt_surface; + + Polygon poly; + if (m_from_axis == 0) { + if (high_side) { + // X >= cutoff + poly.points = { + Point(cutoff_scaled, -large_bound), + Point(large_bound, -large_bound), + Point(large_bound, large_bound), + Point(cutoff_scaled, large_bound) + }; + } else { + // X < cutoff + poly.points = { + Point(-large_bound, -large_bound), + Point(cutoff_scaled, -large_bound), + Point(cutoff_scaled, large_bound), + Point(-large_bound, large_bound) + }; + } + } else { + if (high_side) { + // Y >= cutoff + poly.points = { + Point(-large_bound, cutoff_scaled), + Point( large_bound, cutoff_scaled), + Point( large_bound, large_bound), + Point(-large_bound, large_bound) + }; + } else { + // Y < cutoff + poly.points = { + Point(-large_bound, -large_bound), + Point( large_bound, -large_bound), + Point( large_bound, cutoff_scaled), + Point(-large_bound, cutoff_scaled) + }; + } + } + return { poly }; +} + +} // namespace Slic3r diff --git a/src/libslic3r/Support/BeltFloorContext.hpp b/src/libslic3r/Support/BeltFloorContext.hpp new file mode 100644 index 0000000000..7b8b704ef6 --- /dev/null +++ b/src/libslic3r/Support/BeltFloorContext.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include "../libslic3r.h" +#include "../Point.hpp" +#include "../Polygon.hpp" +#include "../Slicing.hpp" +#include "../PrintConfig.hpp" + +namespace Slic3r { + +class PrintObject; + +// Belt floor context: encapsulates the parameters and polygon computation +// for belt printer floor clipping in support generation. +// +// All belt floor code across SupportMaterial, TreeSupport, TreeSupport3D, +// and TreeModelVolumes uses the same 4 parameters and the same 4-case +// polygon construction. This class consolidates that logic. +// +// Construct once per PrintObject, then call surface_polygon() or +// valid_region_polygon() per layer with the layer's print_z. +class BeltFloorContext +{ +public: + BeltFloorContext() = default; + + // Initialize from slicing parameters and print config. + // Uses global Z coordinates (for SupportMaterial, non-organic TreeSupport). + bool init(const SlicingParameters &sp, const PrintConfig &pcfg); + + // Initialize with a Z offset subtracted from z_shift. + // Uses local Z coordinates (for TreeSupport3D, TreeModelVolumes organic pipeline). + bool init_local(const SlicingParameters &sp, const PrintConfig &pcfg, + double global_z_offset); + + bool is_active() const { return m_active; } + + // Compute the belt-side half-plane polygon at a given print_z. + // This is the region where the belt surface exists. + Polygons surface_polygon(coordf_t print_z) const; + + // Compute the valid-region half-plane polygon at a given print_z. + // This is the complement: the region where support is allowed. + Polygons valid_region_polygon(coordf_t print_z) const; + + // Compute the belt floor Z position at a given XY position (in slicing coords). + // Returns -infinity if not active. + double floor_print_z(const Point &pos_slicing) const; + + // Pre-compute belt floor polygons for a range of layers. + // layer_print_z(i) returns the print_z for layer index i. + std::vector compute_per_layer_floors( + size_t num_layers, + const std::function &layer_print_z) const; + + // Accessors + double shear_factor() const { return m_shear_factor; } + int from_axis() const { return m_from_axis; } + double z_shift() const { return m_z_shift; } + double floor_offset() const { return m_floor_offset; } + +private: + bool m_active = false; + double m_shear_factor = 0.0; + int m_from_axis = 1; // 0=X, 1=Y + double m_z_shift = 0.0; + double m_floor_offset = 0.0; + + // Internal: compute the raw half-plane polygon. + // If belt_surface=true, returns the belt side; otherwise the valid (complement) side. + Polygons half_plane(coordf_t print_z, bool belt_surface) const; +}; + +} // namespace Slic3r diff --git a/src/libslic3r/Support/SupportMaterial.cpp b/src/libslic3r/Support/SupportMaterial.cpp index d70153a7dd..c1f732bc6d 100644 --- a/src/libslic3r/Support/SupportMaterial.cpp +++ b/src/libslic3r/Support/SupportMaterial.cpp @@ -5,6 +5,7 @@ #include "Print.hpp" #include "SupportMaterial.hpp" #include "SupportCommon.hpp" +#include "BeltFloorContext.hpp" #include "Geometry.hpp" #include "Point.hpp" #include "MutablePolygon.hpp" @@ -625,64 +626,10 @@ Polygons belt_floor_surface_polygon( const PrintObject &object, coordf_t print_z) { - const double shear_factor = slicing_params.belt_floor_shear_factor; - if (std::abs(shear_factor) < EPSILON) + BeltFloorContext ctx; + if (!ctx.init(slicing_params, print_config)) return {}; - - const int from_axis = slicing_params.belt_floor_from_axis; // 0=X, 1=Y - const double floor_offset = print_config.belt_support_floor_offset.value; - - // Belt floor line in slicing coordinates: Z = sf * Y + z_shift. - // z_shift accounts for the upward shift applied when post-shear geometry - // extends below the bed (overhangs). Solving for Y: - // cutoff = (print_z - z_shift - floor_offset) / sf - const double z_shift = slicing_params.belt_floor_z_shift; - const double cutoff = (print_z - z_shift - floor_offset) / shear_factor; - const coord_t cutoff_scaled = scale_(cutoff); - const coord_t large_bound = scale_(1e4); - - // Build the belt-side half-plane (inverted from the valid region). - // If shear_factor > 0: valid region is from_axis < cutoff, so belt surface is from_axis >= cutoff. - // If shear_factor < 0: valid region is from_axis > cutoff, so belt surface is from_axis <= cutoff. - Polygon belt_poly; - if (from_axis == 0) { - if (shear_factor > 0) { - // Belt surface: X >= cutoff - belt_poly.points = { - Point(cutoff_scaled, -large_bound), - Point(large_bound, -large_bound), - Point(large_bound, large_bound), - Point(cutoff_scaled, large_bound) - }; - } else { - // Belt surface: X <= cutoff - belt_poly.points = { - Point(-large_bound, -large_bound), - Point(cutoff_scaled, -large_bound), - Point(cutoff_scaled, large_bound), - Point(-large_bound, large_bound) - }; - } - } else { - if (shear_factor > 0) { - // Belt surface: Y >= cutoff - belt_poly.points = { - Point(-large_bound, cutoff_scaled), - Point( large_bound, cutoff_scaled), - Point( large_bound, large_bound), - Point(-large_bound, large_bound) - }; - } else { - // Belt surface: Y <= cutoff - belt_poly.points = { - Point(-large_bound, -large_bound), - Point( large_bound, -large_bound), - Point( large_bound, cutoff_scaled), - Point(-large_bound, cutoff_scaled) - }; - } - } - return { belt_poly }; + return ctx.surface_polygon(print_z); } // Belt printer: compute the valid-region half-plane polygon at a given print_z. @@ -694,58 +641,10 @@ static Polygons belt_floor_valid_region_polygon( const PrintObject &object, coordf_t print_z) { - const double shear_factor = slicing_params.belt_floor_shear_factor; - if (std::abs(shear_factor) < EPSILON) + BeltFloorContext ctx; + if (!ctx.init(slicing_params, print_config)) return {}; - - const int from_axis = slicing_params.belt_floor_from_axis; - const double floor_offset = print_config.belt_support_floor_offset.value; - - const double z_shift = slicing_params.belt_floor_z_shift; - const double cutoff = (print_z - z_shift - floor_offset) / shear_factor; - const coord_t cutoff_scaled = scale_(cutoff); - const coord_t large_bound = scale_(1e4); - - // Valid region: the complement of the belt surface polygon. - Polygon valid_poly; - if (from_axis == 0) { - if (shear_factor > 0) { - // Valid: X < cutoff - valid_poly.points = { - Point(-large_bound, -large_bound), - Point(cutoff_scaled, -large_bound), - Point(cutoff_scaled, large_bound), - Point(-large_bound, large_bound) - }; - } else { - // Valid: X > cutoff - valid_poly.points = { - Point(cutoff_scaled, -large_bound), - Point(large_bound, -large_bound), - Point(large_bound, large_bound), - Point(cutoff_scaled, large_bound) - }; - } - } else { - if (shear_factor > 0) { - // Valid: Y < cutoff - valid_poly.points = { - Point(-large_bound, -large_bound), - Point( large_bound, -large_bound), - Point( large_bound, cutoff_scaled), - Point(-large_bound, cutoff_scaled) - }; - } else { - // Valid: Y > cutoff - valid_poly.points = { - Point(-large_bound, cutoff_scaled), - Point( large_bound, cutoff_scaled), - Point( large_bound, large_bound), - Point(-large_bound, large_bound) - }; - } - } - return { valid_poly }; + return ctx.valid_region_polygon(print_z); } // Collect outer contours of all slices of this layer. @@ -3373,22 +3272,18 @@ static void trim_support_layers_by_belt_floor( const PrintObject &object, SupportGeneratorLayersPtr &support_layers) { - if (std::abs(slicing_params.belt_floor_shear_factor) < EPSILON) + BeltFloorContext ctx; + if (!ctx.init(slicing_params, print_config)) return; if (print_config.belt_support_floor_mode.value != BeltSupportFloorMode::GeneratorOnly) return; tbb::parallel_for(tbb::blocked_range(0, support_layers.size()), [&](const tbb::blocked_range &range) { - for (size_t i = range.begin(); i < range.end(); ++ i) { - SupportGeneratorLayer *layer = support_layers[i]; - if (layer->polygons.empty()) - continue; - Polygons belt_surface = belt_floor_surface_polygon( - slicing_params, print_config, object, layer->print_z); - if (! belt_surface.empty()) - layer->polygons = diff(layer->polygons, belt_surface); - } + for (size_t i = range.begin(); i < range.end(); ++i) + if (support_layers[i]) + support_layers[i]->polygons = diff(support_layers[i]->polygons, + ctx.surface_polygon(support_layers[i]->print_z)); }); } diff --git a/src/libslic3r/Support/TreeModelVolumes.cpp b/src/libslic3r/Support/TreeModelVolumes.cpp index ec38ffed73..ebc3d2c958 100644 --- a/src/libslic3r/Support/TreeModelVolumes.cpp +++ b/src/libslic3r/Support/TreeModelVolumes.cpp @@ -8,6 +8,7 @@ #include "TreeModelVolumes.hpp" #include "TreeSupportCommon.hpp" +#include "BeltFloorContext.hpp" #include "../BuildVolume.hpp" #include "../ClipperUtils.hpp" @@ -101,13 +102,11 @@ TreeModelVolumes::TreeModelVolumes( { const auto &sp = print_object.slicing_parameters(); const auto &pcfg = print_object.print()->config(); - const double sf = sp.belt_floor_shear_factor; - if (std::abs(sf) > EPSILON + BeltFloorContext ctx; + ctx.init_local(sp, pcfg, print_object.belt_global_z_offset()); + if (ctx.is_active() && std::abs(print_object.belt_global_z_offset()) > EPSILON && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { - const int from_axis = sp.belt_floor_from_axis; - const double floor_off = pcfg.belt_support_floor_offset.value; - const double z_shift = sp.belt_floor_z_shift - print_object.belt_global_z_offset(); size_t num_layers_needed = print_object.layer_count(); // Ensure m_anti_overhang is large enough. if (m_anti_overhang.size() < num_layers_needed) @@ -115,22 +114,7 @@ TreeModelVolumes::TreeModelVolumes( for (size_t layer_idx = 0; layer_idx < num_layers_needed; ++layer_idx) { double print_z = print_object.get_layer(layer_idx)->print_z - print_object.belt_global_z_offset(); - double cutoff = (print_z - z_shift - floor_off) / sf; - coord_t cutoff_sc = scale_(cutoff); - coord_t big = scale_(1e4); - Polygon belt_poly; - if (from_axis == 0) { - if (sf > 0) - belt_poly.points = {{cutoff_sc,-big},{big,-big},{big,big},{cutoff_sc,big}}; - else - belt_poly.points = {{-big,-big},{cutoff_sc,-big},{cutoff_sc,big},{-big,big}}; - } else { - if (sf > 0) - belt_poly.points = {{-big,cutoff_sc},{big,cutoff_sc},{big,big},{-big,big}}; - else - belt_poly.points = {{-big,-big},{big,-big},{big,cutoff_sc},{-big,cutoff_sc}}; - } - append(m_anti_overhang[layer_idx], Polygons{belt_poly}); + append(m_anti_overhang[layer_idx], ctx.surface_polygon(print_z)); } } } @@ -180,40 +164,20 @@ TreeModelVolumes::TreeModelVolumes( // Branches grow toward the belt and their slices are clipped at the belt // surface in organic_draw_branches(). The organic pipeline works in LOCAL // Z (no global_z_offset), so use local z_shift and local print_z. - const auto &slicing_params = print_object.slicing_parameters(); - const auto &pcfg = print_object.print()->config(); - const double sf = slicing_params.belt_floor_shear_factor; - if (std::abs(sf) > EPSILON - && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { - const int from_axis = slicing_params.belt_floor_from_axis; - const double floor_off = pcfg.belt_support_floor_offset.value; - // Subtract global_z_offset to get the LOCAL z_shift — the organic - // pipeline's Z coordinates don't include the global offset. - const double z_shift = slicing_params.belt_floor_z_shift - - print_object.belt_global_z_offset(); - m_belt_floor.assign(num_layers, Polygons{}); - for (size_t layer_idx = 0; layer_idx < num_layers; ++layer_idx) { - // Use local print_z (subtract global offset from object layer). - double print_z = (layer_idx >= num_raft_layers) - ? print_object.get_layer(layer_idx - num_raft_layers)->print_z - - print_object.belt_global_z_offset() - : 0.; - double cutoff = (print_z - z_shift - floor_off) / sf; - coord_t cutoff_sc = scale_(cutoff); - coord_t big = scale_(1e4); - Polygon belt_poly; - if (from_axis == 0) { - if (sf > 0) - belt_poly.points = {{cutoff_sc,-big},{big,-big},{big,big},{cutoff_sc,big}}; - else - belt_poly.points = {{-big,-big},{cutoff_sc,-big},{cutoff_sc,big},{-big,big}}; - } else { - if (sf > 0) - belt_poly.points = {{-big,cutoff_sc},{big,cutoff_sc},{big,big},{-big,big}}; - else - belt_poly.points = {{-big,-big},{big,-big},{big,cutoff_sc},{-big,cutoff_sc}}; - } - m_belt_floor[layer_idx] = { belt_poly }; + { + const auto &slicing_params = print_object.slicing_parameters(); + const auto &pcfg2 = print_object.print()->config(); + BeltFloorContext ctx; + ctx.init_local(slicing_params, pcfg2, print_object.belt_global_z_offset()); + if (ctx.is_active() + && pcfg2.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + m_belt_floor = ctx.compute_per_layer_floors(num_layers, [&](size_t layer_idx) -> double { + // Use local print_z (subtract global offset from object layer). + return (layer_idx >= num_raft_layers) + ? print_object.get_layer(layer_idx - num_raft_layers)->print_z + - print_object.belt_global_z_offset() + : 0.; + }); } } } diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 60e681add9..6c359115da 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -14,6 +14,7 @@ #include "TreeSupportCommon.hpp" #include "TreeSupport.hpp" #include "TreeSupport3D.hpp" +#include "BeltFloorContext.hpp" #include #include @@ -639,16 +640,10 @@ TreeSupport::TreeSupport(PrintObject& object, const SlicingParameters &slicing_p double TreeSupport::belt_floor_print_z(const Point &pos_slicing) const { - double sf = m_slicing_params.belt_floor_shear_factor; - if (std::abs(sf) < EPSILON) - return -std::numeric_limits::max(); // no belt floor - int from = m_slicing_params.belt_floor_from_axis; - // Belt floor in slicing coords: Z = sf * Y + z_shift + floor_offset. - // Inverse of cutoff = (Z - z_shift - floor_offset) / sf. - double pos = unscale(from == 0 ? pos_slicing.x() : pos_slicing.y()); - double floor_offset = m_print_config->belt_support_floor_offset.value; - double z_shift = m_slicing_params.belt_floor_z_shift; - return sf * pos + floor_offset + z_shift; + BeltFloorContext ctx; + if (!ctx.init(m_slicing_params, *m_print_config)) + return -std::numeric_limits::infinity(); + return ctx.floor_print_z(pos_slicing); } #define SUPPORT_SURFACES_OFFSET_PARAMETERS ClipperLib::jtSquare, 0. @@ -1738,18 +1733,11 @@ void TreeSupport::generate() // layer and clipped at the belt surface. These layers bypass the tree // algorithm entirely — they're pure geometry added after draw_circles(). { - const auto &sp = m_slicing_params; - const auto &pcfg = *m_print_config; - const double sf = sp.belt_floor_shear_factor; - if (std::abs(sf) > EPSILON - && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly + BeltFloorContext ctx; + if (ctx.init(m_slicing_params, *m_print_config) + && m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly && m_object->support_layer_count() > 0) { - const int from_axis = sp.belt_floor_from_axis; - const double floor_off = pcfg.belt_support_floor_offset.value; - // Support layer print_z values are in GLOBAL Z (non-organic inherits - // from object layers which include global_z_offset). Use the GLOBAL - // belt_floor_z_shift to match. - const double z_shift = sp.belt_floor_z_shift; + const auto &sp = m_slicing_params; // Find the lowest non-empty, non-brim support layer. ExPolygons source_areas; double source_z = 0; @@ -1778,7 +1766,7 @@ void TreeSupport::generate() } if (!source_areas.empty()) { BoundingBoxf3 bb = belt_remapped_bbox(*m_object->model_object(), m_object->print()->config()); - double from_extent = std::abs(bb.min(from_axis)); + double from_extent = std::abs(bb.min(ctx.from_axis())); double bb_min_z = std::abs(bb.min.z()); double first_z = m_object->get_support_layer(0)->print_z; // Depth = from-axis extent + pre-shear bbox Z offset (ensure_on_bed @@ -1792,18 +1780,8 @@ void TreeSupport::generate() for (int i = num_extra; i >= 1 && !prev_areas.empty(); --i) { double print_z = first_z - i * sp.layer_height; if (print_z < -sp.layer_height) continue; - double cutoff = (print_z - z_shift - floor_off) / sf; - coord_t cutoff_sc = scale_(cutoff); - coord_t big = scale_(1e3); - Polygon belt_poly; - if (from_axis == 0) { - if (sf > 0) belt_poly.points = {{cutoff_sc,-big},{big,-big},{big,big},{cutoff_sc,big}}; - else belt_poly.points = {{-big,-big},{cutoff_sc,-big},{cutoff_sc,big},{-big,big}}; - } else { - if (sf > 0) belt_poly.points = {{-big,cutoff_sc},{big,cutoff_sc},{big,big},{-big,big}}; - else belt_poly.points = {{-big,-big},{big,-big},{big,cutoff_sc},{-big,cutoff_sc}}; - } - ExPolygons clipped = diff_ex(source_areas, Polygons{belt_poly}); + Polygons belt_surface = ctx.surface_polygon(print_z); + ExPolygons clipped = diff_ex(source_areas, belt_surface); if (clipped.empty()) continue; SupportLayer *sl = new SupportLayer(0, 0, m_object, sp.layer_height, print_z, -1); sl->base_areas = clipped; @@ -2250,37 +2228,19 @@ void TreeSupport::draw_circles() // Belt floor: clip tree support polygons by the belt surface plane. // ts_layer->print_z is at LOCAL Z (global offset applied later in - // _generate_support_material), but belt_floor_z_shift includes - // global_z_offset — subtract it to get the cutoff in local coords. - if (std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON - && m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { - const double sf = m_slicing_params.belt_floor_shear_factor; - const int from_axis = m_slicing_params.belt_floor_from_axis; - const double floor_off = m_print_config->belt_support_floor_offset.value; - const double z_shift_local = m_slicing_params.belt_floor_z_shift - - m_object->belt_global_z_offset(); - const double cutoff = (ts_layer->print_z - z_shift_local - floor_off) / sf; - const coord_t cutoff_sc = scale_(cutoff); - const coord_t big = scale_(1e4); - - Polygon belt_poly; - if (from_axis == 0) { - if (sf > 0) - belt_poly.points = { {cutoff_sc,-big}, {big,-big}, {big,big}, {cutoff_sc,big} }; - else - belt_poly.points = { {-big,-big}, {cutoff_sc,-big}, {cutoff_sc,big}, {-big,big} }; - } else { - if (sf > 0) - belt_poly.points = { {-big,cutoff_sc}, {big,cutoff_sc}, {big,big}, {-big,big} }; - else - belt_poly.points = { {-big,-big}, {big,-big}, {big,cutoff_sc}, {-big,cutoff_sc} }; + // _generate_support_material), so use init_local to subtract + // belt_global_z_offset from z_shift. + { + BeltFloorContext ctx; + if (ctx.init_local(m_slicing_params, *m_print_config, m_object->belt_global_z_offset()) + && m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + Polygons belt_surface = ctx.surface_polygon(ts_layer->print_z); + base_areas = diff_ex(base_areas, belt_surface); + roof_areas = diff_ex(roof_areas, belt_surface); + roof_1st_layer = diff_ex(roof_1st_layer, belt_surface); + floor_areas = diff_ex(floor_areas, belt_surface); + roof_gap_areas = diff_ex(roof_gap_areas, belt_surface); } - Polygons belt_surface = { belt_poly }; - base_areas = diff_ex(base_areas, belt_surface); - roof_areas = diff_ex(roof_areas, belt_surface); - roof_1st_layer = diff_ex(roof_1st_layer, belt_surface); - floor_areas = diff_ex(floor_areas, belt_surface); - roof_gap_areas = diff_ex(roof_gap_areas, belt_surface); } if (SQUARE_SUPPORT) { @@ -3593,6 +3553,20 @@ TreeSupportData::TreeSupportData(const PrintObject &object, coordf_t xy_distance poly.simplify(scale_(m_radius_sample_resolution), &outline); } + // Belt floor: add belt surface polygon to layer outlines so the + // collision system treats the belt as a physical surface. + { + BeltFloorContext ctx; + double local_print_z = layer->print_z - object.belt_global_z_offset(); + if (ctx.init_local(object.slicing_parameters(), object.print()->config(), + object.belt_global_z_offset()) + && object.print()->config().belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + Polygons belt_surface = ctx.surface_polygon(local_print_z); + for (auto &p : belt_surface) + outline.emplace_back(ExPolygon(p)); + } + } + if (layer_nr == 0) m_layer_outlines_below.push_back(outline); else diff --git a/src/libslic3r/Support/TreeSupport3D.cpp b/src/libslic3r/Support/TreeSupport3D.cpp index 9d35cb2dd8..8a07609930 100644 --- a/src/libslic3r/Support/TreeSupport3D.cpp +++ b/src/libslic3r/Support/TreeSupport3D.cpp @@ -19,6 +19,7 @@ #include "Polygon.hpp" #include "Polyline.hpp" #include "MutablePolygon.hpp" +#include "BeltFloorContext.hpp" #include "SupportCommon.hpp" #include "TriangleMeshSlicer.hpp" #include "TreeSupport.hpp" @@ -3373,8 +3374,9 @@ static void generate_support_areas(Print &print, TreeSupport* tree_support, cons PrintObject &po = *print.get_object(processing.second.front()); const auto &sp = po.slicing_parameters(); const auto &pcfg = po.print()->config(); - const double sf = sp.belt_floor_shear_factor; - if (std::abs(sf) > EPSILON && std::abs(po.belt_global_z_offset()) > EPSILON + BeltFloorContext ctx; + ctx.init_local(sp, pcfg, po.belt_global_z_offset()); + if (ctx.is_active() && std::abs(po.belt_global_z_offset()) > EPSILON && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { // z_shift_local is the belt surface height at Y=0 in local coords. // Extend below the belt so the base expansion and build-plate @@ -3598,32 +3600,16 @@ static void generate_support_areas(Print &print, TreeSupport* tree_support, cons // Compute the belt floor polygon directly from each layer's print_z // rather than mapping to a layer index (avoids index mismatch issues). { - const auto &sp = print_object.slicing_parameters(); - const double sf = sp.belt_floor_shear_factor; - const double z_shift = sp.belt_floor_z_shift - print_object.belt_global_z_offset(); - const double floor_off = print_object.print()->config().belt_support_floor_offset.value; - const int from_axis = sp.belt_floor_from_axis; - if (std::abs(sf) > EPSILON - && print_object.print()->config().belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + const auto &sp = print_object.slicing_parameters(); + const auto &pcfg = print_object.print()->config(); + BeltFloorContext ctx; + ctx.init_local(sp, pcfg, print_object.belt_global_z_offset()); + if (ctx.is_active() + && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { tbb::parallel_for_each(layers_sorted.begin(), layers_sorted.end(), [&](SupportGeneratorLayer *layer) { if (!layer || layer->polygons.empty()) return; - double cutoff = (layer->print_z - z_shift - floor_off) / sf; - coord_t cutoff_sc = scale_(cutoff); - coord_t big = scale_(1e4); - Polygon belt_poly; - if (from_axis == 0) { - if (sf > 0) - belt_poly.points = {{cutoff_sc,-big},{big,-big},{big,big},{cutoff_sc,big}}; - else - belt_poly.points = {{-big,-big},{cutoff_sc,-big},{cutoff_sc,big},{-big,big}}; - } else { - if (sf > 0) - belt_poly.points = {{-big,cutoff_sc},{big,cutoff_sc},{big,big},{-big,big}}; - else - belt_poly.points = {{-big,-big},{big,-big},{big,cutoff_sc},{-big,cutoff_sc}}; - } - layer->polygons = diff(layer->polygons, Polygons{belt_poly}); + layer->polygons = diff(layer->polygons, ctx.surface_polygon(layer->print_z)); }); } } From 44eebdb8ad0d0869c1aa4a58b5a27425ae46d81f Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 28 Mar 2026 02:13:15 -0500 Subject: [PATCH 008/434] Part 3.2: decouple axis remapping, enable viewing settings in Developer mode or when Belt mode is active --- src/libslic3r/BeltGCode.cpp | 14 ++---- src/libslic3r/BeltGCodeWriter.cpp | 25 ++-------- src/libslic3r/BeltGCodeWriter.hpp | 8 +--- src/libslic3r/BeltSliceStrategy.cpp | 15 ++---- src/libslic3r/BeltSliceStrategy.hpp | 10 ++-- src/libslic3r/BeltTransform.cpp | 18 ++++---- src/libslic3r/BeltTransform.hpp | 14 +++--- src/libslic3r/GCode.cpp | 13 ++++++ src/libslic3r/GCode/BeltBackTransform.cpp | 2 +- src/libslic3r/GCode/BeltBackTransform.hpp | 2 +- src/libslic3r/GCode/GCodeProcessor.cpp | 34 +++++++------- src/libslic3r/GCode/GCodeProcessor.hpp | 12 ++--- src/libslic3r/GCodeWriter.cpp | 56 +++++++++++++++++++++-- src/libslic3r/GCodeWriter.hpp | 15 ++++++ src/libslic3r/Preset.cpp | 4 +- src/libslic3r/Print.cpp | 12 ++--- src/libslic3r/PrintConfig.cpp | 52 ++++++++++----------- src/libslic3r/PrintConfig.hpp | 18 ++++---- src/libslic3r/PrintObjectSlice.cpp | 24 +++++++++- src/slic3r/GUI/Tab.cpp | 21 +++++---- 20 files changed, 215 insertions(+), 154 deletions(-) diff --git a/src/libslic3r/BeltGCode.cpp b/src/libslic3r/BeltGCode.cpp index dc1b6ca439..40dabf98dc 100644 --- a/src/libslic3r/BeltGCode.cpp +++ b/src/libslic3r/BeltGCode.cpp @@ -15,13 +15,7 @@ void BeltGCode::init_belt_writer(Print &print, bool is_bbl_printers) auto belt_writer = std::make_unique(); belt_writer->set_is_bbl_machine(is_bbl_printers); belt_writer->set_belt_angle(print.config().belt_printer_angle.value); - belt_writer->set_axis_remap( - int(print.config().belt_gcode_remap_x.value), - int(print.config().belt_gcode_remap_y.value), - int(print.config().belt_gcode_remap_z.value)); - BoundingBoxf bbox_bed(print.config().printable_area.values); - belt_writer->set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(), - print.config().printable_height.value)); + // Axis remap and build volume max are set by base GCode after init_belt_writer returns. belt_writer->set_belt_back_transform(print.config()); m_writer = std::move(belt_writer); @@ -59,9 +53,9 @@ void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print) file.write_format("; belt_scale_z = %s\n", full_cfg.opt_serialize("belt_scale_z").c_str()); file.write_format("; belt_scale_z_angle = %.1f\n", print.config().belt_scale_z_angle.value); // Pre-slice remap configs - file.write_format("; belt_preslice_remap_x = %s\n", full_cfg.opt_serialize("belt_preslice_remap_x").c_str()); - file.write_format("; belt_preslice_remap_y = %s\n", full_cfg.opt_serialize("belt_preslice_remap_y").c_str()); - file.write_format("; belt_preslice_remap_z = %s\n", full_cfg.opt_serialize("belt_preslice_remap_z").c_str()); + file.write_format("; preslice_remap_x = %s\n", full_cfg.opt_serialize("preslice_remap_x").c_str()); + file.write_format("; preslice_remap_y = %s\n", full_cfg.opt_serialize("preslice_remap_y").c_str()); + file.write_format("; preslice_remap_z = %s\n", full_cfg.opt_serialize("preslice_remap_z").c_str()); } void BeltGCode::on_set_origin(const PrintObject *obj, const Point &inst_shift) diff --git a/src/libslic3r/BeltGCodeWriter.cpp b/src/libslic3r/BeltGCodeWriter.cpp index 645bf85a5b..db2bc46fe1 100644 --- a/src/libslic3r/BeltGCodeWriter.cpp +++ b/src/libslic3r/BeltGCodeWriter.cpp @@ -10,18 +10,6 @@ void BeltGCodeWriter::set_belt_angle(double angle_deg) m_belt_angle_rad = Geometry::deg2rad(angle_deg); } -void BeltGCodeWriter::set_axis_remap(int rx, int ry, int rz) -{ - m_remap_x = rx; - m_remap_y = ry; - m_remap_z = rz; -} - -void BeltGCodeWriter::set_build_volume_max(const Vec3d &max) -{ - m_build_vol_max = max; -} - void BeltGCodeWriter::set_belt_back_transform(const PrintConfig &config) { m_belt_back_transform.init_from_config(config); @@ -40,16 +28,9 @@ Vec3d BeltGCodeWriter::to_machine_coords(const Vec3d &pos) const { // Step 1: Undo the shear/scale applied during slicing. Vec3d p = m_belt_back_transform.apply(pos); - // Step 2: Apply axis remapping for the machine's coordinate convention. - // BeltRemapAxis: 0-2 = +X/+Y/+Z, 3-5 = -X/-Y/-Z, 6-8 = Rev X/Y/Z - auto remap = [this, &p](int r) -> double { - int axis = r % 3; - if (r < 3) return p[axis]; - if (r < 6) return -p[axis]; - return m_build_vol_max[axis] - p[axis]; - }; - Vec3d result = { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) }; - // Per-axis origin snap: shift so bbox min on each enabled axis = offset. + // Step 2: Apply axis remap (uses inherited base class method). + Vec3d result = apply_axis_remap(p); + // Step 3: Per-axis origin snap. for (int i = 0; i < 3; ++i) if (m_origin_snap[i]) result[i] -= (m_origin_bbox_min[i] - m_origin_offset[i]); diff --git a/src/libslic3r/BeltGCodeWriter.hpp b/src/libslic3r/BeltGCodeWriter.hpp index 727135b578..a41f890503 100644 --- a/src/libslic3r/BeltGCodeWriter.hpp +++ b/src/libslic3r/BeltGCodeWriter.hpp @@ -15,11 +15,9 @@ class BeltGCodeWriter : public GCodeWriter public: BeltGCodeWriter() : GCodeWriter() {} - // Belt configuration + // Belt configuration (axis remap is inherited from GCodeWriter) void set_belt_angle(double angle_deg); bool is_belt_printer() const { return m_belt_angle_rad != 0.; } - void set_axis_remap(int rx, int ry, int rz); - void set_build_volume_max(const Vec3d &max); void set_belt_back_transform(const PrintConfig &config); void set_origin_snap(int axis, bool enable, double offset, double bbox_min); Vec3d to_machine_coords(const Vec3d &pos) const; @@ -37,10 +35,6 @@ protected: private: double m_belt_angle_rad = 0.; - int m_remap_x = 0; - int m_remap_y = 1; - int m_remap_z = 2; - Vec3d m_build_vol_max = Vec3d::Zero(); BeltBackTransform m_belt_back_transform; bool m_origin_snap[3] = {false, false, false}; double m_origin_offset[3] = {0., 0., 0.}; diff --git a/src/libslic3r/BeltSliceStrategy.cpp b/src/libslic3r/BeltSliceStrategy.cpp index b3adeec64a..c1a300869e 100644 --- a/src/libslic3r/BeltSliceStrategy.cpp +++ b/src/libslic3r/BeltSliceStrategy.cpp @@ -13,31 +13,24 @@ std::unique_ptr BeltSliceStrategy::create(const PrintConfig & BeltSliceStrategy::BeltSliceStrategy(const PrintConfig &config) { - m_has_remap = BeltTransformPipeline::has_preslice_remap(config); - if (m_has_remap) - m_pre_remap = BeltTransformPipeline::build_preslice_remap(config); - m_shear = BeltTransformPipeline::build_shear_matrix(config, &m_has_shear); m_scale = BeltTransformPipeline::build_scale_matrix(config, &m_has_scale); } void BeltSliceStrategy::apply_to_trafo(Transform3d &trafo, const ModelVolumePtrs &model_volumes, + bool has_remap, double *out_belt_min_z) const { - // Step 1: Pre-slice axis remap. - if (m_has_remap) - trafo = m_pre_remap * trafo; - - // Step 2: Shear + scale. + // Shear + scale (belt-only; pre-slice remap is handled separately). if (m_has_shear || m_has_scale) { Transform3d belt_xform = Transform3d::Identity(); belt_xform.linear() = m_scale * m_shear; trafo = belt_xform * trafo; } - // Step 3: Z-shift — detect if mesh clips below build plate after transforms. - if (m_has_remap || m_has_shear || m_has_scale) { + // Z-shift — detect if mesh clips below build plate after transforms. + if (has_remap || m_has_shear || m_has_scale) { double min_z = std::numeric_limits::max(); for (const ModelVolume *mv : model_volumes) { if (!mv->is_model_part()) continue; diff --git a/src/libslic3r/BeltSliceStrategy.hpp b/src/libslic3r/BeltSliceStrategy.hpp index bf19243fd2..ef6e038ee7 100644 --- a/src/libslic3r/BeltSliceStrategy.hpp +++ b/src/libslic3r/BeltSliceStrategy.hpp @@ -23,21 +23,19 @@ public: // Create a strategy if belt_printer is enabled; returns nullptr otherwise. static std::unique_ptr create(const PrintConfig &config); - // Apply belt transforms to the slicing trafo. - // Modifies trafo in-place: trafo = z_shift * scale * shear * pre_remap * trafo - // Scans all model_part volumes to detect minimum Z and add z-shift if needed. - // Sets *out_belt_min_z to the minimum Z of the mesh after transforms. + // Apply belt-specific transforms (shear + scale + z-shift) to the slicing trafo. + // Pre-slice remap is handled separately (standalone feature). + // has_remap: whether pre-slice remap was already applied (affects z-shift detection). void apply_to_trafo(Transform3d &trafo, const ModelVolumePtrs &model_volumes, + bool has_remap, double *out_belt_min_z) const; private: explicit BeltSliceStrategy(const PrintConfig &config); - bool m_has_remap = false; bool m_has_shear = false; bool m_has_scale = false; - Transform3d m_pre_remap = Transform3d::Identity(); Matrix3d m_shear = Matrix3d::Identity(); Matrix3d m_scale = Matrix3d::Identity(); }; diff --git a/src/libslic3r/BeltTransform.cpp b/src/libslic3r/BeltTransform.cpp index db7eea7d80..9bb66fcfb6 100644 --- a/src/libslic3r/BeltTransform.cpp +++ b/src/libslic3r/BeltTransform.cpp @@ -13,9 +13,9 @@ Transform3d BeltTransformPipeline::build_preslice_remap(const PrintConfig &confi if (!has_preslice_remap(config)) return pre_remap; - int pre_rx = int(config.belt_preslice_remap_x.value); - int pre_ry = int(config.belt_preslice_remap_y.value); - int pre_rz = int(config.belt_preslice_remap_z.value); + int pre_rx = int(config.preslice_remap_x.value); + int pre_ry = int(config.preslice_remap_y.value); + int pre_rz = int(config.preslice_remap_z.value); // Each remap value selects a source axis and sign. auto remap_column = [](int r) -> Vec3d { @@ -114,13 +114,13 @@ Transform3d BeltTransformPipeline::build_forward_transform(const PrintConfig &co BoundingBoxf3 BeltTransformPipeline::remap_bbox(const BoundingBoxf3 &bb, const PrintConfig &config) { - int pre_rx = int(config.belt_preslice_remap_x.value); - int pre_ry = int(config.belt_preslice_remap_y.value); - int pre_rz = int(config.belt_preslice_remap_z.value); + int pre_rx = int(config.preslice_remap_x.value); + int pre_ry = int(config.preslice_remap_y.value); + int pre_rz = int(config.preslice_remap_z.value); - if (pre_rx == int(BeltRemapAxis::PosX) && - pre_ry == int(BeltRemapAxis::PosY) && - pre_rz == int(BeltRemapAxis::PosZ)) + if (pre_rx == int(RemapAxis::PosX) && + pre_ry == int(RemapAxis::PosY) && + pre_rz == int(RemapAxis::PosZ)) return bb; // Identity remap. auto remap_coord = [](int r, const Vec3d &v) -> double { diff --git a/src/libslic3r/BeltTransform.hpp b/src/libslic3r/BeltTransform.hpp index b9a60473e5..3c0665c252 100644 --- a/src/libslic3r/BeltTransform.hpp +++ b/src/libslic3r/BeltTransform.hpp @@ -58,21 +58,21 @@ public: static bool has_preslice_remap(const PrintConfig &config) { - return int(config.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || - int(config.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || - int(config.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ); + return int(config.preslice_remap_x.value) != int(RemapAxis::PosX) || + int(config.preslice_remap_y.value) != int(RemapAxis::PosY) || + int(config.preslice_remap_z.value) != int(RemapAxis::PosZ); } // Overload accepting DynamicPrintConfig (used in static slicing_parameters). static bool has_preslice_remap(const DynamicPrintConfig &config) { auto get_int = [&](const char *key) -> int { - auto *opt = config.option>(key); + auto *opt = config.option>(key); return opt ? int(opt->value) : 0; }; - return get_int("belt_preslice_remap_x") != int(BeltRemapAxis::PosX) || - get_int("belt_preslice_remap_y") != int(BeltRemapAxis::PosY) || - get_int("belt_preslice_remap_z") != int(BeltRemapAxis::PosZ); + return get_int("preslice_remap_x") != int(RemapAxis::PosX) || + get_int("preslice_remap_y") != int(RemapAxis::PosY) || + get_int("preslice_remap_z") != int(RemapAxis::PosZ); } static bool has_shear(const PrintConfig &config) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index c8b6e540cf..514efc4b52 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2425,6 +2425,19 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Belt printer: initialize belt-specific writer via virtual hook. this->init_belt_writer(print, is_bbl_printers); + // Standalone axis remap (works with or without belt mode). + { + int rx = int(print.config().gcode_remap_x.value); + int ry = int(print.config().gcode_remap_y.value); + int rz = int(print.config().gcode_remap_z.value); + if (rx != 0 || ry != 1 || rz != 2) { + m_writer->set_axis_remap(rx, ry, rz); + BoundingBoxf bbox_bed(print.config().printable_area.values); + m_writer->set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(), + print.config().printable_height.value)); + } + } + // How many times will be change_layer() called? // change_layer() in turn increments the progress bar status. m_layer_count = 0; diff --git a/src/libslic3r/GCode/BeltBackTransform.cpp b/src/libslic3r/GCode/BeltBackTransform.cpp index 2e2cd26f75..b9cdac779f 100644 --- a/src/libslic3r/GCode/BeltBackTransform.cpp +++ b/src/libslic3r/GCode/BeltBackTransform.cpp @@ -8,7 +8,7 @@ bool BeltBackTransform::init_from_config(const PrintConfig &config) m_active = false; m_inverse = Transform3d::Identity(); - if (!config.belt_printer.value || !config.belt_gcode_back_transform.value) + if (!config.belt_printer.value || !config.gcode_back_transform.value) return false; // Require at least one active transform to proceed. diff --git a/src/libslic3r/GCode/BeltBackTransform.hpp b/src/libslic3r/GCode/BeltBackTransform.hpp index 5a75d94dc8..b98770c688 100644 --- a/src/libslic3r/GCode/BeltBackTransform.hpp +++ b/src/libslic3r/GCode/BeltBackTransform.hpp @@ -15,7 +15,7 @@ namespace Slic3r { // Initialized once from PrintConfig, then applied per-point in // GCodeWriter::to_machine_coords() before axis remapping. // -// Active when belt_gcode_back_transform is true AND at least one of: +// Active when gcode_back_transform is true AND at least one of: // - a shear axis has global mode enabled, or // - a pre-slice axis remap is non-identity. class BeltBackTransform diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 4c630bb6de..5e1bad0b69 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -3129,26 +3129,26 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers try { m_result.belt_scale_z_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; } // Pre-slice axis remap - auto parse_remap_axis = [](const std::string &s) -> BeltRemapAxis { - if (s == "pos_x") return BeltRemapAxis::PosX; - if (s == "pos_y") return BeltRemapAxis::PosY; - if (s == "pos_z") return BeltRemapAxis::PosZ; - if (s == "neg_x") return BeltRemapAxis::NegX; - if (s == "neg_y") return BeltRemapAxis::NegY; - if (s == "neg_z") return BeltRemapAxis::NegZ; - if (s == "rev_x") return BeltRemapAxis::RevX; - if (s == "rev_y") return BeltRemapAxis::RevY; - if (s == "rev_z") return BeltRemapAxis::RevZ; - return BeltRemapAxis::PosX; + auto parse_remap_axis = [](const std::string &s) -> RemapAxis { + if (s == "pos_x") return RemapAxis::PosX; + if (s == "pos_y") return RemapAxis::PosY; + if (s == "pos_z") return RemapAxis::PosZ; + if (s == "neg_x") return RemapAxis::NegX; + if (s == "neg_y") return RemapAxis::NegY; + if (s == "neg_z") return RemapAxis::NegZ; + if (s == "rev_x") return RemapAxis::RevX; + if (s == "rev_y") return RemapAxis::RevY; + if (s == "rev_z") return RemapAxis::RevZ; + return RemapAxis::PosX; }; - if (boost::starts_with(comment, " belt_preslice_remap_x = ")) { - m_result.belt_preslice_remap_x = parse_remap_axis(trim(std::string(comment.substr(25)))); return; + if (boost::starts_with(comment, " preslice_remap_x = ")) { + m_result.preslice_remap_x = parse_remap_axis(trim(std::string(comment.substr(25)))); return; } - if (boost::starts_with(comment, " belt_preslice_remap_y = ")) { - m_result.belt_preslice_remap_y = parse_remap_axis(trim(std::string(comment.substr(25)))); return; + if (boost::starts_with(comment, " preslice_remap_y = ")) { + m_result.preslice_remap_y = parse_remap_axis(trim(std::string(comment.substr(25)))); return; } - if (boost::starts_with(comment, " belt_preslice_remap_z = ")) { - m_result.belt_preslice_remap_z = parse_remap_axis(trim(std::string(comment.substr(25)))); return; + if (boost::starts_with(comment, " preslice_remap_z = ")) { + m_result.preslice_remap_z = parse_remap_axis(trim(std::string(comment.substr(25)))); return; } } // wipe start tag diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index 7d08a9cdfd..4c6e4f6618 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -246,9 +246,9 @@ class Print; float belt_scale_y_angle{ 45.f }; BeltScaleMode belt_scale_z{ BeltScaleMode::None }; float belt_scale_z_angle{ 45.f }; - BeltRemapAxis belt_preslice_remap_x{ BeltRemapAxis::PosX }; - BeltRemapAxis belt_preslice_remap_y{ BeltRemapAxis::PosY }; - BeltRemapAxis belt_preslice_remap_z{ BeltRemapAxis::PosZ }; + RemapAxis preslice_remap_x{ RemapAxis::PosX }; + RemapAxis preslice_remap_y{ RemapAxis::PosY }; + RemapAxis preslice_remap_z{ RemapAxis::PosZ }; SettingsIds settings_ids; size_t filaments_count; bool backtrace_enabled; @@ -324,9 +324,9 @@ class Print; belt_scale_y_angle = other.belt_scale_y_angle; belt_scale_z = other.belt_scale_z; belt_scale_z_angle = other.belt_scale_z_angle; - belt_preslice_remap_x = other.belt_preslice_remap_x; - belt_preslice_remap_y = other.belt_preslice_remap_y; - belt_preslice_remap_z = other.belt_preslice_remap_z; + preslice_remap_x = other.preslice_remap_x; + preslice_remap_y = other.preslice_remap_y; + preslice_remap_z = other.preslice_remap_z; #if ENABLE_GCODE_VIEWER_STATISTICS time = other.time; #endif diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index fea5509bc3..9b78ef1ea5 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -20,6 +20,36 @@ namespace Slic3r { bool GCodeWriter::full_gcode_comment = true; +void GCodeWriter::set_axis_remap(int rx, int ry, int rz) +{ + m_remap_x = rx; + m_remap_y = ry; + m_remap_z = rz; +} + +void GCodeWriter::set_build_volume_max(const Vec3d &max) +{ + m_build_vol_max = max; +} + +bool GCodeWriter::has_axis_remap() const +{ + return m_remap_x != 0 || m_remap_y != 1 || m_remap_z != 2; +} + +Vec3d GCodeWriter::apply_axis_remap(const Vec3d &pos) const +{ + if (!has_axis_remap()) + return pos; + auto remap = [this, &pos](int r) -> double { + int axis = r % 3; + if (r < 3) return pos[axis]; + if (r < 6) return -pos[axis]; + return m_build_vol_max[axis] - pos[axis]; + }; + return { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) }; +} + bool GCodeWriter::supports_separate_travel_acceleration(GCodeFlavor flavor) { return (flavor == gcfRepetier || flavor == gcfMarlinFirmware || flavor == gcfRepRapFirmware); @@ -548,7 +578,13 @@ std::string GCodeWriter::travel_to_xy(const Vec2d &point, const std::string &com Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; GCodeG1Formatter w; - w.emit_xy(point_on_plate); + if (has_axis_remap()) { + // Axis remap may couple XY with Z; emit full XYZ in machine coordinates. + Vec3d machine = apply_axis_remap(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); + w.emit_xyz(machine); + } else { + w.emit_xy(point_on_plate); + } auto speed = m_is_first_layer ? this->config.get_abs_value("initial_layer_travel_speed") : this->config.travel_speed.value; w.emit_f(speed * 60.0); @@ -780,7 +816,13 @@ std::string GCodeWriter::_travel_to_z(double z, const std::string &comment) } GCodeG1Formatter w; - w.emit_z(z); + if (has_axis_remap()) { + // Remap may couple Z with other axes; emit full XYZ. + Vec3d machine = apply_axis_remap(Vec3d(m_pos.x() - m_x_offset, m_pos.y() - m_y_offset, z)); + w.emit_xyz(machine); + } else { + w.emit_z(z); + } w.emit_f(speed * 60.0); //BBS w.emit_comment(GCodeWriter::full_gcode_comment, comment); @@ -885,7 +927,12 @@ std::string GCodeWriter::extrude_to_xy(const Vec2d &point, double dE, const std: Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; GCodeG1Formatter w; - w.emit_xy(point_on_plate); + if (has_axis_remap()) { + Vec3d machine = apply_axis_remap(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); + w.emit_xyz(machine); + } else { + w.emit_xy(point_on_plate); + } if (!force_no_extrusion) w.emit_e(filament()->E()); //BBS @@ -925,6 +972,9 @@ std::string GCodeWriter::extrude_to_xyz(const Vec3d &point, double dE, const std //BBS: take plate offset into consider Vec3d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset, point(2) }; + if (has_axis_remap()) + point_on_plate = apply_axis_remap(point_on_plate); + GCodeG1Formatter w; w.emit_xyz(point_on_plate); if (!force_no_extrusion) diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index c96c3ca2c1..6aa2ee8ffd 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -125,6 +125,12 @@ public: void set_is_first_layer(bool bval) { m_is_first_layer = bval; } GCodeFlavor get_gcode_flavor() const { return config.gcode_flavor; } + // Axis remap: permute/negate/reverse axes in G-code output. + // Works standalone (without belt mode) for printers with non-standard axis conventions. + void set_axis_remap(int rx, int ry, int rz); + void set_build_volume_max(const Vec3d &max); + bool has_axis_remap() const; + // Returns whether this flavor supports separate print and travel acceleration. static bool supports_separate_travel_acceleration(GCodeFlavor flavor); protected: @@ -141,6 +147,15 @@ protected: virtual std::string _travel_to_z(double z, const std::string &comment); + // Axis remap state — accessible to subclasses. + int m_remap_x = 0; // RemapAxis: 0=+X, 1=+Y, 2=+Z, 3=-X, etc. + int m_remap_y = 1; + int m_remap_z = 2; + Vec3d m_build_vol_max = Vec3d::Zero(); + + // Apply axis remap to a point. Returns pos unchanged if remap is identity. + Vec3d apply_axis_remap(const Vec3d &pos) const; + private: // Extruders are sorted by their ID, so that binary search is possible. std::vector m_filament_extruders; diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 9cc5e03d40..cd3af59dae 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1014,8 +1014,8 @@ static std::vector s_Preset_printer_options { "belt_shear_y", "belt_shear_y_angle", "belt_shear_y_from", "belt_shear_y_global", "belt_shear_z", "belt_shear_z_angle", "belt_shear_z_from", "belt_shear_z_global", "belt_scale_x", "belt_scale_x_angle", "belt_scale_y", "belt_scale_y_angle", "belt_scale_z", "belt_scale_z_angle", - "belt_preslice_remap_x", "belt_preslice_remap_y", "belt_preslice_remap_z", - "belt_gcode_remap_x", "belt_gcode_remap_y", "belt_gcode_remap_z", "belt_gcode_back_transform", + "preslice_remap_x", "preslice_remap_y", "preslice_remap_z", + "gcode_remap_x", "gcode_remap_y", "gcode_remap_z", "gcode_back_transform", "belt_origin_snap_x", "belt_origin_offset_x", "belt_origin_snap_y", "belt_origin_offset_y", "belt_origin_snap_z", "belt_origin_offset_z", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 1f787a70cf..aca40e4952 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -101,9 +101,9 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n // or they are only notes not influencing the generated G-code. static std::unordered_set steps_gcode = { // Belt printer G-code axis remap (only affects G-code output, not slicing). - "belt_gcode_remap_x", - "belt_gcode_remap_y", - "belt_gcode_remap_z", + "gcode_remap_x", + "gcode_remap_y", + "gcode_remap_z", "belt_origin_snap_x", "belt_origin_offset_x", "belt_origin_snap_y", "belt_origin_offset_y", "belt_origin_snap_z", "belt_origin_offset_z", @@ -308,9 +308,9 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "belt_scale_y_angle" || opt_key == "belt_scale_z" || opt_key == "belt_scale_z_angle" - || opt_key == "belt_preslice_remap_x" - || opt_key == "belt_preslice_remap_y" - || opt_key == "belt_preslice_remap_z") { + || opt_key == "preslice_remap_x" + || opt_key == "preslice_remap_y" + || opt_key == "preslice_remap_z") { osteps.emplace_back(posSlice); } else if ( opt_key == "belt_support_floor_offset" diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 506bd17a29..a1dbcf8845 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -314,18 +314,18 @@ static t_config_enum_values s_keys_map_BeltScaleMode { }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltScaleMode) -static t_config_enum_values s_keys_map_BeltRemapAxis { - { "pos_x", int(BeltRemapAxis::PosX) }, - { "pos_y", int(BeltRemapAxis::PosY) }, - { "pos_z", int(BeltRemapAxis::PosZ) }, - { "neg_x", int(BeltRemapAxis::NegX) }, - { "neg_y", int(BeltRemapAxis::NegY) }, - { "neg_z", int(BeltRemapAxis::NegZ) }, - { "rev_x", int(BeltRemapAxis::RevX) }, - { "rev_y", int(BeltRemapAxis::RevY) }, - { "rev_z", int(BeltRemapAxis::RevZ) }, +static t_config_enum_values s_keys_map_RemapAxis { + { "pos_x", int(RemapAxis::PosX) }, + { "pos_y", int(RemapAxis::PosY) }, + { "pos_z", int(RemapAxis::PosZ) }, + { "neg_x", int(RemapAxis::NegX) }, + { "neg_y", int(RemapAxis::NegY) }, + { "neg_z", int(RemapAxis::NegZ) }, + { "rev_x", int(RemapAxis::RevX) }, + { "rev_y", int(RemapAxis::RevY) }, + { "rev_z", int(RemapAxis::RevZ) }, }; -CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltRemapAxis) +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(RemapAxis) static t_config_enum_values s_keys_map_BeltSupportFloorMode { { "none", int(BeltSupportFloorMode::None) }, @@ -6120,49 +6120,49 @@ void PrintConfigDef::init_fff_params() add_belt_scale_angle("belt_scale_z_angle", "Angle"); // G-code axis remap with sign - auto add_belt_remap = [this](const char *key, const char *label, const char *tooltip, BeltRemapAxis default_axis) { + auto add_belt_remap = [this](const char *key, const char *label, const char *tooltip, RemapAxis default_axis) { auto def = this->add(key, coEnum); def->label = L(label); def->category = L("Printable space"); def->tooltip = L(tooltip); - def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); def->enum_values = {"pos_x", "pos_y", "pos_z", "neg_x", "neg_y", "neg_z", "rev_x", "rev_y", "rev_z"}; def->enum_labels = {L("+X"), L("+Y"), L("+Z"), L("-X"), L("-Y"), L("-Z"), L("Rev X"), L("Rev Y"), L("Rev Z")}; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionEnum(default_axis)); + def->mode = comSimple; // Visibility controlled by toggle_line in Tab.cpp + def->set_default_value(new ConfigOptionEnum(default_axis)); }; - add_belt_remap("belt_preslice_remap_x", "X", + add_belt_remap("preslice_remap_x", "X", "Before slicing, which model-space axis becomes the slicer's X axis. " "Use this to re-orient the coordinate system so the slicer's XY plane matches " "your belt printer's physical bed plane. For a printer whose bed is in the XZ plane, " "set Y to +Z and Z to +Y (or -Y) to swap the vertical and belt-travel axes. " "Default +X: no change.", - BeltRemapAxis::PosX); - add_belt_remap("belt_preslice_remap_y", "Y", + RemapAxis::PosX); + add_belt_remap("preslice_remap_y", "Y", "Before slicing, which model-space axis becomes the slicer's Y axis. " "The slicer treats Y as one of the two horizontal bed axes. If your physical " "belt surface runs along the Z axis, map Y to +Z here so the slicer slices " "along the correct plane. Default +Y: no change.", - BeltRemapAxis::PosY); - add_belt_remap("belt_preslice_remap_z", "Z", + RemapAxis::PosY); + add_belt_remap("preslice_remap_z", "Z", "Before slicing, which model-space axis becomes the slicer's Z axis (layer stacking direction). " "The slicer builds layers upward along this axis. If your printer's layer-stacking " "direction is the physical Y axis, map Z to +Y (or -Y for inverted direction). " "Rev mode mirrors relative to the build volume maximum. Default +Z: no change.", - BeltRemapAxis::PosZ); + RemapAxis::PosZ); - add_belt_remap("belt_gcode_remap_x", "X", "Which slicing axis maps to machine X in G-code output. Applied AFTER slicing, during G-code generation.", BeltRemapAxis::PosX); - add_belt_remap("belt_gcode_remap_y", "Y", "Which slicing axis maps to machine Y in G-code output. Applied AFTER slicing, during G-code generation.", BeltRemapAxis::PosY); - add_belt_remap("belt_gcode_remap_z", "Z", "Which slicing axis maps to machine Z in G-code output. Applied AFTER slicing, during G-code generation.", BeltRemapAxis::PosZ); + add_belt_remap("gcode_remap_x", "X", "Which slicing axis maps to machine X in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosX); + add_belt_remap("gcode_remap_y", "Y", "Which slicing axis maps to machine Y in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosY); + add_belt_remap("gcode_remap_z", "Z", "Which slicing axis maps to machine Z in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosZ); - def = this->add("belt_gcode_back_transform", coBool); + def = this->add("gcode_back_transform", coBool); def->label = L("G-code back-transform"); def->category = L("Printable space"); def->tooltip = L("Reverse the shear/scale transform applied during slicing so G-code " "coordinates are in the machine's physical coordinate space. " "Requires at least one shear axis with global mode enabled."); - def->mode = comAdvanced; + def->mode = comSimple; // Visibility controlled by toggle_line in Tab.cpp def->set_default_value(new ConfigOptionBool(false)); auto add_belt_origin_snap = [this](const char *key_snap, const char *key_offset, diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 8b469c4dc6..459e9eb26f 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -182,7 +182,7 @@ enum class BeltAxis Z = 2, }; -enum class BeltRemapAxis +enum class RemapAxis { PosX = 0, PosY = 1, PosZ = 2, NegX = 3, NegY = 4, NegZ = 5, @@ -550,7 +550,7 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SlicingMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltShearMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltScaleMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltAxis) -CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltRemapAxis) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(RemapAxis) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltSupportFloorMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltSupportZOffsetMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialPattern) @@ -1488,13 +1488,13 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionFloat, belt_scale_y_angle)) ((ConfigOptionEnum, belt_scale_z)) ((ConfigOptionFloat, belt_scale_z_angle)) - ((ConfigOptionEnum, belt_preslice_remap_x)) - ((ConfigOptionEnum, belt_preslice_remap_y)) - ((ConfigOptionEnum, belt_preslice_remap_z)) - ((ConfigOptionEnum, belt_gcode_remap_x)) - ((ConfigOptionEnum, belt_gcode_remap_y)) - ((ConfigOptionEnum, belt_gcode_remap_z)) - ((ConfigOptionBool, belt_gcode_back_transform)) + ((ConfigOptionEnum, preslice_remap_x)) + ((ConfigOptionEnum, preslice_remap_y)) + ((ConfigOptionEnum, preslice_remap_z)) + ((ConfigOptionEnum, gcode_remap_x)) + ((ConfigOptionEnum, gcode_remap_y)) + ((ConfigOptionEnum, gcode_remap_z)) + ((ConfigOptionBool, gcode_back_transform)) ((ConfigOptionBool, belt_origin_snap_x)) ((ConfigOptionFloat, belt_origin_offset_x)) ((ConfigOptionBool, belt_origin_snap_y)) diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 3067772f09..7279b83d14 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -144,11 +144,31 @@ static std::vector slice_volumes_inner( params_base.closing_radius = print_object_config.slice_closing_radius.value; params_base.extra_offset = 0; params_base.trafo = object_trafo; + // Standalone pre-slice axis remap (works without belt mode). + bool has_remap = BeltTransformPipeline::has_preslice_remap(print_config); + if (has_remap) + params_base.trafo = BeltTransformPipeline::build_preslice_remap(print_config) * params_base.trafo; { - // Belt printer: apply pre-slice transforms (remap, shear, scale, z-shift) via strategy. + // Belt-specific transforms (shear, scale, z-shift) via strategy. auto belt_strategy = BeltSliceStrategy::create(print_config); if (belt_strategy) - belt_strategy->apply_to_trafo(params_base.trafo, model_volumes, out_belt_min_z); + belt_strategy->apply_to_trafo(params_base.trafo, model_volumes, has_remap, out_belt_min_z); + } + // If only remap (no belt), still need z-shift detection. + if (has_remap && !print_config.belt_printer.value) { + double min_z = std::numeric_limits::max(); + for (const ModelVolume *mv : model_volumes) { + if (!mv->is_model_part()) continue; + for (const stl_vertex &v : mv->mesh().its.vertices) { + Vec3d pt = params_base.trafo * v.cast(); + min_z = std::min(min_z, pt.z()); + } + } + if (min_z < 0. && min_z != std::numeric_limits::max()) { + Transform3d z_shift = Transform3d::Identity(); + z_shift.matrix()(2, 3) = -min_z; + params_base.trafo = z_shift * params_base.trafo; + } } //BBS: 0.0025mm is safe enough to simplify the data to speed slicing up for high-resolution model. //Also has on influence on arc fitting which has default resolution 0.0125mm. diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index d5a6b38ebc..6202185440 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4435,19 +4435,19 @@ void TabPrinter::build_fff() L("Remap model axes before slicing so the slicer's coordinate system matches " "the physical bed orientation. For belt printers whose bed is NOT in the XY plane, " "use this to swap axes so layers are stacked in the correct physical direction.") }; - line.append_option(optgroup->get_option("belt_preslice_remap_x")); - line.append_option(optgroup->get_option("belt_preslice_remap_y")); - line.append_option(optgroup->get_option("belt_preslice_remap_z")); + line.append_option(optgroup->get_option("preslice_remap_x")); + line.append_option(optgroup->get_option("preslice_remap_y")); + line.append_option(optgroup->get_option("preslice_remap_z")); optgroup->append_line(line); } { Line line = { L("G-code axis remap (post-slice)"), L("Remap slicing-frame axes to machine axes in G-code output. Applied AFTER slicing, during G-code generation.") }; - line.append_option(optgroup->get_option("belt_gcode_remap_x")); - line.append_option(optgroup->get_option("belt_gcode_remap_y")); - line.append_option(optgroup->get_option("belt_gcode_remap_z")); + line.append_option(optgroup->get_option("gcode_remap_x")); + line.append_option(optgroup->get_option("gcode_remap_y")); + line.append_option(optgroup->get_option("gcode_remap_z")); optgroup->append_line(line); } - optgroup->append_single_option_line("belt_gcode_back_transform"); + optgroup->append_single_option_line("gcode_back_transform"); { Line line = { L("Origin snap X"), L("Snap object bbox min X to offset in G-code output") }; line.append_option(optgroup->get_option("belt_origin_snap_x")); @@ -5320,11 +5320,14 @@ void TabPrinter::toggle_options() toggle_line("belt_printer_infinite_y", is_belt); for (auto el : {"belt_shear_x", "belt_shear_y", "belt_shear_z", "belt_scale_x", "belt_scale_y", "belt_scale_z", - "belt_preslice_remap_x", - "belt_gcode_remap_x", "belt_gcode_back_transform", "belt_origin_snap_x", "belt_origin_snap_y", "belt_origin_snap_z"}) toggle_line(el, is_belt); + // Remap options: visible when belt mode is on OR when UI is in developer mode. + bool show_remap = is_belt || (m_mode >= comDevelop); + for (auto el : {"preslice_remap_x", "gcode_remap_x", "gcode_back_transform"}) + toggle_line(el, show_remap); + // Gray out angle/from sub-options when their parent shear/scale mode is None. auto sx = m_config->option>("belt_shear_x")->value; toggle_option("belt_shear_x_angle", is_belt && sx != BeltShearMode::None); From 372139c770b9112d27c76c27e3253e29de2f5bb0 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 28 Mar 2026 04:03:39 -0500 Subject: [PATCH 009/434] revert CLAUDE.md --- CLAUDE.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9bd162c42c..25aa516a56 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,7 +23,27 @@ cmake --build build/arm64 --config RelWithDebInfo --target all -- ### Building on Linux **Always use this command to build the project when testing build issues on Linux.** ```bash -systemd-run --user --scope -p MemoryMax=48G cmake --build build --config RelWithDebInfo --target all -- -j18 -l 24 +cmake --build build/arm64 --config RelWithDebInfo --target all -- + +``` +### Build test: + +**Always use this command to build the project when testing build issues on Windows.** +```bash +cmake --build . --config %build_type% --target ALL_BUILD -- -m +``` + +### Building on macOS +**Always use this command to build the project when testing build issues on macOS.** +```bash +cmake --build build/arm64 --config RelWithDebInfo --target all -- +``` + +### Building on Linux + **Always use this command to build the project when testing build issues on Linux.** +```bash +cmake --build build --config RelWithDebInfo --target all -- + ``` From 4634a5dfd79e234439b244e43f528f3e33a1c9cb Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sun, 29 Mar 2026 13:14:25 -0500 Subject: [PATCH 010/434] 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 1e56b65146cbab9f1577240db9429e0e71ab0ec7 Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Thu, 2 Apr 2026 02:37:08 -0400 Subject: [PATCH 011/434] feat: Add iXex parallel printing support for IDEX and IQEX printers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces first-class parallel printing (copy/mirror modes) for printers with multiple independent X-axis carriages. Branded iXex (independent X extruder), targeting Klipper firmware with a firmware-agnostic design. - PrintConfig: new printer options declaring iXex capability and geometry (is_ixex, ixex_gantry_count, ixex_tools_per_gantry, carriage dims, tool layout, and per-mode name/role/gcode arrays) - Preset: iXex keys registered in printer and process preset option lists - Tab: IXexModesCtrl visual grid editor in Printer preset tab; mode dropdown in Process → Others tab; clear_pages() nulls iXex pointers to prevent dangling-pointer crash on preset save - PartPlate: 2D zone visualization (active/dimmed/dividers) and placement-violation detection (has_ixex_placement_violations) that blocks slicing when objects fall outside the primary zone - Plater: violation detection wired into update_background_process so the Slice button is disabled with an error notification on violation - GCode: mode-activation G-code injected before machine_start_gcode - GCodeViewer: multi-carriage toolhead markers in sequential preview, filament legend annotated with active carriage count and mode name --- src/libslic3r/GCode.cpp | 18 + src/libslic3r/Preset.cpp | 20 +- src/libslic3r/PrintConfig.cpp | 103 ++++++ src/libslic3r/PrintConfig.hpp | 16 + src/slic3r/GUI/GCodeViewer.cpp | 252 +++++++++++++ src/slic3r/GUI/GCodeViewer.hpp | 6 + src/slic3r/GUI/OG_CustomCtrl.cpp | 5 + src/slic3r/GUI/PartPlate.cpp | 613 ++++++++++++++++++++++++++++--- src/slic3r/GUI/PartPlate.hpp | 13 + src/slic3r/GUI/Plater.cpp | 9 + src/slic3r/GUI/Tab.cpp | 519 ++++++++++++++++++++++++++ src/slic3r/GUI/Tab.hpp | 8 + 12 files changed, 1536 insertions(+), 46 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index fe10cbcd41..e1d218553a 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -3054,6 +3054,24 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.write(m_writer.set_chamber_temperature(max_chamber_temp, true)); // set chamber_temperature } + // iXex: inject mode G-code before machine_start_gcode so the carriage mode + // macro is active before any motion, homing, or probing in start G-code. + if (print.config().is_ixex.value && !print.objects().empty()) { + const std::string& ixex_mode = print.objects().front()->config().ixex_parallel_mode.value; + if (!ixex_mode.empty() && ixex_mode != "primary") { + const auto& mode_names = print.config().ixex_mode_names.values; + const auto& mode_gcodes = print.config().ixex_mode_gcodes.values; + for (size_t i = 0; i < mode_names.size() && i < mode_gcodes.size(); ++i) { + if (mode_names[i] == ixex_mode && !mode_gcodes[i].empty()) { + std::string ixex_gcode = this->placeholder_parser_process( + "ixex_mode_gcode", mode_gcodes[i], initial_extruder_id); + file.writeln(ixex_gcode); + break; + } + } + } + } + // Write the custom start G-code file.writeln(machine_start_gcode); diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 826d8a1a19..b8df0aab53 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -605,6 +605,12 @@ void Preset::save(DynamicPrintConfig* parent_config) ConfigOptionVectorBase* opt_vec_inherit = static_cast(parent_config->option(option)); if (opt_vec_src->size() == 1) opt_dst->set(opt_src); + else if (opt_vec_src->size() != opt_vec_inherit->size()) { + // Size mismatch (e.g. new multi-extruder profile inheriting from a + // single-extruder base): nil-delta encoding requires matching sizes, + // so fall back to storing the full vector. + opt_dst->set(opt_src); + } else if (key_set1->find(option) != key_set1->end()) { opt_vec_dst->set_with_nil(opt_vec_src, opt_vec_inherit, 1); } @@ -955,6 +961,8 @@ static std::vector s_Preset_print_options { "enable_wrapping_detection", "seam_slope_type", "seam_slope_conditional", "scarf_angle_threshold", "scarf_joint_speed", "scarf_joint_flow_ratio", "seam_slope_start_height", "seam_slope_entire_loop", "seam_slope_min_length", "seam_slope_steps", "seam_slope_inner_walls", "scarf_overhang_threshold", "interlocking_beam", "interlocking_orientation", "interlocking_beam_layer_count", "interlocking_depth", "interlocking_boundary_avoidance", "interlocking_beam_width","calib_flowrate_topinfill_special_order", + // iXex parallel print mode (per-print selection) + "ixex_parallel_mode", }; static std::vector s_Preset_filament_options {/*"filament_colour", */ "default_filament_colour", "required_nozzle_HRC", "filament_diameter", "pellet_flow_coefficient", "volumetric_speed_coefficients", "filament_type", @@ -1031,7 +1039,11 @@ static std::vector s_Preset_printer_options { "cooling_tube_length", "high_current_on_filament_swap", "parking_pos_retraction", "extra_loading_move", "wipe_tower_type", "purge_in_prime_tower", "enable_filament_ramming", "z_offset", "disable_m73", "preferred_orientation", "emit_machine_limits_to_gcode", "pellet_modded_printer", "support_multi_bed_types", "default_bed_type", "bed_mesh_min","bed_mesh_max","bed_mesh_probe_distance", "adaptive_bed_mesh_margin", "enable_long_retraction_when_cut","long_retractions_when_cut","retraction_distances_when_cut", - "bed_temperature_formula", "nozzle_flush_dataset" + "bed_temperature_formula", "nozzle_flush_dataset", + // iXex (independent X extruder) — printer capability and user-defined modes + "is_ixex", "ixex_gantry_count", "ixex_tools_per_gantry", "ixex_tool_layout", + "ixex_carriage_width_x", "ixex_carriage_width_y", "ixex_carriage_margin", "ixex_viz_theme", + "ixex_mode_names", "ixex_mode_active_tools", "ixex_mode_gcodes" }; static std::vector s_Preset_sla_print_options { @@ -1444,6 +1456,12 @@ Preset* PresetCollection::get_preset_differed_for_save(Preset& preset) ConfigOptionVectorBase* opt_vec_inherit = static_cast(parent_preset->config.option(option)); if (opt_vec_src->size() == 1) opt_dst->set(opt_src); + else if (opt_vec_src->size() != opt_vec_inherit->size()) { + // Size mismatch (e.g. new multi-extruder profile inheriting from a + // single-extruder base): nil-delta encoding requires matching sizes, + // so fall back to storing the full vector. + opt_dst->set(opt_src); + } else if (key_set1->find(option) != key_set1->end()) { opt_vec_dst->set_with_nil(opt_vec_src, opt_vec_inherit, 1); } diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index c4b3c28ad9..5c75c87622 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -193,6 +193,7 @@ static t_config_enum_values s_keys_map_WipeTowerType { }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(WipeTowerType) + static t_config_enum_values s_keys_map_FuzzySkinMode { { "displacement", int(FuzzySkinMode::Displacement) }, { "extrusion", int(FuzzySkinMode::Extrusion) }, @@ -5482,6 +5483,108 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionBool(true)); + // iXex (independent X extruder) — parallel printing support for IDEX/IQEX printers + def = this->add("is_ixex", coBool); + def->label = L("iXex Printer"); + def->tooltip = L("Enable iXex (independent X extruder) support for IDEX/IQEX printers with multiple independent carriages."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + + def = this->add("ixex_gantry_count", coInt); + def->label = L("Gantry Count"); + def->tooltip = L("Number of independent Y-axis gantries. 1 for IDEX/single-rail IQEX. 2 for dual-gantry systems (divides bed into rows)."); + def->min = 1; + def->max = 2; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionInt(1)); + + def = this->add("ixex_tools_per_gantry", coInt); + def->label = L("Tools per Gantry"); + def->tooltip = L("Number of independent toolheads per gantry along X. 2 for IDEX-style."); + def->min = 1; + def->max = 2; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionInt(2)); + + def = this->add("ixex_primary_col", coInt); + def->label = L("Primary Tool Column"); + def->tooltip = L("X index (0-based) of the primary toolhead. 0 = leftmost column. Determines which zone is the active print area."); + def->min = 0; + def->max = 3; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionInt(0)); + + def = this->add("ixex_primary_row", coInt); + def->label = L("Primary Tool Row"); + def->tooltip = L("Y index (0-based) of the primary toolhead's gantry. 0 = front gantry. Determines which zone is the active print area."); + def->min = 0; + def->max = 3; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionInt(0)); + + def = this->add("ixex_tool_layout", coString); + def->label = L("Tool 0 Corner"); + def->tooltip = L("Physical corner of the bed where tool T0 (index 0) is located. " + "Determines how tool indices map to bed zones. " + "front = lower Y (near the operator), rear = higher Y (back of machine)."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionString("front-left")); + + def = this->add("ixex_carriage_width_x", coFloat); + def->label = L("Carriage Width X"); + def->tooltip = L("Toolhead footprint in the X direction (mm). Used to calculate usable bed zones and detect carriage collisions."); + def->sidetext = L("mm"); + def->min = 0; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(30.0)); + + def = this->add("ixex_carriage_width_y", coFloat); + def->label = L("Carriage Width Y"); + def->tooltip = L("Toolhead footprint in the Y direction (mm). Used for collision detection."); + def->sidetext = L("mm"); + def->min = 0; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(30.0)); + + def = this->add("ixex_carriage_margin", coFloat); + def->label = L("Safety Margin"); + def->tooltip = L("Non-blocking advisory clearance strip (mm) drawn inside the primary zone at each carriage boundary. Parts placed within this strip will still slice; it is a visual reminder to leave extra clearance near the zone edge."); + def->sidetext = L("mm"); + def->min = 0; + def->max = 200; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0.0)); + + def = this->add("ixex_viz_theme", coString); + def->label = L("Visualization Theme"); + def->tooltip = L("Color theme for iXex bed zone visualization. Choose a colorblind-friendly theme if needed."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionString("standard")); + + def = this->add("ixex_parallel_mode", coString); + def->label = L("iXex Print Mode"); + def->tooltip = L("Name of the active iXex parallel print mode, or \"primary\" for single-carriage printing. Requires iXex Printer enabled in the Printer preset (Printer \u2192 iXex tab)."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionString("primary")); + + def = this->add("ixex_mode_names", coStrings); + def->label = L("iXex Mode Names"); + def->tooltip = L("Display names for each user-defined iXex parallel print mode."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionStrings()); + + def = this->add("ixex_mode_active_tools", coStrings); + def->label = L("iXex Mode Active Tools"); + def->tooltip = L("Tool role assignments for each mode. Format: \"idx:P,idx:C,idx:M\" where P=Primary, C=Copy, M=Mirror (e.g. \"0:P,1:C,2:M,3:M\"). Managed by the iXex Modes editor in the Printer preset."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionStrings()); + + def = this->add("ixex_mode_gcodes", coStrings); + def->label = L("iXex Mode G-codes"); + def->tooltip = L("G-code or macro call to inject at print start for each mode."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionStrings()); + def = this->add("manual_filament_change", coBool); def->label = L("Manual Filament Change"); def->tooltip = L("Enable this option to omit the custom Change filament G-code only at the beginning of the print. " diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 54c26c312a..a32c773517 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -63,6 +63,7 @@ enum class WipeTowerType { Type2, }; + enum PrintHostType { htPrusaLink, htPrusaConnect, htOctoPrint, htDuet, htFlashAir, htAstroBox, htRepetier, htMKS, htESP3D, htCrealityPrint, htObico, htFlashforge, htSimplyPrint, htElegooLink }; @@ -496,6 +497,7 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(GCodeFlavor) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(FuzzySkinType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(FuzzySkinMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(WipeTowerType) + CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(NoiseType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(InfillPattern) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(IroningType) @@ -1035,6 +1037,9 @@ PRINT_CONFIG_CLASS_DEFINE( // Orca: internal use only ((ConfigOptionBool, calib_flowrate_topinfill_special_order)) // ORCA: special flag for flow rate calibration + + // iXex parallel print mode (per-print selection, stores mode name or "primary") + ((ConfigOptionString, ixex_parallel_mode)) ) // This object is mapped to Perl as Slic3r::Config::PrintRegion. @@ -1332,6 +1337,17 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionStrings, filament_start_gcode)) ((ConfigOptionBool, single_extruder_multi_material)) ((ConfigOptionBool, manual_filament_change)) + // iXex (independent X extruder) — parallel printing support for IDEX/IQEX printers + ((ConfigOptionBool, is_ixex)) + ((ConfigOptionInt, ixex_gantry_count)) + ((ConfigOptionInt, ixex_tools_per_gantry)) + ((ConfigOptionInt, ixex_primary_col)) + ((ConfigOptionInt, ixex_primary_row)) + ((ConfigOptionFloat, ixex_carriage_width_x)) + ((ConfigOptionFloat, ixex_carriage_width_y)) + ((ConfigOptionStrings, ixex_mode_names)) + ((ConfigOptionStrings, ixex_mode_active_tools)) + ((ConfigOptionStrings, ixex_mode_gcodes)) ((ConfigOptionBool, single_extruder_multi_material_priming)) ((ConfigOptionBool, wipe_tower_no_sparse_layers)) ((ConfigOptionString, change_filament_gcode)) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 4814548a10..0ea8749b33 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -5,6 +5,7 @@ #include "libslic3r/ClipperUtils.hpp" #include "libslic3r/Print.hpp" #include "libslic3r/Geometry.hpp" +#include "libslic3r/TriangleMesh.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/Utils.hpp" #include "libslic3r/LocalesUtils.hpp" @@ -43,6 +44,7 @@ #include #include +#include #include @@ -911,6 +913,9 @@ void GCodeViewer::SequentialView::render(const bool has_render_path, float legen // marker.set_world_offset(current_offset); marker.render(canvas_width, canvas_height, view_type); marker.render_position_window(viewer, canvas_width, canvas_height, view_type); + // iXex secondary carriage markers + for (auto& sec : m_ixex_secondary_markers) + sec.render(canvas_width, canvas_height, view_type); } //float bottom = wxGetApp().plater()->get_current_canvas3D()->get_canvas_size().get_height(); @@ -965,6 +970,7 @@ void GCodeViewer::init(ConfigOptionMode mode, PresetBundle* preset_bundle) } } + m_marker_filename = filename; m_sequential_view.marker.init(filename); // initializes point sizes @@ -1504,9 +1510,254 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) const libvgcode::PathVertex& curr_vertex = m_viewer.get_current_vertex(); m_sequential_view.marker.set_world_position(libvgcode::convert(curr_vertex.position)); m_sequential_view.marker.set_z_offset(m_z_offset + 0.5f); + + // iXex: compute all carriage positions; update secondary nozzle markers; + // carriage_box_draws is populated for toolhead footprint rendering after sequential_view.render(). + // Okabe-Ito colorblind-safe palette — excludes orange (#E69F00) and sky blue (#56B4E9) + // which are used by the bed zone fills, ensuring the markers contrast against the background. + static const std::array s_carriage_colors = {{ + { 1.000f, 1.000f, 1.000f, 0.65f }, // primary — white + { 0.941f, 0.894f, 0.259f, 0.65f }, // secondary 1 — yellow (#F0E442) + { 0.835f, 0.369f, 0.000f, 0.65f }, // secondary 2 — vermilion (#D55E00) + { 0.800f, 0.475f, 0.655f, 0.65f }, // secondary 3 — reddish purple (#CC79A7) + }}; + struct CarriageDraw { Vec3f pos; ColorRGBA color; }; + std::vector carriage_box_draws; + float ixex_box_wx = 0.0f, ixex_box_wy = 0.0f; + { + PresetBundle* preset_bundle = wxGetApp().preset_bundle; + bool ixex_active = false; + if (preset_bundle && m_sequential_view.m_show_marker) { + const DynamicPrintConfig& printer_cfg = preset_bundle->printers.get_edited_preset().config; + auto* is_ixex_opt = printer_cfg.opt("is_ixex"); + if (is_ixex_opt && is_ixex_opt->value) { + const DynamicPrintConfig& process_cfg = preset_bundle->prints.get_edited_preset().config; + auto* mode_opt = process_cfg.opt("ixex_parallel_mode"); + const std::string mode = mode_opt ? mode_opt->value : "primary"; + + if (mode != m_ixex_last_mode) { + m_sequential_view.m_ixex_secondary_markers.clear(); + m_ixex_last_mode = mode; + } + + if (!mode.empty() && mode != "primary") { + auto* mode_names_opt = printer_cfg.opt("ixex_mode_names"); + auto* active_tools_opt = printer_cfg.opt("ixex_mode_active_tools"); + + auto* tpg_opt = printer_cfg.opt("ixex_tools_per_gantry"); + auto* wx_opt = printer_cfg.opt("ixex_carriage_width_x"); + auto* wy_opt = printer_cfg.opt("ixex_carriage_width_y"); + int tools_per_gantry = tpg_opt ? std::max(1, tpg_opt->value) : 1; + ixex_box_wx = wx_opt ? (float)wx_opt->value : 30.0f; + ixex_box_wy = wy_opt ? (float)wy_opt->value : 30.0f; + + // Parse "idx:P/C/M" format — matches PartPlate::calc_ixex_zones() exactly. + // Primary = state 1 (P), Copy = 2 (C), Mirror = 3 (M). + // Inactive tools (state 0) are not included in the stored string. + int pri_tool = -1; + std::vector sec_tool_ids; + std::map sec_tool_states; // tool_id -> 2=Copy, 3=Mirror + if (mode_names_opt && active_tools_opt) { + for (size_t i = 0; i < mode_names_opt->values.size(); ++i) { + if (i < active_tools_opt->values.size() && mode_names_opt->values[i] == mode) { + std::istringstream ss(active_tools_opt->values[i]); + std::string tok; + while (std::getline(ss, tok, ',')) { + tok.erase(std::remove_if(tok.begin(), tok.end(), ::isspace), tok.end()); + if (tok.empty()) continue; + try { + auto colon = tok.find(':'); + int idx = std::stoi(tok.substr(0, colon != std::string::npos ? colon : tok.size())); + int state = 1; // default: primary + if (colon != std::string::npos) { + char role = std::toupper((unsigned char)tok[colon + 1]); + if (role == 'C') state = 2; + else if (role == 'M') state = 3; + } + if (state == 1) pri_tool = idx; + else { + sec_tool_ids.push_back(idx); + sec_tool_states[idx] = state; + } + } catch (...) {} + } + break; + } + } + } + int sec_count = (int)sec_tool_ids.size(); + + if (sec_count > 0) { + ixex_active = true; + + // Lazy init secondary nozzle markers + if ((int)m_sequential_view.m_ixex_secondary_markers.size() != sec_count) { + m_sequential_view.m_ixex_secondary_markers.resize(sec_count); + for (int i = 0; i < sec_count; ++i) { + m_sequential_view.m_ixex_secondary_markers[i].init(m_marker_filename); + m_sequential_view.m_ixex_secondary_markers[i].set_color( + s_carriage_colors[(i + 1) % s_carriage_colors.size()]); + } + } + + // Bed X and Y bounds — read from the current plate's shape, which is + // in world/GL coordinates (same space as curr_vertex.position), and is + // the exact same source used by PartPlate::calc_ixex_zones(). + float bed_x_min, bed_x_max, bed_y_min, bed_y_max; + { + PartPlate* curr_plate = wxGetApp().plater()->get_partplate_list().get_curr_plate(); + const Pointfs& plate_shape = curr_plate->get_shape(); + if (!plate_shape.empty()) { + bed_x_min = (float)plate_shape[0].x(); + bed_x_max = bed_x_min; + bed_y_min = (float)plate_shape[0].y(); + bed_y_max = bed_y_min; + for (const auto& pt : plate_shape) { + bed_x_min = std::min(bed_x_min, (float)pt.x()); + bed_x_max = std::max(bed_x_max, (float)pt.x()); + bed_y_min = std::min(bed_y_min, (float)pt.y()); + bed_y_max = std::max(bed_y_max, (float)pt.y()); + } + } else { + // Fallback: use toolpath bounding box extent + bed_x_min = (float)m_paths_bounding_box.min.x(); + bed_x_max = (float)m_paths_bounding_box.max.x(); + bed_y_min = (float)m_paths_bounding_box.min.y(); + bed_y_max = (float)m_paths_bounding_box.max.y(); + } + } + + auto* gc_opt = printer_cfg.opt("ixex_gantry_count"); + int gantry_count = gc_opt ? std::max(1, gc_opt->value) : 1; + + // Apply the same flip logic as PartPlate::calc_ixex_zones() so physical + // grid positions match the bed zone visualization. + auto* layout_opt = printer_cfg.opt("ixex_tool_layout"); + const std::string layout_str = layout_opt ? layout_opt->value : "front-left"; + const bool flip_x = (layout_str == "front-right" || layout_str == "rear-right"); + const bool flip_y = (layout_str == "rear-left" || layout_str == "rear-right"); + + auto phys_col_of = [&](int tid) -> int { + int raw = tid % tools_per_gantry; + return flip_x ? (tools_per_gantry - 1 - raw) : raw; + }; + auto phys_row_of = [&](int tid) -> int { + int raw = tid / tools_per_gantry; + return flip_y ? (gantry_count - 1 - raw) : raw; + }; + + const Vec3f prim_pos = libvgcode::convert(curr_vertex.position); + const float strip_width = (bed_x_max - bed_x_min) / (float)tools_per_gantry; + const float row_strip_height = (bed_y_max - bed_y_min) / (float)gantry_count; + const int pri_phys_col = (pri_tool >= 0) ? phys_col_of(pri_tool) : 0; + const int pri_phys_row = (pri_tool >= 0) ? phys_row_of(pri_tool) : 0; + + // Pre-pass: for each physical row, find the Copy reference column. + // The primary row's reference is the primary itself. + // Non-primary rows need a Copy tool; Mirror tools on that row + // reflect that Copy's X position. + std::map row_copy_col; + row_copy_col[pri_phys_row] = pri_phys_col; + for (int i = 0; i < sec_count; ++i) { + if (sec_tool_states[sec_tool_ids[i]] == 2) { + row_copy_col[phys_row_of(sec_tool_ids[i])] = + phys_col_of(sec_tool_ids[i]); + } + } + + // Primary carriage box + carriage_box_draws.push_back({ prim_pos, s_carriage_colors[0] }); + + const float pri_zone_x = bed_x_min + (float)pri_phys_col * strip_width; + const float pri_zone_y = bed_y_min + (float)pri_phys_row * row_strip_height; + const float rel_x = prim_pos.x() - pri_zone_x; + const float rel_y = prim_pos.y() - pri_zone_y; + + for (int i = 0; i < sec_count; ++i) { + int sec_phys_col = phys_col_of(sec_tool_ids[i]); + int sec_phys_row = phys_row_of(sec_tool_ids[i]); + const int sec_state = sec_tool_states[sec_tool_ids[i]]; + + // Y: all tools in a row share a Y rail, so Y is always + // zone-relative copy from the primary. + float sec_zone_y = bed_y_min + (float)sec_phys_row * row_strip_height; + float sec_y = sec_zone_y + rel_y; + + // X: Copy places the tool at the same relative position within + // its own zone. Mirror reflects the Copy reference on this row. + float sec_x; + if (sec_state == 2) { + float sec_zone_x = bed_x_min + (float)sec_phys_col * strip_width; + sec_x = sec_zone_x + rel_x; + } else { + // Mirror: find the Copy on the same row and reflect its X. + auto ref_it = row_copy_col.find(sec_phys_row); + if (ref_it != row_copy_col.end()) { + float ref_zone_x = bed_x_min + (float)ref_it->second * strip_width; + float ref_x = ref_zone_x + rel_x; + sec_x = bed_x_min + bed_x_max - ref_x; + } else { + sec_x = bed_x_min + bed_x_max - prim_pos.x(); + } + } + + Vec3f sec_pos{ sec_x, sec_y, prim_pos.z() }; + m_sequential_view.m_ixex_secondary_markers[i].set_world_position(sec_pos); + m_sequential_view.m_ixex_secondary_markers[i].set_z_offset(m_z_offset + 0.5f); + carriage_box_draws.push_back({ + sec_pos, s_carriage_colors[(i + 1) % s_carriage_colors.size()] }); + } + } + } + } + } + if (!ixex_active) + m_sequential_view.m_ixex_secondary_markers.clear(); + } + // BBS fixed buttom margin. m_moves_slider.pos_y m_sequential_view.render(!m_no_render_path, legend_height, &m_viewer, m_viewer.get_current_vertex().gcode_id, canvas_width, canvas_height - bottom_margin * m_scale, right_margin * m_scale, m_viewer.get_view_type()); + // iXex: render toolhead footprint boxes for each active carriage. + // Each box is ixex_carriage_width_x × ixex_carriage_width_y, sitting above the nozzle tip. + if (!carriage_box_draws.empty() && ixex_box_wx > 0.0f && ixex_box_wy > 0.0f) { + // Rebuild box mesh if dimensions changed + const Vec2f new_dims{ ixex_box_wx, ixex_box_wy }; + if (!m_ixex_toolhead_box.is_initialized() || new_dims != m_ixex_toolhead_box_dims) { + m_ixex_toolhead_box_dims = new_dims; + const float box_h = std::max(ixex_box_wx, ixex_box_wy); // height ~ largest horizontal dim + indexed_triangle_set its = its_make_cube((double)ixex_box_wx, (double)ixex_box_wy, (double)box_h); + // Center in X and Y; Z starts at 0 (nozzle tip level) + for (auto& v : its.vertices) + v += Vec3f(-ixex_box_wx * 0.5f, -ixex_box_wy * 0.5f, 0.0f); + m_ixex_toolhead_box.init_from(its); + } + + GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); + if (shader) { + glsafe(::glEnable(GL_BLEND)); + glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + shader->start_using(); + shader->set_uniform("emission_factor", 0.0f); + const Camera& camera = wxGetApp().plater()->get_camera(); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + const Transform3d& view_matrix = camera.get_view_matrix(); + for (const auto& draw : carriage_box_draws) { + const Transform3d model_matrix = Geometry::translation_transform( + (draw.pos + Vec3f(0.0f, 0.0f, m_z_offset)).cast()); + shader->set_uniform("view_model_matrix", view_matrix * model_matrix); + const Matrix3d view_normal_matrix = + view_matrix.matrix().block(0, 0, 3, 3) * + model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); + shader->set_uniform("view_normal_matrix", view_normal_matrix); + m_ixex_toolhead_box.set_color(draw.color); + m_ixex_toolhead_box.render(); + } + shader->stop_using(); + glsafe(::glDisable(GL_BLEND)); + } + } + #if VGCODE_ENABLE_COG_AND_TOOL_MARKERS if (is_legend_shown()) { ImGuiWrapper& imgui = *Slic3r::GUI::wxGetApp().imgui(); @@ -2706,6 +2957,7 @@ void GCodeViewer::render_all_plates_stats(const std::vector m_ixex_secondary_markers; // one per active secondary carriage in iXex mode GCodeWindow gcode_window; float m_scale = 1.0; bool m_show_marker = false; @@ -202,6 +204,10 @@ private: ConfigOptionMode m_user_mode; bool m_fold = {false}; + std::string m_marker_filename; // cached for lazy secondary marker init + std::string m_ixex_last_mode; // detect mode changes for secondary marker rebuild + GLModel m_ixex_toolhead_box; // shared box mesh for all carriage footprint overlays + Vec2f m_ixex_toolhead_box_dims{ 0.0f, 0.0f }; // (width_x, width_y) — rebuild when changed size_t m_extruders_count; std::vector m_filament_diameters; diff --git a/src/slic3r/GUI/OG_CustomCtrl.cpp b/src/slic3r/GUI/OG_CustomCtrl.cpp index 331d4a5db8..da4564f537 100644 --- a/src/slic3r/GUI/OG_CustomCtrl.cpp +++ b/src/slic3r/GUI/OG_CustomCtrl.cpp @@ -196,6 +196,11 @@ wxPoint OG_CustomCtrl::get_pos(const Line& line, Field* field_in/* = nullptr*/) if (line.widget) { #ifndef DISABLE_BLINKING h_pos += (line.has_undo_ui() ? 3 : 1) * blinking_button_width; +#else + // When blinking is disabled, widget lines still need to honour + // option_label_at_right the same way regular lines do below. + if (opt_group->option_label_at_right) + add_buttons_width(blinking_button_width); #endif for (auto child : line.widget_sizer->GetChildren()) diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 98c285f46a..0d0429db3c 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include #include #include @@ -451,6 +453,495 @@ void PartPlate::calc_exclude_triangles(const ExPolygon &poly) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ":Unable to create exclude triangles\n"; } +// Forward declaration (defined later in this file) +static bool init_model_from_lines(GLModel &model, const Lines &lines, float z); + +void PartPlate::calc_ixex_zones() +{ + m_ixex_copy_zones.clear(); + m_ixex_mirror_zones.clear(); + m_ixex_zone_borders.reset(); + // Clear blocking vectors at the top so early returns don't leave stale data. + m_ixex_secondary_zone_boxes.clear(); + m_ixex_collision_zones.clear(); + m_ixex_collision_overlay.clear(); + m_ixex_margin_overlay.clear(); + + if (!wxGetApp().preset_bundle) + return; + + const DynamicPrintConfig& printer_cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + auto* is_ixex_opt = printer_cfg.option("is_ixex"); + if (!is_ixex_opt || !is_ixex_opt->value) + return; + + const DynamicPrintConfig& process_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto* mode_opt = process_cfg.option("ixex_parallel_mode"); + std::string active_mode = mode_opt ? mode_opt->value : "primary"; + if (active_mode == "primary" || active_mode.empty()) + return; + + // Grid dimensions and tool layout from printer config + auto* gantry_opt = printer_cfg.option("ixex_gantry_count"); + auto* tpg_opt = printer_cfg.option("ixex_tools_per_gantry"); + auto* layout_opt = printer_cfg.option("ixex_tool_layout"); + + int n_cols = tpg_opt ? std::max(1, tpg_opt->value) : 2; + int n_rows = gantry_opt ? std::max(1, gantry_opt->value) : 1; + + // Layout: which corner is T0? 0=front-left, 1=front-right, 2=rear-left, 3=rear-right + // flip_x: col 0 is right(max-X); flip_y: row 0 is rear(max-Y) + std::string layout_str = layout_opt ? layout_opt->value : "front-left"; + bool flip_x = (layout_str == "front-right" || layout_str == "rear-right"); + bool flip_y = (layout_str == "rear-left" || layout_str == "rear-right"); + + // Convert tool index to physical (col=X-index, row=Y-index), col/row 0 = min-X/min-Y + auto tool_to_phys = [&](int idx) -> std::pair { + int raw_col = idx % n_cols, raw_row = idx / n_cols; + return { flip_x ? (n_cols - 1 - raw_col) : raw_col, + flip_y ? (n_rows - 1 - raw_row) : raw_row }; + }; + + int pri_col = 0, pri_row = 0; + + if (n_cols == 1 && n_rows == 1) + return; // nothing to dim with a single zone + + // Look up secondary tool indices (active in mode, but NOT the primary tool) + auto* names_opt = printer_cfg.option("ixex_mode_names"); + auto* tools_opt = printer_cfg.option("ixex_mode_active_tools"); + std::string active_tools_str; + if (names_opt && tools_opt) { + for (size_t i = 0; i < names_opt->values.size(); ++i) { + if (names_opt->values[i] == active_mode && i < tools_opt->values.size()) { + active_tools_str = tools_opt->values[i]; + break; + } + } + } + + // Parse "idx:P/C/M" format → map (1=Primary, 2=Copy, 3=Mirror) + // Backwards compat: plain "idx" → Primary + std::map tool_states; + { + std::istringstream ss(active_tools_str); + std::string token; + while (std::getline(ss, token, ',')) { + token.erase(std::remove_if(token.begin(), token.end(), ::isspace), token.end()); + if (token.empty()) continue; + try { + auto colon = token.find(':'); + int idx, state = 1; + if (colon != std::string::npos) { + idx = std::stoi(token.substr(0, colon)); + char role = std::toupper((unsigned char)token[colon + 1]); + if (role == 'C') state = 2; + else if (role == 'M') state = 3; + } else { + idx = std::stoi(token); + } + if (idx >= 0 && idx < n_rows * n_cols) + tool_states[idx] = state; + } catch (...) {} + } + } + + // Identify the Primary tool from the mode definition + for (auto& [idx, state] : tool_states) { + if (state == 1) { auto [c, r] = tool_to_phys(idx); pri_col = c; pri_row = r; break; } + } + + // Separate copy and mirror secondary cells using physical coordinates + std::set> copy_cells, mirror_cells; + for (auto& [idx, state] : tool_states) { + auto [c, r] = tool_to_phys(idx); + if (c == pri_col && r == pri_row) continue; // skip primary + if (state == 2) copy_cells.insert({c, r}); + else if (state == 3) mirror_cells.insert({c, r}); + } + + // All active secondary cells combined (for separation axis computation) + std::set> all_secondary; + for (auto& p : copy_cells) all_secondary.insert(p); + for (auto& p : mirror_cells) all_secondary.insert(p); + + auto bed_ext = get_extents(m_shape); + double x_min = bed_ext.min(0), x_max = bed_ext.max(0); + double y_min = bed_ext.min(1), y_max = bed_ext.max(1); + double zone_w = (x_max - x_min) / n_cols; + double zone_h = (y_max - y_min) / n_rows; + + // Separation axes: row-sep = secondaries on a different gantry, col-sep = different column + bool has_row_sep = false, has_col_sep = false; + for (const auto& [sc, sr] : all_secondary) { + if (sr != pri_row) has_row_sep = true; + if (sc != pri_col) has_col_sep = true; + } + + // Primary zone extent (the clear printable area): + // row-sep only → full bed width × primary row's Y band + // col-sep only → primary col's X band × full bed height + // both → primary quadrant + auto in_primary_zone = [&](int col, int row) { + bool row_ok = !has_row_sep || (row == pri_row); + bool col_ok = !has_col_sep || (col == pri_col); + return row_ok && col_ok; + }; + + ExPolygon bed_poly; + generate_print_polygon(bed_poly); + + // Build a clipped filled GLModel for an expanded zone rect and push into a vector. + auto push_zone_fill = [&](std::vector& vec, double cx0, double cx1, double cy0, double cy1) { + ExPolygon cell; + cell.contour.append({ scale_(cx0), scale_(cy0) }); + cell.contour.append({ scale_(cx1), scale_(cy0) }); + cell.contour.append({ scale_(cx1), scale_(cy1) }); + cell.contour.append({ scale_(cx0), scale_(cy1) }); + ExPolygons clipped = intersection_ex({ cell }, { bed_poly }); + if (!clipped.empty()) { + GLModel m; + if (init_model_from_poly(m, clipped.front(), GROUND_Z)) + vec.push_back(std::move(m)); + } + }; + + // Build expanded zone rects for a set of cells. + // When secondaries share only a row difference (same column as primary) → expand to full X width. + // When secondaries share only a column difference → expand to full Y height. + // When both axes differ → per-quadrant. + struct BoxRect { double x0, x1, y0, y1; }; + auto make_boxes = [&](const std::set>& cells) -> std::vector { + std::vector boxes; + if (cells.empty()) return boxes; + if (has_row_sep && !has_col_sep) { + std::set rows; for (auto& [c,r] : cells) rows.insert(r); + for (int sr : rows) + boxes.push_back({x_min, x_max, y_min + sr*zone_h, y_min + (sr+1)*zone_h}); + } else if (has_col_sep && !has_row_sep) { + std::set cols; for (auto& [c,r] : cells) cols.insert(c); + for (int sc : cols) + boxes.push_back({x_min + sc*zone_w, x_min + (sc+1)*zone_w, y_min, y_max}); + } else { + for (auto& [sc,sr] : cells) + boxes.push_back({x_min + sc*zone_w, x_min + (sc+1)*zone_w, + y_min + sr*zone_h, y_min + (sr+1)*zone_h}); + } + return boxes; + }; + + // Build fills for copy and mirror zones using expanded boxes (inactive cells not rendered). + auto push_expanded_fills = [&](std::vector& vec, const std::set>& cells) { + for (const auto& b : make_boxes(cells)) + push_zone_fill(vec, b.x0, b.x1, b.y0, b.y1); + }; + push_expanded_fills(m_ixex_copy_zones, copy_cells); + push_expanded_fills(m_ixex_mirror_zones, mirror_cells); + + // White borders: one outline per expanded box of each active secondary type. + auto build_rect_lines = [](const std::vector& boxes) -> Lines { + Lines lines; + for (const auto& b : boxes) { + lines.emplace_back(Point(scale_(b.x0), scale_(b.y0)), Point(scale_(b.x1), scale_(b.y0))); + lines.emplace_back(Point(scale_(b.x1), scale_(b.y0)), Point(scale_(b.x1), scale_(b.y1))); + lines.emplace_back(Point(scale_(b.x1), scale_(b.y1)), Point(scale_(b.x0), scale_(b.y1))); + lines.emplace_back(Point(scale_(b.x0), scale_(b.y1)), Point(scale_(b.x0), scale_(b.y0))); + } + return lines; + }; + + // Combine copy+mirror for borders (each expands independently then we collect all lines) + Lines all_border_lines = build_rect_lines(make_boxes(copy_cells)); + auto mirror_border_lines = build_rect_lines(make_boxes(mirror_cells)); + all_border_lines.insert(all_border_lines.end(), mirror_border_lines.begin(), mirror_border_lines.end()); + + if (!all_border_lines.empty()) + if (!init_model_from_lines(m_ixex_zone_borders, all_border_lines, GROUND_Z_GRIDLINE)) + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": unable to create iXex zone border lines\n"; + + // --- Secondary zone blocking --- + // Collect the expanded bounding boxes for all secondary (copy+mirror) zones. + // check_outside() uses these to prevent objects being placed outside the primary zone. + for (const auto& b : make_boxes(copy_cells)) + m_ixex_secondary_zone_boxes.push_back( + BoundingBoxf3(Vec3d(b.x0, b.y0, -1.0), Vec3d(b.x1, b.y1, 1e4))); + for (const auto& b : make_boxes(mirror_cells)) + m_ixex_secondary_zone_boxes.push_back( + BoundingBoxf3(Vec3d(b.x0, b.y0, -1.0), Vec3d(b.x1, b.y1, 1e4))); + + // --- Carriage collision danger strips --- + // Only add strips at boundaries of the PRIMARY zone — objects are only placed in the + // primary zone, so secondary-to-secondary boundaries have no relevance. + // + // Strip width is carriage/2 on the primary side only: + // right X boundary: [bnd_x - carriage_w/2, bnd_x] + // left X boundary: [bnd_x, bnd_x + carriage_w/2] + // top Y boundary: [bnd_y - carriage_h/2, bnd_y] + // bottom Y boundary:[bnd_y, bnd_y + carriage_h/2] + // + // Strip length matches the primary zone extent (same expansion logic as make_boxes): + // !has_row_sep → full bed height; has_row_sep → primary row only + // !has_col_sep → full bed width; has_col_sep → primary column only + + auto* cw_opt = printer_cfg.option("ixex_carriage_width_x"); + auto* ch_opt = printer_cfg.option("ixex_carriage_width_y"); + auto* mgn_opt = printer_cfg.option("ixex_carriage_margin"); + double carriage_w = cw_opt ? cw_opt->value : 0.0; + double carriage_h = ch_opt ? ch_opt->value : 0.0; + double margin = mgn_opt ? mgn_opt->value : 0.0; + + // Helper: build a filled GLModel polygon and push into margin overlay (advisory, non-blocking) + auto add_margin_fill = [&](double sx0, double sx1, double sy0, double sy1) { + ExPolygon cell; + cell.contour.append({ scale_(sx0), scale_(sy0) }); + cell.contour.append({ scale_(sx1), scale_(sy0) }); + cell.contour.append({ scale_(sx1), scale_(sy1) }); + cell.contour.append({ scale_(sx0), scale_(sy1) }); + ExPolygons clipped = intersection_ex({ cell }, { bed_poly }); + if (!clipped.empty()) { + GLModel m; + if (init_model_from_poly(m, clipped.front(), GROUND_Z)) + m_ixex_margin_overlay.push_back(std::move(m)); + } + }; + + // Primary zone extent — mirrors in_primary_zone / make_boxes expansion logic + double pz_x0 = has_col_sep ? x_min + pri_col * zone_w : x_min; + double pz_x1 = has_col_sep ? x_min + (pri_col + 1) * zone_w : x_max; + double pz_y0 = has_row_sep ? y_min + pri_row * zone_h : y_min; + double pz_y1 = has_row_sep ? y_min + (pri_row + 1) * zone_h : y_max; + + // Helper: build a BoundingBoxf3 strip, clip to bed, add to members + auto add_strip = [&](double sx0, double sx1, double sy0, double sy1) { + BoundingBoxf3 box; + box.min = Vec3d(sx0, sy0, -1.0); + box.max = Vec3d(sx1, sy1, 1e4); + m_ixex_collision_zones.push_back(box); + + ExPolygon cell; + cell.contour.append({ scale_(sx0), scale_(sy0) }); + cell.contour.append({ scale_(sx1), scale_(sy0) }); + cell.contour.append({ scale_(sx1), scale_(sy1) }); + cell.contour.append({ scale_(sx0), scale_(sy1) }); + ExPolygons clipped = intersection_ex({ cell }, { bed_poly }); + if (!clipped.empty()) { + GLModel m; + if (init_model_from_poly(m, clipped.front(), GROUND_Z)) + m_ixex_collision_overlay.push_back(std::move(m)); + } + }; + + // Determine which directions have mirror secondaries adjacent to the primary. + // Only mirror tools can cause carriage collisions — they move toward each other. + // Copy tools always move in the same direction, so no collision strip is needed. + bool has_right_sec = false, has_left_sec = false; + bool has_top_sec = false, has_bottom_sec = false; + for (auto& [idx, state] : tool_states) { + if (state != 3) continue; // only mirror tools (state==3) require collision strips + auto [c, r] = tool_to_phys(idx); + // X-boundary strips: any mirror in an adjacent column can cause an X collision. + if (c == pri_col + 1) has_right_sec = true; + if (c == pri_col - 1) has_left_sec = true; + // Y-boundary strips: only a mirror DIRECTLY above/below (same column) causes a Y collision. + // A mirror in a different column that happens to be in an adjacent row is only an X risk. + if (r == pri_row + 1 && c == pri_col) has_top_sec = true; + if (r == pri_row - 1 && c == pri_col) has_bottom_sec = true; + } + + // X-axis boundaries (vertical strips, width = carriage_w/2 on primary side) + if (carriage_w > 0.0) { + if (has_right_sec) { + double bnd_x = x_min + (pri_col + 1) * zone_w; + double strip_inner = bnd_x - carriage_w * 0.5; + add_strip(strip_inner, bnd_x, pz_y0, pz_y1); + if (margin > 0.0) + add_margin_fill(strip_inner - margin, strip_inner, pz_y0, pz_y1); + } + if (has_left_sec) { + double bnd_x = x_min + pri_col * zone_w; + double strip_inner = bnd_x + carriage_w * 0.5; + add_strip(bnd_x, strip_inner, pz_y0, pz_y1); + if (margin > 0.0) + add_margin_fill(strip_inner, strip_inner + margin, pz_y0, pz_y1); + } + } + + // Y-axis boundaries (horizontal strips, width = carriage_h/2 on primary side) + if (carriage_h > 0.0) { + if (has_top_sec) { + double bnd_y = y_min + (pri_row + 1) * zone_h; + double strip_inner = bnd_y - carriage_h * 0.5; + add_strip(pz_x0, pz_x1, strip_inner, bnd_y); + if (margin > 0.0) + add_margin_fill(pz_x0, pz_x1, strip_inner - margin, strip_inner); + } + if (has_bottom_sec) { + double bnd_y = y_min + pri_row * zone_h; + double strip_inner = bnd_y + carriage_h * 0.5; + add_strip(pz_x0, pz_x1, bnd_y, strip_inner); + if (margin > 0.0) + add_margin_fill(pz_x0, pz_x1, strip_inner, strip_inner + margin); + } + } +} + +// Build a cache key from the current iXex config options, or "" if iXex is off. +static std::string build_ixex_cache_key() +{ + if (!wxGetApp().preset_bundle) + return ""; + const DynamicPrintConfig& printer_cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + auto* is_ixex_opt = printer_cfg.option("is_ixex"); + if (!is_ixex_opt || !is_ixex_opt->value) + return ""; + const DynamicPrintConfig& process_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto* mode_opt = process_cfg.option("ixex_parallel_mode"); + auto* n_col_opt = printer_cfg.option("ixex_tools_per_gantry"); + auto* n_row_opt = printer_cfg.option("ixex_gantry_count"); + auto* cw_opt = printer_cfg.option("ixex_carriage_width_x"); + auto* ch_opt = printer_cfg.option("ixex_carriage_width_y"); + auto* mgn_opt = printer_cfg.option("ixex_carriage_margin"); + return (mode_opt ? mode_opt->value : "primary") + + "|" + std::to_string(n_col_opt ? n_col_opt->value : 2) + + "x" + std::to_string(n_row_opt ? n_row_opt->value : 1) + + "|cw" + std::to_string(cw_opt ? (int)cw_opt->value : 0) + + "|ch" + std::to_string(ch_opt ? (int)ch_opt->value : 0) + + "|mg" + std::to_string(mgn_opt ? (int)(mgn_opt->value * 10) : 0); +} + +// Ensure zone geometry (secondary boxes, collision strips, visual meshes) is up to date. +// Called before any collision check AND before rendering so both paths share the same data. +void PartPlate::ensure_ixex_zones() +{ + std::string cache_key = build_ixex_cache_key(); + if (cache_key != m_ixex_zones_mode_cache) { + m_ixex_zones_mode_cache = cache_key; + calc_ixex_zones(); + } +} + +bool PartPlate::has_ixex_placement_violations() +{ + ensure_ixex_zones(); + if (m_ixex_secondary_zone_boxes.empty() && m_ixex_collision_zones.empty()) + return false; + for (const auto& pr : obj_to_instance_set) { + int obj_id = pr.first; + int instance_id = pr.second; + if (!valid_instance(obj_id, instance_id)) + continue; + ModelInstance* instance = m_model->objects[obj_id]->instances[instance_id]; + Polygon hull = instance->convex_hull_2d(); + if (hull.points.empty()) + continue; + for (const auto& box : m_ixex_secondary_zone_boxes) { + if (!intersection({box.polygon(true)}, {hull}).empty()) + return true; + } + for (const auto& strip : m_ixex_collision_zones) { + if (!intersection({strip.polygon(true)}, {hull}).empty()) + return true; + } + } + return false; +} + +void PartPlate::render_ixex_zones(bool force_default_color) +{ + if (force_default_color) + return; + + ensure_ixex_zones(); + + // Read visualization theme from printer config. + struct IXexTheme { + ColorRGBA copy; // secondary copy zone fill + ColorRGBA mirror; // secondary mirror zone fill + ColorRGBA border; // zone outline + ColorRGBA danger; // blocking collision strip + ColorRGBA margin; // advisory safety margin + }; + + // Standard (Okabe-Ito orange + sky blue) + static const IXexTheme k_standard = { + { 0.337f, 0.706f, 0.914f, 0.45f }, // copy — sky blue #56B4E9 + { 0.902f, 0.624f, 0.000f, 0.45f }, // mirror — orange #E69F00 + { 1.000f, 1.000f, 1.000f, 0.90f }, // border — white + { 0.850f, 0.100f, 0.100f, 0.55f }, // danger — red + { 0.300f, 0.900f, 0.200f, 0.40f }, // margin — lime green + }; + // Deuteranopia / Protanopia: avoids red-green confusion. + // Danger strip uses strong blue-violet (red invisible to protanopes). + static const IXexTheme k_deuteranopia = { + { 0.000f, 0.447f, 0.698f, 0.50f }, // copy — blue #0072B2 + { 0.941f, 0.894f, 0.259f, 0.50f }, // mirror — yellow #F0E442 + { 1.000f, 1.000f, 1.000f, 0.90f }, // border — white + { 0.200f, 0.100f, 0.800f, 0.65f }, // danger — blue-violet (red not visible) + { 0.800f, 0.475f, 0.655f, 0.45f }, // margin — reddish purple #CC79A7 + }; + // Tritanopia: avoids blue-yellow confusion. + // Copy uses vermilion, mirror uses reddish-purple, margin uses teal. + static const IXexTheme k_tritanopia = { + { 0.835f, 0.369f, 0.000f, 0.50f }, // copy — vermilion #D55E00 + { 0.800f, 0.475f, 0.655f, 0.50f }, // mirror — reddish purple #CC79A7 + { 1.000f, 1.000f, 1.000f, 0.90f }, // border — white + { 0.850f, 0.100f, 0.100f, 0.55f }, // danger — red (visible to tritanopes) + { 0.000f, 0.700f, 0.600f, 0.45f }, // margin — teal + }; + // High contrast: saturated, higher alpha for low-vision users. + static const IXexTheme k_high_contrast = { + { 0.000f, 0.600f, 1.000f, 0.65f }, // copy — vivid blue + { 1.000f, 0.600f, 0.000f, 0.65f }, // mirror — vivid orange + { 1.000f, 1.000f, 0.000f, 1.00f }, // border — bright yellow + { 1.000f, 0.000f, 0.000f, 0.75f }, // danger — full red + { 0.000f, 1.000f, 0.200f, 0.60f }, // margin — bright green + }; + + const IXexTheme* theme = &k_standard; + if (wxGetApp().preset_bundle) { + const DynamicPrintConfig& pcfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + if (auto* t = pcfg.option("ixex_viz_theme")) { + if (t->value == "deuteranopia") theme = &k_deuteranopia; + else if (t->value == "tritanopia") theme = &k_tritanopia; + else if (t->value == "high_contrast") theme = &k_high_contrast; + } + } + + // Both fills and border lines use the flat shader already active from render(). + glsafe(::glDepthMask(GL_FALSE)); + + if (!m_ixex_copy_zones.empty()) + for (GLModel& z : m_ixex_copy_zones) + if (z.is_initialized()) { z.set_color(theme->copy); z.render(); } + + if (!m_ixex_mirror_zones.empty()) + for (GLModel& z : m_ixex_mirror_zones) + if (z.is_initialized()) { z.set_color(theme->mirror); z.render(); } + + glsafe(::glDepthMask(GL_TRUE)); + + if (m_ixex_zone_borders.is_initialized()) { + m_ixex_zone_borders.set_color(theme->border); + glsafe(::glLineWidth(2.0f)); + m_ixex_zone_borders.render(); + glsafe(::glLineWidth(1.0f)); + } + + if (!m_ixex_collision_overlay.empty()) { + glsafe(::glDepthMask(GL_FALSE)); + for (GLModel& s : m_ixex_collision_overlay) + if (s.is_initialized()) { s.set_color(theme->danger); s.render(); } + glsafe(::glDepthMask(GL_TRUE)); + } + + if (!m_ixex_margin_overlay.empty()) { + glsafe(::glDepthMask(GL_FALSE)); + for (GLModel& b : m_ixex_margin_overlay) + if (b.is_initialized()) { b.set_color(theme->margin); b.render(); } + glsafe(::glDepthMask(GL_TRUE)); + } +} + void PartPlate::calc_triangles_from_polygon(const ExPolygon &poly, GLModel &render_model){ if (poly.empty()) { render_model.reset(); @@ -1882,7 +2373,7 @@ bool PartPlate::check_mixture_of_pla_and_petg(const DynamicPrintConfig &config) if (filament_type == "PETG") has_petg = true; } else { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " check error:array bound"; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " check error:array bound"; } } } @@ -2147,7 +2638,7 @@ arrangement::ArrangePolygon PartPlate::estimate_wipe_tower_polygon(const Dynamic if (wipe_tower_brim_width_opt) { wp_brim_width = wipe_tower_brim_width_opt->getFloat(); if (wp_brim_width < 0) wp_brim_width = WipeTower::get_auto_brim_by_height((float) wt_size.z()); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("arrange wipe_tower: wp_brim_width %1%") % wp_brim_width; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("arrange wipe_tower: wp_brim_width %1%") % wp_brim_width; } x = std::clamp(x, margin, (float)plate_width - w - margin - wp_brim_width); @@ -2211,9 +2702,9 @@ void PartPlate::set_pos_and_size(Vec3d& origin, int width, int depth, int height bool size_changed = false; //size changed means the machine changed bool pos_changed = false; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate_id %1%, before, origin {%2%,%3%,%4%}, plate_width %5%, plate_depth %6%, plate_height %7%")\ + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate_id %1%, before, origin {%2%,%3%,%4%}, plate_width %5%, plate_depth %6%, plate_height %7%")\ % m_plate_index % m_origin.x() % m_origin.y() % m_origin.z() % m_width % m_depth % m_height; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": with_instance_move %1%, after, origin {%2%,%3%,%4%}, plate_width %5%, plate_depth %6%, plate_height %7%")\ + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": with_instance_move %1%, after, origin {%2%,%3%,%4%}, plate_width %5%, plate_depth %6%, plate_height %7%")\ % with_instance_move % origin.x() % origin.y() % origin.z() % width % depth % height; size_changed = ((width != m_width) || (depth != m_depth) || (height != m_height)); pos_changed = (m_origin != origin); @@ -2253,7 +2744,7 @@ void PartPlate::set_pos_and_size(Vec3d& origin, int width, int depth, int height offset.x() = offset.x() + off_x; offset.y() = offset.y() + off_y; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": object %1%, instance %2%, moved {%3%,%4%} to {%5%, %6%}")\ + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": object %1%, instance %2%, moved {%3%,%4%} to {%5%, %6%}")\ % obj_id % instance_id % off_x % off_y % offset.x() % offset.y(); instance->set_offset(offset); @@ -2390,7 +2881,7 @@ bool PartPlate::is_valid_gcode_file() return false; boost::filesystem::path gcode_file(m_gcode_result->filename); if (!boost::filesystem::exists(gcode_file)) { - BOOST_LOG_TRIVIAL(info) << "invalid gcode file, file is missing, file = " << m_gcode_result->filename; + BOOST_LOG_TRIVIAL(warning) << "invalid gcode file, file is missing, file = " << m_gcode_result->filename; return false; } return true; @@ -2469,6 +2960,9 @@ bool PartPlate::contain_instance_totally(int obj_id, int instance_id) const //check whether instance is outside the plate or not bool PartPlate::check_outside(int obj_id, int instance_id, BoundingBoxf3* bounding_box) { + // Ensure iXex zone geometry is current before any placement check. + ensure_ixex_zones(); + bool outside = true; ModelObject* object = m_model->objects[obj_id]; @@ -2512,6 +3006,32 @@ bool PartPlate::check_outside(int obj_id, int instance_id, BoundingBoxf3* boundi outside = false; } + // iXex placement check (prepare-mode bounding-box test). + // Block objects that overlap secondary (copy/mirror) zones or the carriage + // danger strip at the primary zone boundary. Reuses the existing + // outside=true → instance_outside_set → update_states() → blocks slicing path. + if (!outside && (!m_ixex_secondary_zone_boxes.empty() || !m_ixex_collision_zones.empty())) { + Polygon obj_hull = instance->convex_hull_2d(); // scaled Clipper coords + // 1. Object must not touch any secondary zone. + for (const auto& box : m_ixex_secondary_zone_boxes) { + Polygon p = box.polygon(true); + if (!intersection({ p }, { obj_hull }).empty()) { + outside = true; + break; + } + } + // 2. Object must not enter the carriage danger strip inside the primary zone. + if (!outside) { + for (const auto& strip : m_ixex_collision_zones) { + Polygon strip_poly = strip.polygon(true); + if (!intersection({ strip_poly }, { obj_hull }).empty()) { + outside = true; + break; + } + } + } + } + return outside; } @@ -2602,7 +3122,7 @@ int PartPlate::add_instance(int obj_id, int instance_id, bool move_position, Bou m_ready_for_slice = true; } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1% , m_ready_for_slice changes to %2%") % m_plate_index %m_ready_for_slice; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1% , m_ready_for_slice changes to %2%") % m_plate_index %m_ready_for_slice; return 0; } @@ -2696,7 +3216,7 @@ void PartPlate::duplicate_all_instance(unsigned int dup_count, bool need_skip, s if (skip_objects.find(instance->loaded_id) != skip_objects.end()) { instance->printable = false; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": skipped object, loaded_id %1%, name %2%, set to unprintable, no need to duplicate") % instance->loaded_id % object->name; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": skipped object, loaded_id %1%, name %2%, set to unprintable, no need to duplicate") % instance->loaded_id % object->name; continue; } } @@ -2708,7 +3228,7 @@ void PartPlate::duplicate_all_instance(unsigned int dup_count, bool need_skip, s for ( size_t new_instance_id = 0; new_instance_id < newObj->instances.size(); new_instance_id++ ) { obj_to_instance_set.emplace(std::pair(new_obj_id, new_instance_id)); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": duplicate object into plate: index_pair [%1%,%2%], obj_id %3%") % new_obj_id % new_instance_id % newObj->id().id; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": duplicate object into plate: index_pair [%1%,%2%], obj_id %3%") % new_obj_id % new_instance_id % newObj->id().id; } } } @@ -2731,10 +3251,10 @@ void PartPlate::duplicate_all_instance(unsigned int dup_count, bool need_skip, s while (skip_objects.find(instance->loaded_id) != skip_objects.end()) { instance->loaded_id ++; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": duplicated id %1% with skip, try new one %2%") %instance->id().id % instance->loaded_id; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": duplicated id %1% with skip, try new one %2%") %instance->id().id % instance->loaded_id; } } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": set obj %1% instance %2%'s loaded_id to its id %3%, name %4%") % obj_id %instance_id %instance->loaded_id % object->name; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": set obj %1% instance %2%'s loaded_id to its id %3%, name %4%") % obj_id %instance_id %instance->loaded_id % object->name; } } } @@ -3056,7 +3576,7 @@ bool PartPlate::set_shape(const Pointfs& shape, const Pointfs& exclude_areas, co if ((m_shape == new_shape)&&(m_exclude_area == new_exclude_areas) &&(m_height_to_lid == height_to_lid)&&(m_height_to_rod == height_to_rod)) { - BOOST_LOG_TRIVIAL(info) << "PartPlate same shape, skip directly"; + BOOST_LOG_TRIVIAL(warning) << "PartPlate same shape, skip directly"; return false; } @@ -3101,6 +3621,8 @@ bool PartPlate::set_shape(const Pointfs& shape, const Pointfs& exclude_areas, co const BoundingBox& pp_bbox = poly.contour.bounding_box(); calc_gridlines(poly, pp_bbox); + m_ixex_zones_mode_cache = "\x01"; // force rebuild on next render + calc_ixex_zones(); calc_vertex_for_icons(0, m_del_icon); calc_vertex_for_icons(1, m_orient_icon); @@ -3205,6 +3727,7 @@ void PartPlate::render(const Transform3d& view_matrix, const Transform3d& projec render_background(force_background_color); render_exclude_area(force_background_color); + render_ixex_zones(force_background_color); if(m_selected && wxGetApp().plater()->get_enable_wrapping_detection()){ if(!m_wrapping_detection_triangles.is_initialized()){ auto points = get_plate_wrapping_detection_area(); @@ -3286,7 +3809,7 @@ void PartPlate::update_states() } } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1% , m_ready_for_slice changes to %2%") % m_plate_index %m_ready_for_slice; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1% , m_ready_for_slice changes to %2%") % m_plate_index %m_ready_for_slice; return; } @@ -3294,7 +3817,7 @@ void PartPlate::update_states() //invalid sliced result void PartPlate::update_slice_result_valid_state(bool valid) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1% , update slice result from %2% to %3%") % m_plate_index %m_slice_result_valid %valid; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1% , update slice result from %2% to %3%") % m_plate_index %m_slice_result_valid %valid; m_slice_result_valid = valid; if (valid) m_slice_percent = 100.0f; @@ -3366,13 +3889,13 @@ int PartPlate::load_gcode_from_file(const std::string& filename) // BBS: use backup path to save temp gcode // auto path = get_tmp_gcode_path(); // if (boost::filesystem::exists(boost::filesystem::path(path))) { - // BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": file %1% exists, delete it firstly") % filename.c_str(); + // BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": file %1% exists, delete it firstly") % filename.c_str(); // boost::nowide::remove(path.c_str()); //} // std::error_code error = rename_file(filename, path); // if (error) { - // BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("Failed to rename the output G-code file from %1% to %2%, error code %3%") % filename.c_str() % path.c_str() % + // BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("Failed to rename the output G-code file from %1% to %2%, error code %3%") % filename.c_str() % path.c_str() % //error.message(); return -1; //} if (boost::filesystem::exists(filename)) { @@ -3383,7 +3906,7 @@ int PartPlate::load_gcode_from_file(const std::string& filename) update_slice_result_valid_state(true); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": found valid gcode file %1%") % filename.c_str(); + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": found valid gcode file %1%") % filename.c_str(); } else { BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": can not find gcode file %1%") % filename.c_str(); @@ -3714,7 +4237,7 @@ PartPlateList::PartPlateList(int width, int depth, int height, Plater* platerObj :m_plate_width(width), m_plate_depth(depth), m_plate_height(height), m_plater(platerObj), m_model(modelObj), printer_technology(tech), unprintable_plate(this, Vec3d(0.0 + width * (1. + LOGICAL_PART_PLATE_GAP), 0.0, 0.0), width, depth, height, platerObj, modelObj, false, tech) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":plate_width %1%, plate_depth %2%, plate_height %3%") % width % depth % height; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":plate_width %1%, plate_depth %2%, plate_height %3%") % width % depth % height; init(); } @@ -4135,8 +4658,8 @@ void PartPlateList::reset_size(int width, int depth, int height, bool reload_obj { Vec3d origin1, origin2; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":before size: plate_width %1%, plate_depth %2%, plate_height %3%") % m_plate_width % m_plate_depth % m_plate_height; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":after size: plate_width %1%, plate_depth %2%, plate_height %3%") % width % depth % height; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":before size: plate_width %1%, plate_depth %2%, plate_height %3%") % m_plate_width % m_plate_depth % m_plate_height; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":after size: plate_width %1%, plate_depth %2%, plate_height %3%") % width % depth % height; if ((m_plate_width != width) || (m_plate_depth != depth) || (m_plate_height != height)) { m_plate_width = width; @@ -4352,7 +4875,7 @@ int PartPlateList::duplicate_plate(int index) // go over the instances and pair with the object for (size_t new_instance_id = 0; new_instance_id < object_copy->instances.size(); new_instance_id++){ new_plate->obj_to_instance_set.emplace(std::pair(new_obj_id, new_instance_id)); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": duplicate object into plate: index_pair [%1%,%2%], obj_id %3%") % new_obj_id % new_instance_id % object_copy->id().id; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": duplicate object into plate: index_pair [%1%,%2%], obj_id %3%") % new_obj_id % new_instance_id % object_copy->id().id; } } new_plate->translate_all_instance(plate_to_plate_offset); @@ -4715,7 +5238,7 @@ void PartPlateList::update_plate_cols() m_plate_count = m_plate_list.size(); m_plate_cols = compute_colum_count(m_plate_count); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":m_plate_count %1%, m_plate_cols change to %2%") % m_plate_count % m_plate_cols; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":m_plate_count %1%, m_plate_cols change to %2%") % m_plate_count % m_plate_cols; return; } @@ -4796,7 +5319,7 @@ int PartPlateList::lock_plate(int index, bool state) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(":can not get plate for index %1%, size %2%") % index % m_plate_list.size(); return -1; } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":lock plate %1%, to state %2%") % index % state; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":lock plate %1%, to state %2%") % index % state; plate->lock(state); @@ -5714,7 +6237,7 @@ Print& PartPlateList::get_current_fff_print() const Print* print; current_plate = m_plate_list[m_current_plate]; - //BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":m_current_plate %1%, current_plate %2%") % m_current_plate % current_plate; + //BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":m_current_plate %1%, current_plate %2%") % m_current_plate % current_plate; assert(current_plate != NULL); current_plate->get_print((PrintBase **)&print, nullptr, nullptr); @@ -5728,7 +6251,7 @@ GCodeProcessorResult* PartPlateList::get_current_slice_result() const PartPlate* current_plate; current_plate = m_plate_list[m_current_plate]; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":m_current_plate %1%, current_plate %2%") % m_current_plate % current_plate; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":m_current_plate %1%, current_plate %2%") % m_current_plate % current_plate; assert(current_plate != NULL); return current_plate->get_slice_result(); @@ -5933,7 +6456,7 @@ int PartPlateList::rebuild_plates_after_deserialize(std::vector& previous_ //not used /*if (m_plate_width == 0) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": jump to the first init state, need to re-set size!"); + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": jump to the first init state, need to re-set size!"); Vec3d max = m_plater->get_bed().get_bounding_box(false).max; Vec3d min = m_plater->get_bed().get_bounding_box(false).min; double z = m_plater->config()->opt_float("printable_height"); @@ -5947,7 +6470,7 @@ int PartPlateList::rebuild_plates_after_arrangement(bool recycle_plates, bool ex { int ret = 0; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":before rebuild, plates count %1%, recycle_plates %2%") % m_plate_list.size() % recycle_plates; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":before rebuild, plates count %1%, recycle_plates %2%") % m_plate_list.size() % recycle_plates; // sort by arrange_order std::sort(m_model->objects.begin(), m_model->objects.end(), [](auto a, auto b) {return a->instances[0]->arrange_order < b->instances[0]->arrange_order; }); @@ -5964,7 +6487,7 @@ int PartPlateList::rebuild_plates_after_arrangement(bool recycle_plates, bool ex || !m_plate_list[i]->has_printable_instances()) { //delete it - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":delete plate %1% for empty") % i; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":delete plate %1% for empty") % i; delete_plate(i); } else if (m_plate_list[i]->is_locked()) { @@ -5984,7 +6507,7 @@ int PartPlateList::rebuild_plates_after_arrangement(bool recycle_plates, bool ex } #endif - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":after rebuild, plates count %1%") % m_plate_list.size(); + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":after rebuild, plates count %1%") % m_plate_list.size(); return ret; } @@ -6002,10 +6525,10 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w plate_data_item->locked = m_plate_list[i]->m_locked; plate_data_item->plate_index = m_plate_list[i]->m_plate_index; plate_data_item->plate_name = m_plate_list[i]->get_plate_name(); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1% before load, width %2%, height %3%, size %4%!") + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1% before load, width %2%, height %3%, size %4%!") %(i+1) %m_plate_list[i]->thumbnail_data.width %m_plate_list[i]->thumbnail_data.height %m_plate_list[i]->thumbnail_data.pixels.size(); plate_data_item->plate_thumbnail.load_from(m_plate_list[i]->thumbnail_data); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1% after load, width %2%, height %3%, size %4%!") + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1% after load, width %2%, height %3%, size %4%!") %(i+1) %plate_data_item->plate_thumbnail.width %plate_data_item->plate_thumbnail.height %plate_data_item->plate_thumbnail.pixels.size(); plate_data_item->config.apply(*m_plate_list[i]->config()); @@ -6022,7 +6545,7 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w plate_data_item->objects_and_instances.emplace_back(it->first, it->second); } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ <m_gcode_result->filename % with_slice_info %m_plate_list[i]->is_slice_result_valid()%plate_data_item->objects_and_instances.size(); if (with_slice_info) { @@ -6055,12 +6578,12 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w } plate_data_item->is_support_used = print->is_support_used(); } else { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("print is null!"); + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("print is null!"); } //parse filament info plate_data_item->parse_filament_info(m_plate_list[i]->get_slice_result()); } else { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "slice result = " << m_plate_list[i]->get_slice_result() + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << "slice result = " << m_plate_list[i]->get_slice_result() << ", result valid = " << m_plate_list[i]->is_slice_result_valid(); } } @@ -6068,7 +6591,7 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w plate_data_list.push_back(plate_data_item); } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":stored %1% plates!") % m_plate_list.size(); + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":stored %1% plates!") % m_plate_list.size(); return ret; } @@ -6094,7 +6617,7 @@ int PartPlateList::load_from_3mf_structure(PlateDataPtrs& plate_data_list, int f { BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":plate index %1% seems invalid, skip it")% plate_data_list[i]->plate_index; } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1%, gcode_file %2%, is_sliced_valid %3%, toolpath_outside %4%, is_support_used %5% is_label_object_enabled %6%") + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1%, gcode_file %2%, is_sliced_valid %3%, toolpath_outside %4%, is_support_used %5% is_label_object_enabled %6%") %i %plate_data_list[i]->gcode_file %plate_data_list[i]->is_sliced_valid %plate_data_list[i]->toolpath_outside %plate_data_list[i]->is_support_used %plate_data_list[i]->is_label_object_enabled; //load object and instance from 3mf //just test for file correct or not, we will rebuild later @@ -6123,17 +6646,17 @@ int PartPlateList::load_from_3mf_structure(PlateDataPtrs& plate_data_list, int f gcode_result->warnings = plate_data_list[i]->warnings; gcode_result->filament_maps = plate_data_list[i]->filament_maps; if (m_plater && !plate_data_list[i]->thumbnail_file.empty()) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1%, load thumbnail from %2%.")%(i+1) %plate_data_list[i]->thumbnail_file; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1%, load thumbnail from %2%.")%(i+1) %plate_data_list[i]->thumbnail_file; if (boost::filesystem::exists(plate_data_list[i]->thumbnail_file)) { m_plate_list[index]->load_thumbnail_data(plate_data_list[i]->thumbnail_file, m_plate_list[index]->thumbnail_data); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ <thumbnail_data.width %m_plate_list[index]->thumbnail_data.height %m_plate_list[index]->thumbnail_data.pixels.size(); } } if (m_plater && !plate_data_list[i]->no_light_thumbnail_file.empty()) { if (boost::filesystem::exists(plate_data_list[i]->no_light_thumbnail_file)) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1%, load no_light_thumbnail_file from %2%.")%(i+1) %plate_data_list[i]->no_light_thumbnail_file; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1%, load no_light_thumbnail_file from %2%.")%(i+1) %plate_data_list[i]->no_light_thumbnail_file; m_plate_list[index]->load_thumbnail_data(plate_data_list[i]->no_light_thumbnail_file, m_plate_list[index]->no_light_thumbnail_data); } } @@ -6146,13 +6669,13 @@ int PartPlateList::load_from_3mf_structure(PlateDataPtrs& plate_data_list, int f }*/ if (m_plater && !plate_data_list[i]->top_file.empty()) { if (boost::filesystem::exists(plate_data_list[i]->top_file)) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1%, load top_thumbnail from %2%.")%(i+1) %plate_data_list[i]->top_file; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1%, load top_thumbnail from %2%.")%(i+1) %plate_data_list[i]->top_file; m_plate_list[index]->load_thumbnail_data(plate_data_list[i]->top_file, m_plate_list[index]->top_thumbnail_data); } } if (m_plater && !plate_data_list[i]->pick_file.empty()) { if (boost::filesystem::exists(plate_data_list[i]->pick_file)) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1%, load pick_thumbnail from %2%.")%(i+1) %plate_data_list[i]->pick_file; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1%, load pick_thumbnail from %2%.")%(i+1) %plate_data_list[i]->pick_file; m_plate_list[index]->load_thumbnail_data(plate_data_list[i]->pick_file, m_plate_list[index]->pick_thumbnail_data); } } @@ -6588,7 +7111,7 @@ void PartPlateList::on_extruder_count_changed(int extruder_count) for (unsigned int i = 0; i < (unsigned int) m_plate_list.size(); ++i) { m_plate_list[i]->on_extruder_count_changed(extruder_count); } - BOOST_LOG_TRIVIAL(info) << boost::format("%1%: extruder_count=%2%")% __FUNCTION__ %extruder_count; + BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: extruder_count=%2%")% __FUNCTION__ %extruder_count; } void PartPlateList::set_filament_count(int filament_count) @@ -6598,7 +7121,7 @@ void PartPlateList::set_filament_count(int filament_count) { m_plate_list[i]->set_filament_count(filament_count); } - BOOST_LOG_TRIVIAL(info) << boost::format("%1%: filament_count=%2%")% __FUNCTION__ %filament_count; + BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: filament_count=%2%")% __FUNCTION__ %filament_count; } void PartPlateList::on_filament_added(int filament_count) @@ -6608,7 +7131,7 @@ void PartPlateList::on_filament_added(int filament_count) { m_plate_list[i]->on_filament_added(); } - BOOST_LOG_TRIVIAL(info) << boost::format("%1%: filament_count=%2%")% __FUNCTION__ %filament_count; + BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: filament_count=%2%")% __FUNCTION__ %filament_count; } void PartPlateList::on_filament_deleted(int filament_count, int filament_id) @@ -6618,7 +7141,7 @@ void PartPlateList::on_filament_deleted(int filament_count, int filament_id) { m_plate_list[i]->on_filament_deleted(filament_count, filament_id); } - BOOST_LOG_TRIVIAL(info) << boost::format("%1%: filament_count=%2%, filament_id=%3%")% __FUNCTION__ %filament_count %filament_id; + BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: filament_count=%2%, filament_id=%3%")% __FUNCTION__ %filament_count %filament_id; } }//end namespace GUI diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index 9585c7aa95..a82d315328 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -135,6 +135,14 @@ private: GLModel m_logo_triangles; GLModel m_gridlines; GLModel m_gridlines_bolder; + std::vector m_ixex_copy_zones; // blue tint (Wong #56B4E9) + std::vector m_ixex_mirror_zones; // orange tint (Wong #E69F00) + GLModel m_ixex_zone_borders; // white outlines around active secondary zones + std::vector m_ixex_secondary_zone_boxes; // secondary (copy/mirror) zone rects — objects blocked here + std::vector m_ixex_collision_zones; // carriage danger strips inside primary zone (mm) + std::vector m_ixex_collision_overlay; // red-orange rendered fill for danger strips + std::vector m_ixex_margin_overlay; // amber advisory bands just inside collision strips + std::string m_ixex_zones_mode_cache{ "\x01" }; // cached mode+topology key; sentinel = unbuilt GLModel m_height_limit_common; GLModel m_height_limit_bottom; GLModel m_height_limit_top; @@ -177,6 +185,9 @@ private: void calc_triangles_from_polygon(const ExPolygon &poly, GLModel& render_model); void calc_gridlines(const ExPolygon& poly, const BoundingBox& pp_bbox); void calc_height_limit(); + void calc_ixex_zones(); + void ensure_ixex_zones(); + void render_ixex_zones(bool force_default_color); void calc_vertex_for_number(int index, bool one_number, GLModel &buffer); void calc_vertex_for_plate_name_edit_icon(GLTexture *texture, int index, PickingModel &model); void calc_vertex_for_icons(int index, PickingModel &model); @@ -432,6 +443,8 @@ public: { return m_ready_for_slice && !m_apply_invalid; } + // Returns true if any instance on this plate overlaps an iXex secondary or collision zone. + bool has_ixex_placement_violations(); void update_slice_ready_status(bool ready_slice) { m_ready_for_slice = ready_slice; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 64274adf19..2b07bc94a8 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -7722,6 +7722,15 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool q->post_process_string_object_exception(err); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": validate err=%1%, warning=%2%")%err.string%warning.string; + // iXex placement check: if objects overlap secondary zones, treat it as a + // validation error so the standard pathway handles button state, notifications, + // and auto-slice blocking consistently. + if (err.string.empty()) { + PartPlate* ixex_plate = partplate_list.get_curr_plate(); + if (ixex_plate && ixex_plate->has_ixex_placement_violations()) + err.string = _u8L("Cannot slice: objects are in secondary zones reserved for iXex parallel printing."); + } + if (err.string.empty()) { this->partplate_list.get_curr_plate()->update_apply_result_invalid(false); notification_manager->set_all_slicing_errors_gray(true); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index c94fd1c7a4..d19800a376 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -45,6 +45,7 @@ #include "Widgets/Label.hpp" #include "Widgets/TabCtrl.hpp" +#include "Widgets/ComboBox.hpp" #include "MarkdownTip.hpp" #include "Search.hpp" #include "BedShapeDialog.hpp" @@ -2673,6 +2674,26 @@ void TabPrint::build() optgroup->append_single_option_line("timelapse_type", "others_settings_special_mode#timelapse"); optgroup->append_single_option_line("enable_wrapping_detection"); + // Dynamic combo box — choices come from the printer preset's ixex_mode_names. + // Use create_line_with_widget so the undo bitmaps are set (has_undo_ui()=true), + // ensuring the widget x-position matches other option fields in OG_CustomCtrl. + create_line_with_widget(optgroup.get(), "ixex_parallel_mode", "", [this](wxWindow* parent) -> wxSizer* { + m_ixex_mode_combo = new ComboBox(parent, wxID_ANY, wxEmptyString, + wxDefaultPosition, + wxSize(12 * wxGetApp().em_unit(), -1), + 0, nullptr, wxCB_READONLY); + m_ixex_mode_combo->GetDropDown().SetUseContentWidth(true); + refresh_ixex_mode_combo(); + m_ixex_mode_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { + if (!m_ixex_mode_combo || !m_config) return; + std::string val = m_ixex_mode_combo->GetStringSelection().ToStdString(); + m_config->set_key_value("ixex_parallel_mode", new ConfigOptionString(val)); + on_value_change("ixex_parallel_mode", val); + }); + auto* s = new wxBoxSizer(wxHORIZONTAL); + s->Add(m_ixex_mode_combo, 0, wxALIGN_CENTER_VERTICAL); + return s; + }); optgroup = page->new_optgroup(L("Fuzzy Skin"), L"fuzzy_skin"); optgroup->append_single_option_line("fuzzy_skin", "others_settings_fuzzy_skin"); @@ -2731,6 +2752,36 @@ void TabPrint::reload_config() { this->compatible_widget_reload(m_compatible_printers); Tab::reload_config(); + refresh_ixex_mode_combo(); +} + +void TabPrint::refresh_ixex_mode_combo() +{ + if (!m_ixex_mode_combo) return; + // Rebuild choices: "primary" + names from the active printer preset + m_ixex_mode_combo->Clear(); + m_ixex_mode_combo->Append("primary"); + if (m_preset_bundle) { + auto* names = m_preset_bundle->printers.get_edited_preset().config + .option("ixex_mode_names"); + if (names) { + for (const auto& n : names->values) + if (!n.empty()) m_ixex_mode_combo->Append(wxString::FromUTF8(n)); + } + } + // Restore current selection from process config (safe: option may not exist in older presets) + std::string cur = "primary"; + if (m_config) { + auto* opt = m_config->option("ixex_parallel_mode"); + if (opt) cur = opt->value; + } + if (!m_ixex_mode_combo->SetStringSelection(cur)) { + m_ixex_mode_combo->SetSelection(0); // fall back to "primary" + // The active mode no longer exists — reset config so stale name doesn't crash downstream. + if (m_config) + m_config->set_key_value("ixex_parallel_mode", new ConfigOptionString("primary")); + on_value_change("ixex_parallel_mode", std::string("primary")); + } } void TabPrint::update_description_lines() @@ -4335,6 +4386,324 @@ void TabPrinter::build() m_printer_technology == ptSLA ? build_sla() : build_fff(); } +// --------------------------------------------------------------------------- +// IXexModesCtrl — visual parallel-mode editor for the iXex printer tab +// +// Tool button states: +// 0 = Inactive (grey) — not participating in this mode +// 1 = Primary (green) — the tool the slicer generates paths for +// 2 = Copy (blue) — firmware duplicates Primary at an offset +// 3 = Mirror (amber) — firmware mirrors Primary about an axis +// +// Active-tools string format: "idx:P,idx:C,idx:M" (backwards-compat: plain "idx" = Primary) +// --------------------------------------------------------------------------- +class IXexModesCtrl : public wxPanel { +public: + std::function on_change; + + // layout: 0=front-left, 1=front-right, 2=rear-left, 3=rear-right (T0 corner) + static int parse_layout(const std::string& s) { + if (s == "front-right") return 1; + if (s == "rear-left") return 2; + if (s == "rear-right") return 3; + return 0; // "front-left" default + } + + IXexModesCtrl(wxWindow* parent, int n_cols, int n_rows, int layout = 0) + : wxPanel(parent, wxID_ANY), m_n_cols(std::max(1, n_cols)), m_n_rows(std::max(1, n_rows)), m_layout(layout) + { + m_outer = new wxBoxSizer(wxVERTICAL); + m_rows_sizer = new wxBoxSizer(wxVERTICAL); + + // --- Info panel: instruction text + color legend --- + auto* info_panel = new wxPanel(this, wxID_ANY); + auto* info_sizer = new wxBoxSizer(wxVERTICAL); + + auto* inst = new wxStaticText(info_panel, wxID_ANY, + _L("Each mode defines which tool heads participate and their roles. " + "The Primary tool (green) drives all sliced paths. " + "Copy (blue) and Mirror (amber) tools follow the Primary at the firmware level. " + "Click a tool button to cycle its role — clear the Primary before reassigning it.")); + inst->Wrap(620); + info_sizer->Add(inst, 0, wxBOTTOM, 6); + + // Color legend + auto* leg_sizer = new wxBoxSizer(wxHORIZONTAL); + struct { int state; const char* label; } legend[] = { + {1,"Primary"},{2,"Copy"},{3,"Mirror"} + }; + for (auto& l : legend) { + auto* swatch = new wxPanel(info_panel, wxID_ANY, wxDefaultPosition, wxSize(12, 12)); + swatch->SetBackgroundColour(btn_color(l.state)); + leg_sizer->Add(swatch, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 4); + leg_sizer->Add(new wxStaticText(info_panel, wxID_ANY, l.label), + 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 16); + } + info_sizer->Add(leg_sizer, 0, wxBOTTOM, 6); + info_panel->SetSizer(info_sizer); + + // Column header row (fixed-pixel spacers to approximately align with mode row fields) + // Name field: 130px + 6px gap; Tool grid: n_cols*(36+2)-2 px + 6px gap + auto* hdr_panel = new wxPanel(this, wxID_ANY); + auto* hdr_sizer = new wxBoxSizer(wxHORIZONTAL); + auto* hdr_name = new wxStaticText(hdr_panel, wxID_ANY, _L("Name")); + auto* hdr_tools = new wxStaticText(hdr_panel, wxID_ANY, _L("Tools")); + auto* hdr_gcode = new wxStaticText(hdr_panel, wxID_ANY, _L("G-code")); + int grid_px = m_n_cols * 38 - 2; // approx grid panel width + hdr_sizer->Add(hdr_name, 0, wxALIGN_CENTER_VERTICAL); + hdr_sizer->AddSpacer(std::max(0, 136 - hdr_name->GetBestSize().x)); // "Name" col = 136px total + hdr_sizer->Add(hdr_tools, 0, wxALIGN_CENTER_VERTICAL); + hdr_sizer->AddSpacer(std::max(0, grid_px + 6 - hdr_tools->GetBestSize().x)); // "Tools" col = grid_px+6 total + hdr_sizer->Add(hdr_gcode, 1, wxALIGN_CENTER_VERTICAL); + hdr_panel->SetSizer(hdr_sizer); + + auto* add_btn = new wxButton(this, wxID_ANY, _L("+ Add Mode"), + wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT); + add_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { add_row(); notify(); }); + + m_outer->Add(info_panel, 0, wxEXPAND | wxBOTTOM, 4); + m_outer->Add(hdr_panel, 0, wxEXPAND | wxBOTTOM, 2); + m_outer->Add(m_rows_sizer, 0, wxEXPAND); + m_outer->AddSpacer(4); + m_outer->Add(add_btn, 0); + SetSizer(m_outer); + } + + void set_grid_size(int n_cols, int n_rows, int layout = -1) { + n_cols = std::max(1, n_cols); + n_rows = std::max(1, n_rows); + if (layout < 0) layout = m_layout; + if (n_cols == m_n_cols && n_rows == m_n_rows && layout == m_layout) return; + auto [names, tools, gcodes] = get_mode_data(); + clear_rows(); + m_n_cols = n_cols; + m_n_rows = n_rows; + m_layout = layout; + for (size_t i = 0; i < names.size(); ++i) + add_row(names[i], tools[i], gcodes[i]); + Layout(); + } + + void load_from_config(const DynamicPrintConfig& cfg) { + clear_rows(); + auto* names = cfg.option("ixex_mode_names"); + auto* tools = cfg.option("ixex_mode_active_tools"); + auto* gcodes = cfg.option("ixex_mode_gcodes"); + size_t n = names ? names->values.size() : 0; + for (size_t i = 0; i < n; ++i) + add_row(names->values[i], + (tools && i < tools->values.size()) ? tools->values[i] : "", + (gcodes && i < gcodes->values.size()) ? gcodes->values[i] : ""); + Layout(); + } + + std::tuple, std::vector, std::vector> + get_mode_data() const { + std::vector names, tools, gcodes; + for (auto& r : m_rows) { + std::string nm = r.name->GetValue().ToStdString(); + if (nm.empty()) continue; + names.push_back(nm); + tools.push_back(active_tools_string(r)); + gcodes.push_back(r.gcode->GetValue().ToStdString()); + } + return {names, tools, gcodes}; + } + + // Parse "idx:P,idx:C,idx:M" → map + // Backwards compat: plain "idx" (no role) → state 1 (Primary) + static std::map parse_tool_states(const std::string& s) { + std::map result; + if (s.empty()) return result; + std::istringstream ss(s); + std::string tok; + while (std::getline(ss, tok, ',')) { + tok.erase(std::remove_if(tok.begin(), tok.end(), ::isspace), tok.end()); + if (tok.empty()) continue; + try { + auto colon = tok.find(':'); + int idx, state = 1; + if (colon != std::string::npos) { + idx = std::stoi(tok.substr(0, colon)); + char role = std::toupper((unsigned char)tok[colon + 1]); + if (role == 'C') state = 2; + else if (role == 'M') state = 3; + } else { + idx = std::stoi(tok); + } + result[idx] = state; + } catch (...) {} + } + return result; + } + +private: + // State 0=inactive, 1=primary, 2=copy, 3=mirror + static wxColour btn_color(int state) { + switch (state) { + case 1: return wxColour(50, 160, 50); // green — Primary + case 2: return wxColour(60, 120, 210); // blue — Copy + case 3: return wxColour(210, 130, 20); // amber — Mirror + default: return wxColour(90, 90, 90); // grey — Inactive + } + } + static wxColour btn_fg(int /*state*/) { return *wxWHITE; } + static const char* state_role(int state) { + switch (state) { case 1: return "P"; case 2: return "C"; case 3: return "M"; default: return ""; } + } + + void apply_btn(wxButton* btn, int tool_idx, int state) { + // Always label as T{n} — the button color already encodes the role. + btn->SetLabel(wxString::Format("T%d", tool_idx)); + btn->SetBackgroundColour(btn_color(state)); + btn->SetForegroundColour(btn_fg(state)); + btn->Refresh(); + } + + struct Row { + wxPanel* panel; + wxTextCtrl* name; + wxTextCtrl* gcode; + std::vector btns; + std::vector btn_states; // parallel to btns + // tool_idx for btns[i]: computed during add_row, stored here for quick lookup + std::vector btn_tool_idx; + }; + + void add_row(const std::string& name = "", + const std::string& active_tools = "", + const std::string& gcode = "") + { + Row r; + r.panel = new wxPanel(this, wxID_ANY); + auto* sizer = new wxBoxSizer(wxHORIZONTAL); + + r.name = new wxTextCtrl(r.panel, wxID_ANY, name, wxDefaultPosition, wxSize(130, -1)); + r.name->Bind(wxEVT_TEXT, [this](wxCommandEvent&) { notify(); }); + + auto* grid_panel = new wxPanel(r.panel, wxID_ANY); + auto* grid_sizer = new wxGridSizer(m_n_rows, m_n_cols, 2, 2); + + auto tool_states = parse_tool_states(active_tools); + wxPanel* this_panel = r.panel; + + // Buttons rendered top=rear (high Y), bottom=front (low Y). + // Layout remapping: m_layout encodes which corner T0 is at physically. + // flip_x (layout 1,3): col 0 is on the right (max-X) instead of left + // flip_y (layout 2,3): row 0 is at the rear (max-Y) instead of front + // Display iterates: row from n_rows-1 down to 0 (rear→front = top→bottom). + // phys_row = row (loop var maps directly to physical bed Y index). + // tool raw index: raw_row = flip_y ? (n_rows-1-row) : row + // raw_col = flip_x ? (n_cols-1-col) : col + bool flip_x = (m_layout == 1 || m_layout == 3); + bool flip_y = (m_layout == 2 || m_layout == 3); + for (int row = m_n_rows - 1; row >= 0; --row) { + for (int col = 0; col < m_n_cols; ++col) { + int raw_row = flip_y ? (m_n_rows - 1 - row) : row; + int raw_col = flip_x ? (m_n_cols - 1 - col) : col; + int tool_idx = raw_row * m_n_cols + raw_col; + int state = 0; + auto it = tool_states.find(tool_idx); + if (it != tool_states.end()) state = it->second; + + auto* btn = new wxButton(grid_panel, wxID_ANY, wxEmptyString, + wxDefaultPosition, wxSize(36, 26), wxBU_EXACTFIT); + apply_btn(btn, tool_idx, state); + + int btn_pos = (int)r.btns.size(); + r.btns.push_back(btn); + r.btn_states.push_back(state); + r.btn_tool_idx.push_back(tool_idx); + + btn->Bind(wxEVT_BUTTON, [this, this_panel, btn_pos](wxCommandEvent&) { + // Find the row by panel pointer (stable across add/remove) + for (auto& row_ref : m_rows) { + if (row_ref.panel != this_panel) continue; + int& st = row_ref.btn_states[btn_pos]; + + // Check if another button already holds the Primary state + bool other_primary = false; + for (int j = 0; j < (int)row_ref.btn_states.size(); ++j) + if (j != btn_pos && row_ref.btn_states[j] == 1) { other_primary = true; break; } + + // Advance state, skipping Primary(1) if another tool is already Primary + st = (st + 1) % 4; + if (st == 1 && other_primary) + st = 2; // skip Primary → go straight to Copy + + apply_btn(row_ref.btns[btn_pos], row_ref.btn_tool_idx[btn_pos], st); + notify(); + break; + } + }); + + grid_sizer->Add(btn, 0); + } + } + grid_panel->SetSizer(grid_sizer); + + r.gcode = new wxTextCtrl(r.panel, wxID_ANY, gcode, + wxDefaultPosition, wxSize(220, 54), wxTE_MULTILINE); + r.gcode->Bind(wxEVT_TEXT, [this](wxCommandEvent&) { notify(); }); + + auto* rm = new wxButton(r.panel, wxID_ANY, wxT("\u00d7"), + wxDefaultPosition, wxSize(24, 24), wxBU_EXACTFIT); + rm->Bind(wxEVT_BUTTON, [this, this_panel](wxCommandEvent&) { + remove_row(this_panel); + notify(); + }); + + sizer->Add(r.name, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6); + sizer->Add(grid_panel, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6); + sizer->Add(r.gcode, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6); + sizer->Add(rm, 0, wxALIGN_CENTER_VERTICAL); + r.panel->SetSizer(sizer); + + m_rows_sizer->Add(r.panel, 0, wxEXPAND | wxBOTTOM, 4); + m_rows.push_back(std::move(r)); + Layout(); + } + + void remove_row(wxPanel* panel) { + for (size_t i = 0; i < m_rows.size(); ++i) { + if (m_rows[i].panel == panel) { + m_rows_sizer->Detach(panel); + m_rows.erase(m_rows.begin() + i); + Layout(); + // Defer widget destruction so any in-flight GTK events for + // panel's children (including the × button we're inside) finish + // processing before the wxEvtHandlers are freed. + wxTheApp->CallAfter([panel]() { panel->Destroy(); }); + return; + } + } + } + + void clear_rows() { + for (auto& r : m_rows) { m_rows_sizer->Detach(r.panel); r.panel->Destroy(); } + m_rows.clear(); + } + + std::string active_tools_string(const Row& r) const { + std::string s; + for (size_t i = 0; i < r.btn_states.size(); ++i) { + if (r.btn_states[i] == 0) continue; + if (!s.empty()) s += ','; + s += std::to_string(r.btn_tool_idx[i]) + ':' + state_role(r.btn_states[i]); + } + return s; + } + + void notify() { if (on_change) on_change(); } + + wxBoxSizer* m_outer; + wxBoxSizer* m_rows_sizer; + std::vector m_rows; + int m_n_cols, m_n_rows; + int m_layout {0}; // 0=front-left, 1=front-right, 2=rear-left, 3=rear-right +}; +// --------------------------------------------------------------------------- + void TabPrinter::build_fff() { if (!m_pages.empty()) @@ -4589,6 +4958,117 @@ void TabPrinter::build_fff() option.opt.height = gcode_field_height;//150; optgroup->append_single_option_line(option, "printer_machine_gcode#template-custom-g-code"); + page = add_options_page(L("iXex"), "custom-gcode_multi_material"); // ORCA: icon only visible on placeholders + optgroup = page->new_optgroup(L("Carriage Configuration"), L"param_advanced"); + optgroup->append_single_option_line("is_ixex"); + optgroup->append_single_option_line("ixex_gantry_count"); + optgroup->append_single_option_line("ixex_tools_per_gantry"); + { + // Tool 0 corner selector + auto opt = optgroup->get_option("ixex_tool_layout"); + auto line = Line{ opt.opt.label, opt.opt.tooltip }; + line.append_option(opt); + line.widget = [this](wxWindow* parent) -> wxSizer* { + static const wxString choices[] = { + _L("Front-left"), _L("Front-right"), + _L("Rear-left"), _L("Rear-right") + }; + static const char* values[] = { "front-left", "front-right", "rear-left", "rear-right" }; + m_ixex_layout_combo = new ComboBox(parent, wxID_ANY, wxEmptyString, + wxDefaultPosition, + wxSize(12 * wxGetApp().em_unit(), -1), + 4, choices, wxCB_READONLY); + m_ixex_layout_combo->GetDropDown().SetUseContentWidth(true); + // Set current selection + std::string cur = "front-left"; + if (auto* o = m_config->option("ixex_tool_layout")) cur = o->value; + for (int i = 0; i < 4; ++i) + if (cur == values[i]) { m_ixex_layout_combo->SetSelection(i); break; } + m_ixex_layout_combo->Bind(wxEVT_COMBOBOX, [this, values](wxCommandEvent&) { + int sel = m_ixex_layout_combo->GetSelection(); + std::string val = (sel >= 0 && sel < 4) ? values[sel] : "front-left"; + m_config->set_key_value("ixex_tool_layout", new ConfigOptionString(val)); + on_value_change("ixex_tool_layout", val); + if (m_ixex_modes_ctrl) + m_ixex_modes_ctrl->set_grid_size( + m_config->opt_int("ixex_tools_per_gantry"), + m_config->opt_int("ixex_gantry_count"), + IXexModesCtrl::parse_layout(val)); + }); + auto* s = new wxBoxSizer(wxHORIZONTAL); + s->Add(m_ixex_layout_combo, 0, wxALIGN_CENTER_VERTICAL); + return s; + }; + optgroup->append_line(line); + } + optgroup->append_single_option_line("ixex_carriage_width_x"); + optgroup->append_single_option_line("ixex_carriage_width_y"); + optgroup->append_single_option_line("ixex_carriage_margin"); + + { + static const wxString theme_choices[] = { + _L("Standard"), + _L("Deuteranopia / Protanopia (red-green)"), + _L("Tritanopia (blue-yellow)"), + _L("High Contrast") }; + static const char* theme_vals[] = { "standard", "deuteranopia", "tritanopia", "high_contrast" }; + auto opt = optgroup->get_option("ixex_viz_theme"); + auto line = Line{ opt.opt.label, opt.opt.tooltip }; + line.append_option(opt); + line.widget = [this](wxWindow* parent) -> wxSizer* { + m_ixex_theme_combo = new ComboBox(parent, wxID_ANY, wxEmptyString, + wxDefaultPosition, + wxSize(22 * wxGetApp().em_unit(), -1), + 4, theme_choices, wxCB_READONLY); + m_ixex_theme_combo->GetDropDown().SetUseContentWidth(true); + std::string cur = "standard"; + if (auto* o = m_config->option("ixex_viz_theme")) cur = o->value; + for (int i = 0; i < 4; ++i) + if (cur == theme_vals[i]) { m_ixex_theme_combo->SetSelection(i); break; } + m_ixex_theme_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { + int sel = m_ixex_theme_combo->GetSelection(); + std::string val = (sel >= 0 && sel < 4) ? theme_vals[sel] : "standard"; + m_config->set_key_value("ixex_viz_theme", new ConfigOptionString(val)); + on_value_change("ixex_viz_theme", val); + }); + auto* s = new wxBoxSizer(wxHORIZONTAL); + s->Add(m_ixex_theme_combo, 0, wxALIGN_CENTER_VERTICAL); + return s; + }; + optgroup->append_line(line); + } + + { + // Parallel modes panel: visual toggle-grid list with + button + auto modes_og = page->new_optgroup(L("Parallel Modes"), L"param_advanced"); + auto line = Line{ L("Modes"), L("") }; + line.full_width = 1; + line.widget = [this](wxWindow* parent) -> wxSizer* { + int n_cols = m_config->opt_int("ixex_tools_per_gantry"); + int n_rows = m_config->opt_int("ixex_gantry_count"); + std::string layout_str = "front-left"; + if (auto* o = m_config->option("ixex_tool_layout")) layout_str = o->value; + int layout = IXexModesCtrl::parse_layout(layout_str); + m_ixex_modes_ctrl = new IXexModesCtrl(parent, n_cols, n_rows, layout); + m_ixex_modes_ctrl->load_from_config(*m_config); + m_ixex_modes_ctrl->on_change = [this]() { + auto [names, tools, gcodes] = m_ixex_modes_ctrl->get_mode_data(); + m_config->set_key_value("ixex_mode_names", new ConfigOptionStrings(names)); + m_config->set_key_value("ixex_mode_active_tools", new ConfigOptionStrings(tools)); + m_config->set_key_value("ixex_mode_gcodes", new ConfigOptionStrings(gcodes)); + update_dirty(); + on_value_change("ixex_mode_names", std::string("")); + // Refresh the process tab's mode dropdown immediately + if (auto* print_tab = dynamic_cast(wxGetApp().get_tab(Preset::TYPE_PRINT))) + print_tab->refresh_ixex_mode_combo(); + }; + auto* sizer = new wxBoxSizer(wxHORIZONTAL); + sizer->Add(m_ixex_modes_ctrl, 1, wxEXPAND); + return sizer; + }; + modes_og->append_line(line); + } + page = add_options_page(L("Notes"), "custom-gcode_note"); // ORCA: icon only visible on placeholders optgroup = page->new_optgroup(L("Notes"), "note", 0); option = optgroup->get_option("printer_notes"); @@ -5168,6 +5648,33 @@ void TabPrinter::reload_config() // so update it implicitly if (m_active_page && m_active_page->title() == "Multimaterial") m_active_page->set_value("extruders_count", int(m_extruders_count)); + + if (m_config) { + // Sync layout combo + if (m_ixex_layout_combo) { + static const char* layout_vals[] = { "front-left", "front-right", "rear-left", "rear-right" }; + std::string cur = "front-left"; + if (auto* o = m_config->option("ixex_tool_layout")) cur = o->value; + for (int i = 0; i < 4; ++i) + if (cur == layout_vals[i]) { m_ixex_layout_combo->SetSelection(i); break; } + } + // Sync theme combo + if (m_ixex_theme_combo) { + static const char* theme_vals[] = { "standard", "deuteranopia", "tritanopia", "high_contrast" }; + std::string cur = "standard"; + if (auto* o = m_config->option("ixex_viz_theme")) cur = o->value; + for (int i = 0; i < 4; ++i) + if (cur == theme_vals[i]) { m_ixex_theme_combo->SetSelection(i); break; } + } + if (m_ixex_modes_ctrl) { + int n_cols = m_config->opt_int("ixex_tools_per_gantry"); + int n_rows = m_config->opt_int("ixex_gantry_count"); + std::string layout_str = "front-left"; + if (auto* o = m_config->option("ixex_tool_layout")) layout_str = o->value; + m_ixex_modes_ctrl->set_grid_size(n_cols, n_rows, IXexModesCtrl::parse_layout(layout_str)); + m_ixex_modes_ctrl->load_from_config(*m_config); + } + } } void TabPrinter::activate_selected_page(std::function throw_if_canceled) @@ -5184,6 +5691,9 @@ void TabPrinter::clear_pages() { Tab::clear_pages(); m_reset_to_filament_color = nullptr; + m_ixex_modes_ctrl = nullptr; + m_ixex_layout_combo = nullptr; + m_ixex_theme_combo = nullptr; } void TabPrinter::toggle_options() @@ -5393,6 +5903,15 @@ void TabPrinter::update_fff() m_use_silent_mode = m_config->opt_bool("silent_mode"); } + // Sync iXex tool grid whenever carriage configuration changes. + // set_grid_size() is a no-op when dimensions and layout are unchanged. + if (m_ixex_modes_ctrl) { + int n_cols = m_config->opt_int("ixex_tools_per_gantry"); + int n_rows = m_config->opt_int("ixex_gantry_count"); + std::string layout_str = m_config->opt_string("ixex_tool_layout"); + m_ixex_modes_ctrl->set_grid_size(n_cols, n_rows, IXexModesCtrl::parse_layout(layout_str)); + } + toggle_options(); } diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index 96b35b26e1..687cfeea73 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -41,6 +41,7 @@ #include "Widgets/RoundedRectangle.hpp" #include "Widgets/TextInput.hpp" #include "Widgets/CheckBox.hpp" // ORCA +#include "Widgets/ComboBox.hpp" class TabCtrl; @@ -463,10 +464,12 @@ public: void update() override; void clear_pages() override; bool supports_printer_technology(const PrinterTechnology tech) const override { return tech == ptFFF; } + void refresh_ixex_mode_combo(); private: ogStaticText* m_recommended_thin_wall_thickness_description_line = nullptr; ogStaticText* m_top_bottom_shell_thickness_explanation = nullptr; + ComboBox* m_ixex_mode_combo {nullptr}; }; class TabPrintModel : public TabPrint @@ -588,6 +591,8 @@ public: void set_custom_gcode(const t_config_option_key& opt_key, const std::string& value) override; }; +class IXexModesCtrl; + class TabPrinter : public Tab { private: @@ -602,6 +607,9 @@ private: std::vector m_pages_sla; wxBoxSizer* m_presets_sizer {nullptr}; + IXexModesCtrl* m_ixex_modes_ctrl {nullptr}; + ComboBox* m_ixex_layout_combo {nullptr}; + ComboBox* m_ixex_theme_combo {nullptr}; public: ScalableButton* m_reset_to_filament_color = nullptr; From e786fabb6a0eaa022273e6d462fd815811a77b40 Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Thu, 2 Apr 2026 03:02:08 -0400 Subject: [PATCH 012/434] fix: restore info log levels in PartPlate accidentally raised to warning Co-Authored-By: Claude Sonnet 4.6 --- src/slic3r/GUI/PartPlate.cpp | 86 ++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 0d0429db3c..1fb2b80d5b 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -2373,7 +2373,7 @@ bool PartPlate::check_mixture_of_pla_and_petg(const DynamicPrintConfig &config) if (filament_type == "PETG") has_petg = true; } else { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " check error:array bound"; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " check error:array bound"; } } } @@ -2638,7 +2638,7 @@ arrangement::ArrangePolygon PartPlate::estimate_wipe_tower_polygon(const Dynamic if (wipe_tower_brim_width_opt) { wp_brim_width = wipe_tower_brim_width_opt->getFloat(); if (wp_brim_width < 0) wp_brim_width = WipeTower::get_auto_brim_by_height((float) wt_size.z()); - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("arrange wipe_tower: wp_brim_width %1%") % wp_brim_width; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("arrange wipe_tower: wp_brim_width %1%") % wp_brim_width; } x = std::clamp(x, margin, (float)plate_width - w - margin - wp_brim_width); @@ -2702,9 +2702,9 @@ void PartPlate::set_pos_and_size(Vec3d& origin, int width, int depth, int height bool size_changed = false; //size changed means the machine changed bool pos_changed = false; - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate_id %1%, before, origin {%2%,%3%,%4%}, plate_width %5%, plate_depth %6%, plate_height %7%")\ + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate_id %1%, before, origin {%2%,%3%,%4%}, plate_width %5%, plate_depth %6%, plate_height %7%")\ % m_plate_index % m_origin.x() % m_origin.y() % m_origin.z() % m_width % m_depth % m_height; - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": with_instance_move %1%, after, origin {%2%,%3%,%4%}, plate_width %5%, plate_depth %6%, plate_height %7%")\ + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": with_instance_move %1%, after, origin {%2%,%3%,%4%}, plate_width %5%, plate_depth %6%, plate_height %7%")\ % with_instance_move % origin.x() % origin.y() % origin.z() % width % depth % height; size_changed = ((width != m_width) || (depth != m_depth) || (height != m_height)); pos_changed = (m_origin != origin); @@ -2744,7 +2744,7 @@ void PartPlate::set_pos_and_size(Vec3d& origin, int width, int depth, int height offset.x() = offset.x() + off_x; offset.y() = offset.y() + off_y; - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": object %1%, instance %2%, moved {%3%,%4%} to {%5%, %6%}")\ + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": object %1%, instance %2%, moved {%3%,%4%} to {%5%, %6%}")\ % obj_id % instance_id % off_x % off_y % offset.x() % offset.y(); instance->set_offset(offset); @@ -2881,7 +2881,7 @@ bool PartPlate::is_valid_gcode_file() return false; boost::filesystem::path gcode_file(m_gcode_result->filename); if (!boost::filesystem::exists(gcode_file)) { - BOOST_LOG_TRIVIAL(warning) << "invalid gcode file, file is missing, file = " << m_gcode_result->filename; + BOOST_LOG_TRIVIAL(info) << "invalid gcode file, file is missing, file = " << m_gcode_result->filename; return false; } return true; @@ -3122,7 +3122,7 @@ int PartPlate::add_instance(int obj_id, int instance_id, bool move_position, Bou m_ready_for_slice = true; } - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1% , m_ready_for_slice changes to %2%") % m_plate_index %m_ready_for_slice; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1% , m_ready_for_slice changes to %2%") % m_plate_index %m_ready_for_slice; return 0; } @@ -3216,7 +3216,7 @@ void PartPlate::duplicate_all_instance(unsigned int dup_count, bool need_skip, s if (skip_objects.find(instance->loaded_id) != skip_objects.end()) { instance->printable = false; - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": skipped object, loaded_id %1%, name %2%, set to unprintable, no need to duplicate") % instance->loaded_id % object->name; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": skipped object, loaded_id %1%, name %2%, set to unprintable, no need to duplicate") % instance->loaded_id % object->name; continue; } } @@ -3228,7 +3228,7 @@ void PartPlate::duplicate_all_instance(unsigned int dup_count, bool need_skip, s for ( size_t new_instance_id = 0; new_instance_id < newObj->instances.size(); new_instance_id++ ) { obj_to_instance_set.emplace(std::pair(new_obj_id, new_instance_id)); - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": duplicate object into plate: index_pair [%1%,%2%], obj_id %3%") % new_obj_id % new_instance_id % newObj->id().id; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": duplicate object into plate: index_pair [%1%,%2%], obj_id %3%") % new_obj_id % new_instance_id % newObj->id().id; } } } @@ -3251,10 +3251,10 @@ void PartPlate::duplicate_all_instance(unsigned int dup_count, bool need_skip, s while (skip_objects.find(instance->loaded_id) != skip_objects.end()) { instance->loaded_id ++; - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": duplicated id %1% with skip, try new one %2%") %instance->id().id % instance->loaded_id; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": duplicated id %1% with skip, try new one %2%") %instance->id().id % instance->loaded_id; } } - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": set obj %1% instance %2%'s loaded_id to its id %3%, name %4%") % obj_id %instance_id %instance->loaded_id % object->name; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": set obj %1% instance %2%'s loaded_id to its id %3%, name %4%") % obj_id %instance_id %instance->loaded_id % object->name; } } } @@ -3576,7 +3576,7 @@ bool PartPlate::set_shape(const Pointfs& shape, const Pointfs& exclude_areas, co if ((m_shape == new_shape)&&(m_exclude_area == new_exclude_areas) &&(m_height_to_lid == height_to_lid)&&(m_height_to_rod == height_to_rod)) { - BOOST_LOG_TRIVIAL(warning) << "PartPlate same shape, skip directly"; + BOOST_LOG_TRIVIAL(info) << "PartPlate same shape, skip directly"; return false; } @@ -3809,7 +3809,7 @@ void PartPlate::update_states() } } - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1% , m_ready_for_slice changes to %2%") % m_plate_index %m_ready_for_slice; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1% , m_ready_for_slice changes to %2%") % m_plate_index %m_ready_for_slice; return; } @@ -3817,7 +3817,7 @@ void PartPlate::update_states() //invalid sliced result void PartPlate::update_slice_result_valid_state(bool valid) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1% , update slice result from %2% to %3%") % m_plate_index %m_slice_result_valid %valid; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1% , update slice result from %2% to %3%") % m_plate_index %m_slice_result_valid %valid; m_slice_result_valid = valid; if (valid) m_slice_percent = 100.0f; @@ -3906,7 +3906,7 @@ int PartPlate::load_gcode_from_file(const std::string& filename) update_slice_result_valid_state(true); - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": found valid gcode file %1%") % filename.c_str(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": found valid gcode file %1%") % filename.c_str(); } else { BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": can not find gcode file %1%") % filename.c_str(); @@ -4237,7 +4237,7 @@ PartPlateList::PartPlateList(int width, int depth, int height, Plater* platerObj :m_plate_width(width), m_plate_depth(depth), m_plate_height(height), m_plater(platerObj), m_model(modelObj), printer_technology(tech), unprintable_plate(this, Vec3d(0.0 + width * (1. + LOGICAL_PART_PLATE_GAP), 0.0, 0.0), width, depth, height, platerObj, modelObj, false, tech) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":plate_width %1%, plate_depth %2%, plate_height %3%") % width % depth % height; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":plate_width %1%, plate_depth %2%, plate_height %3%") % width % depth % height; init(); } @@ -4658,8 +4658,8 @@ void PartPlateList::reset_size(int width, int depth, int height, bool reload_obj { Vec3d origin1, origin2; - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":before size: plate_width %1%, plate_depth %2%, plate_height %3%") % m_plate_width % m_plate_depth % m_plate_height; - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":after size: plate_width %1%, plate_depth %2%, plate_height %3%") % width % depth % height; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":before size: plate_width %1%, plate_depth %2%, plate_height %3%") % m_plate_width % m_plate_depth % m_plate_height; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":after size: plate_width %1%, plate_depth %2%, plate_height %3%") % width % depth % height; if ((m_plate_width != width) || (m_plate_depth != depth) || (m_plate_height != height)) { m_plate_width = width; @@ -4875,7 +4875,7 @@ int PartPlateList::duplicate_plate(int index) // go over the instances and pair with the object for (size_t new_instance_id = 0; new_instance_id < object_copy->instances.size(); new_instance_id++){ new_plate->obj_to_instance_set.emplace(std::pair(new_obj_id, new_instance_id)); - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": duplicate object into plate: index_pair [%1%,%2%], obj_id %3%") % new_obj_id % new_instance_id % object_copy->id().id; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": duplicate object into plate: index_pair [%1%,%2%], obj_id %3%") % new_obj_id % new_instance_id % object_copy->id().id; } } new_plate->translate_all_instance(plate_to_plate_offset); @@ -5238,7 +5238,7 @@ void PartPlateList::update_plate_cols() m_plate_count = m_plate_list.size(); m_plate_cols = compute_colum_count(m_plate_count); - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":m_plate_count %1%, m_plate_cols change to %2%") % m_plate_count % m_plate_cols; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":m_plate_count %1%, m_plate_cols change to %2%") % m_plate_count % m_plate_cols; return; } @@ -5319,7 +5319,7 @@ int PartPlateList::lock_plate(int index, bool state) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(":can not get plate for index %1%, size %2%") % index % m_plate_list.size(); return -1; } - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":lock plate %1%, to state %2%") % index % state; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":lock plate %1%, to state %2%") % index % state; plate->lock(state); @@ -6237,7 +6237,7 @@ Print& PartPlateList::get_current_fff_print() const Print* print; current_plate = m_plate_list[m_current_plate]; - //BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":m_current_plate %1%, current_plate %2%") % m_current_plate % current_plate; + //BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":m_current_plate %1%, current_plate %2%") % m_current_plate % current_plate; assert(current_plate != NULL); current_plate->get_print((PrintBase **)&print, nullptr, nullptr); @@ -6251,7 +6251,7 @@ GCodeProcessorResult* PartPlateList::get_current_slice_result() const PartPlate* current_plate; current_plate = m_plate_list[m_current_plate]; - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":m_current_plate %1%, current_plate %2%") % m_current_plate % current_plate; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":m_current_plate %1%, current_plate %2%") % m_current_plate % current_plate; assert(current_plate != NULL); return current_plate->get_slice_result(); @@ -6456,7 +6456,7 @@ int PartPlateList::rebuild_plates_after_deserialize(std::vector& previous_ //not used /*if (m_plate_width == 0) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": jump to the first init state, need to re-set size!"); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": jump to the first init state, need to re-set size!"); Vec3d max = m_plater->get_bed().get_bounding_box(false).max; Vec3d min = m_plater->get_bed().get_bounding_box(false).min; double z = m_plater->config()->opt_float("printable_height"); @@ -6470,7 +6470,7 @@ int PartPlateList::rebuild_plates_after_arrangement(bool recycle_plates, bool ex { int ret = 0; - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":before rebuild, plates count %1%, recycle_plates %2%") % m_plate_list.size() % recycle_plates; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":before rebuild, plates count %1%, recycle_plates %2%") % m_plate_list.size() % recycle_plates; // sort by arrange_order std::sort(m_model->objects.begin(), m_model->objects.end(), [](auto a, auto b) {return a->instances[0]->arrange_order < b->instances[0]->arrange_order; }); @@ -6487,7 +6487,7 @@ int PartPlateList::rebuild_plates_after_arrangement(bool recycle_plates, bool ex || !m_plate_list[i]->has_printable_instances()) { //delete it - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":delete plate %1% for empty") % i; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":delete plate %1% for empty") % i; delete_plate(i); } else if (m_plate_list[i]->is_locked()) { @@ -6507,7 +6507,7 @@ int PartPlateList::rebuild_plates_after_arrangement(bool recycle_plates, bool ex } #endif - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":after rebuild, plates count %1%") % m_plate_list.size(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":after rebuild, plates count %1%") % m_plate_list.size(); return ret; } @@ -6525,10 +6525,10 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w plate_data_item->locked = m_plate_list[i]->m_locked; plate_data_item->plate_index = m_plate_list[i]->m_plate_index; plate_data_item->plate_name = m_plate_list[i]->get_plate_name(); - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1% before load, width %2%, height %3%, size %4%!") + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1% before load, width %2%, height %3%, size %4%!") %(i+1) %m_plate_list[i]->thumbnail_data.width %m_plate_list[i]->thumbnail_data.height %m_plate_list[i]->thumbnail_data.pixels.size(); plate_data_item->plate_thumbnail.load_from(m_plate_list[i]->thumbnail_data); - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1% after load, width %2%, height %3%, size %4%!") + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1% after load, width %2%, height %3%, size %4%!") %(i+1) %plate_data_item->plate_thumbnail.width %plate_data_item->plate_thumbnail.height %plate_data_item->plate_thumbnail.pixels.size(); plate_data_item->config.apply(*m_plate_list[i]->config()); @@ -6545,7 +6545,7 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w plate_data_item->objects_and_instances.emplace_back(it->first, it->second); } - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ <m_gcode_result->filename % with_slice_info %m_plate_list[i]->is_slice_result_valid()%plate_data_item->objects_and_instances.size(); if (with_slice_info) { @@ -6578,12 +6578,12 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w } plate_data_item->is_support_used = print->is_support_used(); } else { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("print is null!"); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("print is null!"); } //parse filament info plate_data_item->parse_filament_info(m_plate_list[i]->get_slice_result()); } else { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << "slice result = " << m_plate_list[i]->get_slice_result() + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "slice result = " << m_plate_list[i]->get_slice_result() << ", result valid = " << m_plate_list[i]->is_slice_result_valid(); } } @@ -6591,7 +6591,7 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w plate_data_list.push_back(plate_data_item); } - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":stored %1% plates!") % m_plate_list.size(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":stored %1% plates!") % m_plate_list.size(); return ret; } @@ -6617,7 +6617,7 @@ int PartPlateList::load_from_3mf_structure(PlateDataPtrs& plate_data_list, int f { BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":plate index %1% seems invalid, skip it")% plate_data_list[i]->plate_index; } - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1%, gcode_file %2%, is_sliced_valid %3%, toolpath_outside %4%, is_support_used %5% is_label_object_enabled %6%") + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1%, gcode_file %2%, is_sliced_valid %3%, toolpath_outside %4%, is_support_used %5% is_label_object_enabled %6%") %i %plate_data_list[i]->gcode_file %plate_data_list[i]->is_sliced_valid %plate_data_list[i]->toolpath_outside %plate_data_list[i]->is_support_used %plate_data_list[i]->is_label_object_enabled; //load object and instance from 3mf //just test for file correct or not, we will rebuild later @@ -6646,17 +6646,17 @@ int PartPlateList::load_from_3mf_structure(PlateDataPtrs& plate_data_list, int f gcode_result->warnings = plate_data_list[i]->warnings; gcode_result->filament_maps = plate_data_list[i]->filament_maps; if (m_plater && !plate_data_list[i]->thumbnail_file.empty()) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1%, load thumbnail from %2%.")%(i+1) %plate_data_list[i]->thumbnail_file; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1%, load thumbnail from %2%.")%(i+1) %plate_data_list[i]->thumbnail_file; if (boost::filesystem::exists(plate_data_list[i]->thumbnail_file)) { m_plate_list[index]->load_thumbnail_data(plate_data_list[i]->thumbnail_file, m_plate_list[index]->thumbnail_data); - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ <thumbnail_data.width %m_plate_list[index]->thumbnail_data.height %m_plate_list[index]->thumbnail_data.pixels.size(); } } if (m_plater && !plate_data_list[i]->no_light_thumbnail_file.empty()) { if (boost::filesystem::exists(plate_data_list[i]->no_light_thumbnail_file)) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1%, load no_light_thumbnail_file from %2%.")%(i+1) %plate_data_list[i]->no_light_thumbnail_file; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1%, load no_light_thumbnail_file from %2%.")%(i+1) %plate_data_list[i]->no_light_thumbnail_file; m_plate_list[index]->load_thumbnail_data(plate_data_list[i]->no_light_thumbnail_file, m_plate_list[index]->no_light_thumbnail_data); } } @@ -6669,13 +6669,13 @@ int PartPlateList::load_from_3mf_structure(PlateDataPtrs& plate_data_list, int f }*/ if (m_plater && !plate_data_list[i]->top_file.empty()) { if (boost::filesystem::exists(plate_data_list[i]->top_file)) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1%, load top_thumbnail from %2%.")%(i+1) %plate_data_list[i]->top_file; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1%, load top_thumbnail from %2%.")%(i+1) %plate_data_list[i]->top_file; m_plate_list[index]->load_thumbnail_data(plate_data_list[i]->top_file, m_plate_list[index]->top_thumbnail_data); } } if (m_plater && !plate_data_list[i]->pick_file.empty()) { if (boost::filesystem::exists(plate_data_list[i]->pick_file)) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1%, load pick_thumbnail from %2%.")%(i+1) %plate_data_list[i]->pick_file; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1%, load pick_thumbnail from %2%.")%(i+1) %plate_data_list[i]->pick_file; m_plate_list[index]->load_thumbnail_data(plate_data_list[i]->pick_file, m_plate_list[index]->pick_thumbnail_data); } } @@ -7111,7 +7111,7 @@ void PartPlateList::on_extruder_count_changed(int extruder_count) for (unsigned int i = 0; i < (unsigned int) m_plate_list.size(); ++i) { m_plate_list[i]->on_extruder_count_changed(extruder_count); } - BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: extruder_count=%2%")% __FUNCTION__ %extruder_count; + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: extruder_count=%2%")% __FUNCTION__ %extruder_count; } void PartPlateList::set_filament_count(int filament_count) @@ -7121,7 +7121,7 @@ void PartPlateList::set_filament_count(int filament_count) { m_plate_list[i]->set_filament_count(filament_count); } - BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: filament_count=%2%")% __FUNCTION__ %filament_count; + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: filament_count=%2%")% __FUNCTION__ %filament_count; } void PartPlateList::on_filament_added(int filament_count) @@ -7131,7 +7131,7 @@ void PartPlateList::on_filament_added(int filament_count) { m_plate_list[i]->on_filament_added(); } - BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: filament_count=%2%")% __FUNCTION__ %filament_count; + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: filament_count=%2%")% __FUNCTION__ %filament_count; } void PartPlateList::on_filament_deleted(int filament_count, int filament_id) @@ -7141,7 +7141,7 @@ void PartPlateList::on_filament_deleted(int filament_count, int filament_id) { m_plate_list[i]->on_filament_deleted(filament_count, filament_id); } - BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: filament_count=%2%, filament_id=%3%")% __FUNCTION__ %filament_count %filament_id; + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: filament_count=%2%, filament_id=%3%")% __FUNCTION__ %filament_count %filament_id; } }//end namespace GUI From c75231ec03871e1474cb27f503667f0f2b39e5ed Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Thu, 2 Apr 2026 03:06:48 -0400 Subject: [PATCH 013/434] fix: remove spurious blank lines in PrintConfig.hpp Co-Authored-By: Claude Sonnet 4.6 --- src/libslic3r/PrintConfig.hpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index a32c773517..6c70772cda 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -63,7 +63,6 @@ enum class WipeTowerType { Type2, }; - enum PrintHostType { htPrusaLink, htPrusaConnect, htOctoPrint, htDuet, htFlashAir, htAstroBox, htRepetier, htMKS, htESP3D, htCrealityPrint, htObico, htFlashforge, htSimplyPrint, htElegooLink }; @@ -497,7 +496,6 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(GCodeFlavor) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(FuzzySkinType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(FuzzySkinMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(WipeTowerType) - CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(NoiseType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(InfillPattern) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(IroningType) From a03d2cd566b3bd43c7505ec8ef1d4dd69ff8d46b Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Thu, 2 Apr 2026 03:11:17 -0400 Subject: [PATCH 014/434] fix: restore info log level in two commented-out PartPlate log lines Co-Authored-By: Claude Sonnet 4.6 --- src/slic3r/GUI/PartPlate.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 1fb2b80d5b..6f00eb69f3 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -3889,13 +3889,13 @@ int PartPlate::load_gcode_from_file(const std::string& filename) // BBS: use backup path to save temp gcode // auto path = get_tmp_gcode_path(); // if (boost::filesystem::exists(boost::filesystem::path(path))) { - // BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": file %1% exists, delete it firstly") % filename.c_str(); + // BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": file %1% exists, delete it firstly") % filename.c_str(); // boost::nowide::remove(path.c_str()); //} // std::error_code error = rename_file(filename, path); // if (error) { - // BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("Failed to rename the output G-code file from %1% to %2%, error code %3%") % filename.c_str() % path.c_str() % + // BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("Failed to rename the output G-code file from %1% to %2%, error code %3%") % filename.c_str() % path.c_str() % //error.message(); return -1; //} if (boost::filesystem::exists(filename)) { From 52974175186df0bd033a702510e76d3b64113c68 Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Thu, 2 Apr 2026 03:15:56 -0400 Subject: [PATCH 015/434] fix: remove phantom phys_row variable reference from iXex button layout comment Co-Authored-By: Claude Sonnet 4.6 --- src/slic3r/GUI/Tab.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index d19800a376..f8ab45eac2 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4592,9 +4592,8 @@ private: // flip_x (layout 1,3): col 0 is on the right (max-X) instead of left // flip_y (layout 2,3): row 0 is at the rear (max-Y) instead of front // Display iterates: row from n_rows-1 down to 0 (rear→front = top→bottom). - // phys_row = row (loop var maps directly to physical bed Y index). - // tool raw index: raw_row = flip_y ? (n_rows-1-row) : row - // raw_col = flip_x ? (n_cols-1-col) : col + // raw_row = flip_y ? (n_rows-1-row) : row + // raw_col = flip_x ? (n_cols-1-col) : col bool flip_x = (m_layout == 1 || m_layout == 3); bool flip_y = (m_layout == 2 || m_layout == 3); for (int row = m_n_rows - 1; row >= 0; --row) { From f4808ae7babe590d2bbc3dff47ac3d4bb6a3dafe Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Thu, 2 Apr 2026 13:38:19 -0400 Subject: [PATCH 016/434] fix: avoid capturing C array in lambda for ixex_tool_layout combo Arrays cannot be captured by value in C++ lambdas; replaced with a static local declared inside the event handler. Co-Authored-By: Claude Sonnet 4.6 --- src/slic3r/GUI/Tab.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index f8ab45eac2..9845867735 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4983,9 +4983,10 @@ void TabPrinter::build_fff() if (auto* o = m_config->option("ixex_tool_layout")) cur = o->value; for (int i = 0; i < 4; ++i) if (cur == values[i]) { m_ixex_layout_combo->SetSelection(i); break; } - m_ixex_layout_combo->Bind(wxEVT_COMBOBOX, [this, values](wxCommandEvent&) { + m_ixex_layout_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { + static const char* layout_vals[] = { "front-left", "front-right", "rear-left", "rear-right" }; int sel = m_ixex_layout_combo->GetSelection(); - std::string val = (sel >= 0 && sel < 4) ? values[sel] : "front-left"; + std::string val = (sel >= 0 && sel < 4) ? layout_vals[sel] : "front-left"; m_config->set_key_value("ixex_tool_layout", new ConfigOptionString(val)); on_value_change("ixex_tool_layout", val); if (m_ixex_modes_ctrl) From 185b7084d2a88a261443e833631455d630e5b5fe Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Fri, 3 Apr 2026 23:31:21 -0400 Subject: [PATCH 017/434] fix: null m_ixex_mode_combo in TabPrint::clear_pages() to prevent dangling pointer crash Same class of bug as the TabPrinter fix: clear_pages() destroys all page widgets but didn't null m_ixex_mode_combo, causing a SIGSEGV in refresh_ixex_mode_combo() when load_current_preset() ran after a page rebuild (e.g. triggered by Plater::reset()). Co-Authored-By: Claude Sonnet 4.6 --- src/slic3r/GUI/Tab.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 9845867735..dd1d6d84f9 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -2903,6 +2903,7 @@ void TabPrint::clear_pages() m_recommended_thin_wall_thickness_description_line = nullptr; m_top_bottom_shell_thickness_explanation = nullptr; + m_ixex_mode_combo = nullptr; } //BBS: GUI refactor From a3cac6c139fbb35a3eccc62dda604428864dc82f Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Sun, 5 Apr 2026 13:18:32 -0400 Subject: [PATCH 018/434] fix: remove iXex zone outline borders to eliminate aliasing flicker The thin rectangular outlines around copy/mirror zones caused visible aliasing and appeared to flash during interaction. Removed the border GLModel, its build code in calc_ixex_zones(), the render block, and the unused border field from IXexTheme. Co-Authored-By: Claude Sonnet 4.6 --- src/slic3r/GUI/PartPlate.cpp | 32 -------------------------------- src/slic3r/GUI/PartPlate.hpp | 3 +-- 2 files changed, 1 insertion(+), 34 deletions(-) diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 6f00eb69f3..09db0bb072 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -460,7 +460,6 @@ void PartPlate::calc_ixex_zones() { m_ixex_copy_zones.clear(); m_ixex_mirror_zones.clear(); - m_ixex_zone_borders.reset(); // Clear blocking vectors at the top so early returns don't leave stale data. m_ixex_secondary_zone_boxes.clear(); m_ixex_collision_zones.clear(); @@ -638,26 +637,6 @@ void PartPlate::calc_ixex_zones() push_expanded_fills(m_ixex_copy_zones, copy_cells); push_expanded_fills(m_ixex_mirror_zones, mirror_cells); - // White borders: one outline per expanded box of each active secondary type. - auto build_rect_lines = [](const std::vector& boxes) -> Lines { - Lines lines; - for (const auto& b : boxes) { - lines.emplace_back(Point(scale_(b.x0), scale_(b.y0)), Point(scale_(b.x1), scale_(b.y0))); - lines.emplace_back(Point(scale_(b.x1), scale_(b.y0)), Point(scale_(b.x1), scale_(b.y1))); - lines.emplace_back(Point(scale_(b.x1), scale_(b.y1)), Point(scale_(b.x0), scale_(b.y1))); - lines.emplace_back(Point(scale_(b.x0), scale_(b.y1)), Point(scale_(b.x0), scale_(b.y0))); - } - return lines; - }; - - // Combine copy+mirror for borders (each expands independently then we collect all lines) - Lines all_border_lines = build_rect_lines(make_boxes(copy_cells)); - auto mirror_border_lines = build_rect_lines(make_boxes(mirror_cells)); - all_border_lines.insert(all_border_lines.end(), mirror_border_lines.begin(), mirror_border_lines.end()); - - if (!all_border_lines.empty()) - if (!init_model_from_lines(m_ixex_zone_borders, all_border_lines, GROUND_Z_GRIDLINE)) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": unable to create iXex zone border lines\n"; // --- Secondary zone blocking --- // Collect the expanded bounding boxes for all secondary (copy+mirror) zones. @@ -857,7 +836,6 @@ void PartPlate::render_ixex_zones(bool force_default_color) struct IXexTheme { ColorRGBA copy; // secondary copy zone fill ColorRGBA mirror; // secondary mirror zone fill - ColorRGBA border; // zone outline ColorRGBA danger; // blocking collision strip ColorRGBA margin; // advisory safety margin }; @@ -866,7 +844,6 @@ void PartPlate::render_ixex_zones(bool force_default_color) static const IXexTheme k_standard = { { 0.337f, 0.706f, 0.914f, 0.45f }, // copy — sky blue #56B4E9 { 0.902f, 0.624f, 0.000f, 0.45f }, // mirror — orange #E69F00 - { 1.000f, 1.000f, 1.000f, 0.90f }, // border — white { 0.850f, 0.100f, 0.100f, 0.55f }, // danger — red { 0.300f, 0.900f, 0.200f, 0.40f }, // margin — lime green }; @@ -875,7 +852,6 @@ void PartPlate::render_ixex_zones(bool force_default_color) static const IXexTheme k_deuteranopia = { { 0.000f, 0.447f, 0.698f, 0.50f }, // copy — blue #0072B2 { 0.941f, 0.894f, 0.259f, 0.50f }, // mirror — yellow #F0E442 - { 1.000f, 1.000f, 1.000f, 0.90f }, // border — white { 0.200f, 0.100f, 0.800f, 0.65f }, // danger — blue-violet (red not visible) { 0.800f, 0.475f, 0.655f, 0.45f }, // margin — reddish purple #CC79A7 }; @@ -884,7 +860,6 @@ void PartPlate::render_ixex_zones(bool force_default_color) static const IXexTheme k_tritanopia = { { 0.835f, 0.369f, 0.000f, 0.50f }, // copy — vermilion #D55E00 { 0.800f, 0.475f, 0.655f, 0.50f }, // mirror — reddish purple #CC79A7 - { 1.000f, 1.000f, 1.000f, 0.90f }, // border — white { 0.850f, 0.100f, 0.100f, 0.55f }, // danger — red (visible to tritanopes) { 0.000f, 0.700f, 0.600f, 0.45f }, // margin — teal }; @@ -892,7 +867,6 @@ void PartPlate::render_ixex_zones(bool force_default_color) static const IXexTheme k_high_contrast = { { 0.000f, 0.600f, 1.000f, 0.65f }, // copy — vivid blue { 1.000f, 0.600f, 0.000f, 0.65f }, // mirror — vivid orange - { 1.000f, 1.000f, 0.000f, 1.00f }, // border — bright yellow { 1.000f, 0.000f, 0.000f, 0.75f }, // danger — full red { 0.000f, 1.000f, 0.200f, 0.60f }, // margin — bright green }; @@ -920,12 +894,6 @@ void PartPlate::render_ixex_zones(bool force_default_color) glsafe(::glDepthMask(GL_TRUE)); - if (m_ixex_zone_borders.is_initialized()) { - m_ixex_zone_borders.set_color(theme->border); - glsafe(::glLineWidth(2.0f)); - m_ixex_zone_borders.render(); - glsafe(::glLineWidth(1.0f)); - } if (!m_ixex_collision_overlay.empty()) { glsafe(::glDepthMask(GL_FALSE)); diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index a82d315328..2bb575afca 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -137,8 +137,7 @@ private: GLModel m_gridlines_bolder; std::vector m_ixex_copy_zones; // blue tint (Wong #56B4E9) std::vector m_ixex_mirror_zones; // orange tint (Wong #E69F00) - GLModel m_ixex_zone_borders; // white outlines around active secondary zones - std::vector m_ixex_secondary_zone_boxes; // secondary (copy/mirror) zone rects — objects blocked here +std::vector m_ixex_secondary_zone_boxes; // secondary (copy/mirror) zone rects — objects blocked here std::vector m_ixex_collision_zones; // carriage danger strips inside primary zone (mm) std::vector m_ixex_collision_overlay; // red-orange rendered fill for danger strips std::vector m_ixex_margin_overlay; // amber advisory bands just inside collision strips From b9f7148080a5d7540d4b7ae691025a7210d5b282 Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Mon, 6 Apr 2026 00:52:37 -0400 Subject: [PATCH 019/434] fix: constrain auto-arrange to iXex primary zone when parallel mode is active Adds std::optional m_ixex_primary_zone_box to PartPlate, populated by calc_ixex_zones() alongside the existing secondary/collision zone geometry. The new ixex_primary_zone() getter calls ensure_ixex_zones() so callers always get fresh data. ArrangeJob::process() replaces the full-bed bedpts with the primary zone corners when the getter returns a value, so auto-arrange no longer drops objects into the bed center. Co-Authored-By: Claude Sonnet 4.6 --- src/slic3r/GUI/Jobs/ArrangeJob.cpp | 13 +++++++++++++ src/slic3r/GUI/PartPlate.cpp | 2 ++ src/slic3r/GUI/PartPlate.hpp | 7 ++++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.cpp b/src/slic3r/GUI/Jobs/ArrangeJob.cpp index 2e43f0271e..dded97556a 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.cpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.cpp @@ -540,6 +540,19 @@ void ArrangeJob::process(Ctl &ctl) Points bedpts = get_shrink_bedpts(m_plater->config(),params); + // When an iXex parallel mode is active, constrain auto-arrange to the primary zone only. + if (PartPlate* curr_plate = partplate_list.get_curr_plate()) { + if (auto pz = curr_plate->ixex_primary_zone()) { + BoundingBox scaled_pz = scaled(*pz); + bedpts = { + { scaled_pz.min.x(), scaled_pz.min.y() }, + { scaled_pz.max.x(), scaled_pz.min.y() }, + { scaled_pz.max.x(), scaled_pz.max.y() }, + { scaled_pz.min.x(), scaled_pz.max.y() }, + }; + } + } + bool enable_wrapping = global_config.option("enable_wrapping_detection")->value; partplate_list.preprocess_exclude_areas(params.excluded_regions, enable_wrapping, 1, scale_(1)); diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 09db0bb072..f48e12cb76 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -465,6 +465,7 @@ void PartPlate::calc_ixex_zones() m_ixex_collision_zones.clear(); m_ixex_collision_overlay.clear(); m_ixex_margin_overlay.clear(); + m_ixex_primary_zone_box = std::nullopt; if (!wxGetApp().preset_bundle) return; @@ -689,6 +690,7 @@ void PartPlate::calc_ixex_zones() double pz_x1 = has_col_sep ? x_min + (pri_col + 1) * zone_w : x_max; double pz_y0 = has_row_sep ? y_min + pri_row * zone_h : y_min; double pz_y1 = has_row_sep ? y_min + (pri_row + 1) * zone_h : y_max; + m_ixex_primary_zone_box = BoundingBoxf(Vec2d(pz_x0, pz_y0), Vec2d(pz_x1, pz_y1)); // Helper: build a BoundingBoxf3 strip, clip to bed, add to members auto add_strip = [&](double sx0, double sx1, double sy0, double sy1) { diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index 2bb575afca..23d63da7e0 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -137,10 +138,11 @@ private: GLModel m_gridlines_bolder; std::vector m_ixex_copy_zones; // blue tint (Wong #56B4E9) std::vector m_ixex_mirror_zones; // orange tint (Wong #E69F00) -std::vector m_ixex_secondary_zone_boxes; // secondary (copy/mirror) zone rects — objects blocked here + std::vector m_ixex_secondary_zone_boxes; // secondary (copy/mirror) zone rects — objects blocked here std::vector m_ixex_collision_zones; // carriage danger strips inside primary zone (mm) std::vector m_ixex_collision_overlay; // red-orange rendered fill for danger strips std::vector m_ixex_margin_overlay; // amber advisory bands just inside collision strips + std::optional m_ixex_primary_zone_box; // primary zone extents in mm; empty when iXex is off/primary-only std::string m_ixex_zones_mode_cache{ "\x01" }; // cached mode+topology key; sentinel = unbuilt GLModel m_height_limit_common; GLModel m_height_limit_bottom; @@ -444,6 +446,9 @@ public: } // Returns true if any instance on this plate overlaps an iXex secondary or collision zone. bool has_ixex_placement_violations(); + // Returns the primary-zone bounding box in mm when an iXex parallel mode is active. + // Empty (nullopt) when iXex is off or the mode is "primary" (full-bed). + std::optional ixex_primary_zone() { ensure_ixex_zones(); return m_ixex_primary_zone_box; } void update_slice_ready_status(bool ready_slice) { m_ready_for_slice = ready_slice; From 1cacc6d02d88bd991690f5482e0e17fcaf7d316d Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Mon, 6 Apr 2026 01:02:44 -0400 Subject: [PATCH 020/434] fix: exclude iXex collision strips from auto-arrange placement Auto-arrange now injects the carriage collision zones as hard excluded regions (is_virt_object) when an iXex parallel mode is active, using the same BoundingBoxf3 data already stored for rendering and violation checks. Objects will no longer be placed in the mirror-edge danger strips. Co-Authored-By: Claude Sonnet 4.6 --- src/slic3r/GUI/Jobs/ArrangeJob.cpp | 20 +++++++++++++++++++- src/slic3r/GUI/PartPlate.hpp | 3 +++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.cpp b/src/slic3r/GUI/Jobs/ArrangeJob.cpp index dded97556a..16e5531a74 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.cpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.cpp @@ -540,7 +540,8 @@ void ArrangeJob::process(Ctl &ctl) Points bedpts = get_shrink_bedpts(m_plater->config(),params); - // When an iXex parallel mode is active, constrain auto-arrange to the primary zone only. + // When an iXex parallel mode is active, constrain auto-arrange to the primary zone only + // and treat carriage collision strips as hard excluded regions. if (PartPlate* curr_plate = partplate_list.get_curr_plate()) { if (auto pz = curr_plate->ixex_primary_zone()) { BoundingBox scaled_pz = scaled(*pz); @@ -550,6 +551,23 @@ void ArrangeJob::process(Ctl &ctl) { scaled_pz.max.x(), scaled_pz.max.y() }, { scaled_pz.min.x(), scaled_pz.max.y() }, }; + for (const BoundingBoxf3& cz : curr_plate->ixex_collision_zones()) { + Polygon poly({ + { scaled(cz.min.x()), scaled(cz.min.y()) }, + { scaled(cz.max.x()), scaled(cz.min.y()) }, + { scaled(cz.max.x()), scaled(cz.max.y()) }, + { scaled(cz.min.x()), scaled(cz.max.y()) }, + }); + arrangement::ArrangePolygon ap; + ap.poly.contour = poly; + ap.translation = Vec2crd(0, 0); + ap.rotation = 0.0; + ap.is_virt_object = true; + ap.bed_idx = current_plate_index; + ap.height = 1; + ap.name = "IXexCollisionZone"; + params.excluded_regions.emplace_back(std::move(ap)); + } } } diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index 23d63da7e0..4954e6f3b5 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -449,6 +449,9 @@ public: // Returns the primary-zone bounding box in mm when an iXex parallel mode is active. // Empty (nullopt) when iXex is off or the mode is "primary" (full-bed). std::optional ixex_primary_zone() { ensure_ixex_zones(); return m_ixex_primary_zone_box; } + // Returns carriage collision strips (mm) for the current iXex mode. + // Always call ixex_primary_zone() first to ensure the cache is warm. + const std::vector& ixex_collision_zones() const { return m_ixex_collision_zones; } void update_slice_ready_status(bool ready_slice) { m_ready_for_slice = ready_slice; From a9ad17cea2b9f8331716d4e30f24890c7f941044 Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Mon, 6 Apr 2026 01:36:33 -0400 Subject: [PATCH 021/434] fix: route iXex collision zones through m_unselected for effective exclusion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit params.excluded_regions flows into libnest2d PlacementConfig.m_excluded_regions which is declared but never consumed by the NFP placer — the field is dead code. m_unselected is the working path: it becomes fixeditems that are preloaded as physical fixed obstacles in the NFP computation, same as the wipe tower. Co-Authored-By: Claude Sonnet 4.6 --- src/slic3r/GUI/Jobs/ArrangeJob.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.cpp b/src/slic3r/GUI/Jobs/ArrangeJob.cpp index 16e5531a74..9d1d778504 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.cpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.cpp @@ -566,7 +566,7 @@ void ArrangeJob::process(Ctl &ctl) ap.bed_idx = current_plate_index; ap.height = 1; ap.name = "IXexCollisionZone"; - params.excluded_regions.emplace_back(std::move(ap)); + m_unselected.emplace_back(std::move(ap)); } } } From 167211c85c30d2bc6d23651756c5f012b15d734d Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Mon, 6 Apr 2026 14:53:18 -0400 Subject: [PATCH 022/434] =?UTF-8?q?refactor:=20rename=20ixex=5Fcarriage=5F?= =?UTF-8?q?width=20=E2=86=92=20ixex=5Fnozzle=5Fclearance;=20fix=20strip=20?= =?UTF-8?q?math=20and=20carriage=20box=20visualization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Config key rename (breaking for saved profiles — call out in PR): ixex_carriage_width_x/y → ixex_nozzle_clearance_x/y Labels updated to "Nozzle Clearance X/Y" with consistent tooltips describing the measurement as nozzle-to-collision-side-edge distance. Strip math fix: Previously halved the clearance value (× 0.5) under the assumption the nozzle was centered in the carriage. The measurement is now the literal nozzle-to-edge distance, so the × 0.5 factor is removed. The collision strip width now equals the configured value directly. Carriage box visualization (GCodeViewer): - Add per-carriage box_offset_x/y so the nozzle marker sits at the physically correct edge of the footprint box rather than centered. - X: zone-based by default (nozzle at inner edge facing bed center). Copy secondaries inherit the primary's X orientation (same movement direction). Mirror secondaries use the collision-side edge. - Y: always row-based regardless of copy/mirror mode. Gantry is always behind the nozzle (high-Y); front-row primaries with a back-row secondary override to place nozzle at the low-Y edge. - Fix stale mesh bug: GLModel::init_from() is a no-op when already initialized. Call reset() before init_from() so mesh rebuilds correctly when nozzle clearance values change in config. - Remove m_ixex_toolhead_box_dims (was the now-unnecessary cache key). Co-Authored-By: Claude Sonnet 4.6 --- src/libslic3r/Preset.cpp | 2 +- src/libslic3r/PrintConfig.cpp | 12 +++---- src/libslic3r/PrintConfig.hpp | 4 +-- src/slic3r/GUI/GCodeViewer.cpp | 63 +++++++++++++++++++++++++--------- src/slic3r/GUI/GCodeViewer.hpp | 1 - src/slic3r/GUI/PartPlate.cpp | 30 ++++++++-------- src/slic3r/GUI/Tab.cpp | 4 +-- 7 files changed, 73 insertions(+), 43 deletions(-) diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index b8df0aab53..4625a631f4 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1042,7 +1042,7 @@ static std::vector s_Preset_printer_options { "bed_temperature_formula", "nozzle_flush_dataset", // iXex (independent X extruder) — printer capability and user-defined modes "is_ixex", "ixex_gantry_count", "ixex_tools_per_gantry", "ixex_tool_layout", - "ixex_carriage_width_x", "ixex_carriage_width_y", "ixex_carriage_margin", "ixex_viz_theme", + "ixex_nozzle_clearance_x", "ixex_nozzle_clearance_y", "ixex_carriage_margin", "ixex_viz_theme", "ixex_mode_names", "ixex_mode_active_tools", "ixex_mode_gcodes" }; diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 5c75c87622..cf4fba50d8 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -5530,17 +5530,17 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionString("front-left")); - def = this->add("ixex_carriage_width_x", coFloat); - def->label = L("Carriage Width X"); - def->tooltip = L("Toolhead footprint in the X direction (mm). Used to calculate usable bed zones and detect carriage collisions."); + def = this->add("ixex_nozzle_clearance_x", coFloat); + def->label = L("Nozzle Clearance X"); + def->tooltip = L("Distance (mm) from the nozzle to the collision-side carriage edge in X. Used to calculate the width of the collision exclusion strip at each X boundary."); def->sidetext = L("mm"); def->min = 0; def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(30.0)); - def = this->add("ixex_carriage_width_y", coFloat); - def->label = L("Carriage Width Y"); - def->tooltip = L("Toolhead footprint in the Y direction (mm). Used for collision detection."); + def = this->add("ixex_nozzle_clearance_y", coFloat); + def->label = L("Nozzle Clearance Y"); + def->tooltip = L("Distance (mm) from the nozzle to the collision-side carriage edge in Y. Used to calculate the width of the collision exclusion strip at each Y boundary."); def->sidetext = L("mm"); def->min = 0; def->mode = comAdvanced; diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 6c70772cda..1e89eba383 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1341,8 +1341,8 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionInt, ixex_tools_per_gantry)) ((ConfigOptionInt, ixex_primary_col)) ((ConfigOptionInt, ixex_primary_row)) - ((ConfigOptionFloat, ixex_carriage_width_x)) - ((ConfigOptionFloat, ixex_carriage_width_y)) + ((ConfigOptionFloat, ixex_nozzle_clearance_x)) + ((ConfigOptionFloat, ixex_nozzle_clearance_y)) ((ConfigOptionStrings, ixex_mode_names)) ((ConfigOptionStrings, ixex_mode_active_tools)) ((ConfigOptionStrings, ixex_mode_gcodes)) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 0ea8749b33..b4e2a9627c 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1521,7 +1521,7 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) { 0.835f, 0.369f, 0.000f, 0.65f }, // secondary 2 — vermilion (#D55E00) { 0.800f, 0.475f, 0.655f, 0.65f }, // secondary 3 — reddish purple (#CC79A7) }}; - struct CarriageDraw { Vec3f pos; ColorRGBA color; }; + struct CarriageDraw { Vec3f pos; ColorRGBA color; float box_offset_x = 0.0f; float box_offset_y = 0.0f; }; std::vector carriage_box_draws; float ixex_box_wx = 0.0f, ixex_box_wy = 0.0f; { @@ -1545,8 +1545,8 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) auto* active_tools_opt = printer_cfg.opt("ixex_mode_active_tools"); auto* tpg_opt = printer_cfg.opt("ixex_tools_per_gantry"); - auto* wx_opt = printer_cfg.opt("ixex_carriage_width_x"); - auto* wy_opt = printer_cfg.opt("ixex_carriage_width_y"); + auto* wx_opt = printer_cfg.opt("ixex_nozzle_clearance_x"); + auto* wy_opt = printer_cfg.opt("ixex_nozzle_clearance_y"); int tools_per_gantry = tpg_opt ? std::max(1, tpg_opt->value) : 1; ixex_box_wx = wx_opt ? (float)wx_opt->value : 30.0f; ixex_box_wy = wy_opt ? (float)wy_opt->value : 30.0f; @@ -1665,8 +1665,19 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) } } - // Primary carriage box - carriage_box_draws.push_back({ prim_pos, s_carriage_colors[0] }); + // Primary carriage box: nozzle sits at the collision-side edge. + // Default to centered; override once we know which side the secondaries are on. + float pri_box_offset_x = (pri_phys_col == 0) ? 0.0f : -ixex_box_wx; // nozzle at inner edge (facing center of bed) + float pri_box_offset_y = -ixex_box_wy; // nozzle at high-Y edge by default (gantry always behind nozzle); override if secondary is behind + for (int i = 0; i < sec_count; ++i) { + int sc = phys_col_of(sec_tool_ids[i]); + int sr = phys_row_of(sec_tool_ids[i]); + if (sc > pri_phys_col) { pri_box_offset_x = 0.0f; } + else if (sc < pri_phys_col) { pri_box_offset_x = -ixex_box_wx; } + if (sr > pri_phys_row) { pri_box_offset_y = 0.0f; } + else if (sr < pri_phys_row) { pri_box_offset_y = -ixex_box_wy; } + } + carriage_box_draws.push_back({ prim_pos, s_carriage_colors[0], pri_box_offset_x, pri_box_offset_y }); const float pri_zone_x = bed_x_min + (float)pri_phys_col * strip_width; const float pri_zone_y = bed_y_min + (float)pri_phys_row * row_strip_height; @@ -1704,8 +1715,26 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) Vec3f sec_pos{ sec_x, sec_y, prim_pos.z() }; m_sequential_view.m_ixex_secondary_markers[i].set_world_position(sec_pos); m_sequential_view.m_ixex_secondary_markers[i].set_z_offset(m_z_offset + 0.5f); + // X: copy tools move in sync with primary so they share the same + // X-facing orientation. Mirror tools approach from the opposite side + // so the nozzle sits at the collision-side edge. + float sec_box_offset_x; + if (sec_state == 2) { + sec_box_offset_x = pri_box_offset_x; + } else { + if (sec_phys_col > pri_phys_col) sec_box_offset_x = -ixex_box_wx; + else if (sec_phys_col < pri_phys_col) sec_box_offset_x = 0.0f; + else sec_box_offset_x = pri_box_offset_x; + } + // Y: always row-based — gantry is always behind nozzle regardless of + // copy/mirror (a back-row copy still has its gantry at the back). + float sec_box_offset_y; + if (sec_phys_row > pri_phys_row) sec_box_offset_y = -ixex_box_wy; + else if (sec_phys_row < pri_phys_row) sec_box_offset_y = 0.0f; + else sec_box_offset_y = -ixex_box_wy; carriage_box_draws.push_back({ - sec_pos, s_carriage_colors[(i + 1) % s_carriage_colors.size()] }); + sec_pos, s_carriage_colors[(i + 1) % s_carriage_colors.size()], + sec_box_offset_x, sec_box_offset_y }); } } } @@ -1719,17 +1748,19 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) m_sequential_view.render(!m_no_render_path, legend_height, &m_viewer, m_viewer.get_current_vertex().gcode_id, canvas_width, canvas_height - bottom_margin * m_scale, right_margin * m_scale, m_viewer.get_view_type()); // iXex: render toolhead footprint boxes for each active carriage. - // Each box is ixex_carriage_width_x × ixex_carriage_width_y, sitting above the nozzle tip. + // Each box is ixex_nozzle_clearance_x × ixex_nozzle_clearance_y, sitting above the nozzle tip. if (!carriage_box_draws.empty() && ixex_box_wx > 0.0f && ixex_box_wy > 0.0f) { - // Rebuild box mesh if dimensions changed - const Vec2f new_dims{ ixex_box_wx, ixex_box_wy }; - if (!m_ixex_toolhead_box.is_initialized() || new_dims != m_ixex_toolhead_box_dims) { - m_ixex_toolhead_box_dims = new_dims; - const float box_h = std::max(ixex_box_wx, ixex_box_wy); // height ~ largest horizontal dim + // Rebuild box mesh every frame — dimensions can change via config edit without a + // G-code reload, so dimension-based caching isn't safe. + // Mesh origin: nozzle at x=0, centered in Y, Z starts at nozzle tip level. + // Per-carriage box_offset_x shifts the mesh left or right so the nozzle lands + // at the correct (collision-side) edge. + { + const float box_h = std::max(ixex_box_wx, ixex_box_wy); indexed_triangle_set its = its_make_cube((double)ixex_box_wx, (double)ixex_box_wy, (double)box_h); - // Center in X and Y; Z starts at 0 (nozzle tip level) - for (auto& v : its.vertices) - v += Vec3f(-ixex_box_wx * 0.5f, -ixex_box_wy * 0.5f, 0.0f); + // No vertex pre-shifting — box_offset_x/y in the per-carriage transform + // positions the nozzle at the correct collision-side edge. + m_ixex_toolhead_box.reset(); m_ixex_toolhead_box.init_from(its); } @@ -1744,7 +1775,7 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) const Transform3d& view_matrix = camera.get_view_matrix(); for (const auto& draw : carriage_box_draws) { const Transform3d model_matrix = Geometry::translation_transform( - (draw.pos + Vec3f(0.0f, 0.0f, m_z_offset)).cast()); + (draw.pos + Vec3f(draw.box_offset_x, draw.box_offset_y, m_z_offset)).cast()); shader->set_uniform("view_model_matrix", view_matrix * model_matrix); const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 71880da962..ee3c1e1ec7 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -207,7 +207,6 @@ private: std::string m_marker_filename; // cached for lazy secondary marker init std::string m_ixex_last_mode; // detect mode changes for secondary marker rebuild GLModel m_ixex_toolhead_box; // shared box mesh for all carriage footprint overlays - Vec2f m_ixex_toolhead_box_dims{ 0.0f, 0.0f }; // (width_x, width_y) — rebuild when changed size_t m_extruders_count; std::vector m_filament_diameters; diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index f48e12cb76..16a87009a1 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -653,18 +653,18 @@ void PartPlate::calc_ixex_zones() // Only add strips at boundaries of the PRIMARY zone — objects are only placed in the // primary zone, so secondary-to-secondary boundaries have no relevance. // - // Strip width is carriage/2 on the primary side only: - // right X boundary: [bnd_x - carriage_w/2, bnd_x] - // left X boundary: [bnd_x, bnd_x + carriage_w/2] - // top Y boundary: [bnd_y - carriage_h/2, bnd_y] - // bottom Y boundary:[bnd_y, bnd_y + carriage_h/2] + // Strip width is the literal nozzle clearance value on the primary side only: + // right X boundary: [bnd_x - nozzle_clearance_x, bnd_x] + // left X boundary: [bnd_x, bnd_x + nozzle_clearance_x] + // top Y boundary: [bnd_y - nozzle_clearance_y, bnd_y] + // bottom Y boundary:[bnd_y, bnd_y + nozzle_clearance_y] // // Strip length matches the primary zone extent (same expansion logic as make_boxes): // !has_row_sep → full bed height; has_row_sep → primary row only // !has_col_sep → full bed width; has_col_sep → primary column only - auto* cw_opt = printer_cfg.option("ixex_carriage_width_x"); - auto* ch_opt = printer_cfg.option("ixex_carriage_width_y"); + auto* cw_opt = printer_cfg.option("ixex_nozzle_clearance_x"); + auto* ch_opt = printer_cfg.option("ixex_nozzle_clearance_y"); auto* mgn_opt = printer_cfg.option("ixex_carriage_margin"); double carriage_w = cw_opt ? cw_opt->value : 0.0; double carriage_h = ch_opt ? ch_opt->value : 0.0; @@ -729,36 +729,36 @@ void PartPlate::calc_ixex_zones() if (r == pri_row - 1 && c == pri_col) has_bottom_sec = true; } - // X-axis boundaries (vertical strips, width = carriage_w/2 on primary side) + // X-axis boundaries (vertical strips, width = nozzle_clearance_x on primary side) if (carriage_w > 0.0) { if (has_right_sec) { double bnd_x = x_min + (pri_col + 1) * zone_w; - double strip_inner = bnd_x - carriage_w * 0.5; + double strip_inner = bnd_x - carriage_w; add_strip(strip_inner, bnd_x, pz_y0, pz_y1); if (margin > 0.0) add_margin_fill(strip_inner - margin, strip_inner, pz_y0, pz_y1); } if (has_left_sec) { double bnd_x = x_min + pri_col * zone_w; - double strip_inner = bnd_x + carriage_w * 0.5; + double strip_inner = bnd_x + carriage_w; add_strip(bnd_x, strip_inner, pz_y0, pz_y1); if (margin > 0.0) add_margin_fill(strip_inner, strip_inner + margin, pz_y0, pz_y1); } } - // Y-axis boundaries (horizontal strips, width = carriage_h/2 on primary side) + // Y-axis boundaries (horizontal strips, width = nozzle_clearance_y on primary side) if (carriage_h > 0.0) { if (has_top_sec) { double bnd_y = y_min + (pri_row + 1) * zone_h; - double strip_inner = bnd_y - carriage_h * 0.5; + double strip_inner = bnd_y - carriage_h; add_strip(pz_x0, pz_x1, strip_inner, bnd_y); if (margin > 0.0) add_margin_fill(pz_x0, pz_x1, strip_inner - margin, strip_inner); } if (has_bottom_sec) { double bnd_y = y_min + pri_row * zone_h; - double strip_inner = bnd_y + carriage_h * 0.5; + double strip_inner = bnd_y + carriage_h; add_strip(pz_x0, pz_x1, bnd_y, strip_inner); if (margin > 0.0) add_margin_fill(pz_x0, pz_x1, strip_inner, strip_inner + margin); @@ -779,8 +779,8 @@ static std::string build_ixex_cache_key() auto* mode_opt = process_cfg.option("ixex_parallel_mode"); auto* n_col_opt = printer_cfg.option("ixex_tools_per_gantry"); auto* n_row_opt = printer_cfg.option("ixex_gantry_count"); - auto* cw_opt = printer_cfg.option("ixex_carriage_width_x"); - auto* ch_opt = printer_cfg.option("ixex_carriage_width_y"); + auto* cw_opt = printer_cfg.option("ixex_nozzle_clearance_x"); + auto* ch_opt = printer_cfg.option("ixex_nozzle_clearance_y"); auto* mgn_opt = printer_cfg.option("ixex_carriage_margin"); return (mode_opt ? mode_opt->value : "primary") + "|" + std::to_string(n_col_opt ? n_col_opt->value : 2) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index dd1d6d84f9..77283cfd8a 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -5002,8 +5002,8 @@ void TabPrinter::build_fff() }; optgroup->append_line(line); } - optgroup->append_single_option_line("ixex_carriage_width_x"); - optgroup->append_single_option_line("ixex_carriage_width_y"); + optgroup->append_single_option_line("ixex_nozzle_clearance_x"); + optgroup->append_single_option_line("ixex_nozzle_clearance_y"); optgroup->append_single_option_line("ixex_carriage_margin"); { From 3bc13e5cfd91f2949bca14466423214c6c1ce111 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Tue, 7 Apr 2026 10:37:34 +0800 Subject: [PATCH 023/434] 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 zcmV(|K+(U6P)Px#1ZP1_K>z@;j|==^1poj7(@;!QMMrQ9vvdt+}$iKJSZnH zA0H|;Fg+1etCmDI7LcNVk$F7PEA~Vdy73i zOgS+=h=-FG79oX*ie6rCV`zDdiHKK=o@{S_(9q1XsG{cG(V?THU}Jl!v9#UV)br7~ zL^UuWASP*JVIUqM5D*ncHZo{oU>6u292y-J78`42WD5)u5fd3EA|)6ZARHSbgo=(r zGB51y?1G4kB_t|VMnh;{UM(swEG8%y6BdSykniyDQ$s*)XlNW486p!S>FVizf`&0L zH!&?RP(VC~j+E!<=Zcb-8VwwQg@`F9F9-(=q^+>>^z%PHLVSRPWnEk~H9O32OGHCP zWLjF|<>X;kR?VAnkeQx2IzL-aPE1Wvp{lNomYh5;EK*ZeM@UOvQ&U=8VRUzUZ*X&y zouX`zrjSXGu(r9Xw7E<@J4-q?D;Fv^Gc|pMi@wFjSW8JrI5cu|c6WMy_W1aVkdj_t zWy{jkd3}Kx4-lB5rd3y6MreJJQ$K zacpdgLx+%4kF2w`(v4u7Oq!9EmV!Keq_4DqM}Ei4%(1(_W@v1kQ=Y$*acVkca+j)7 zEKQrApixIir(von8Wy0Xrkkg%wz|5yzreU~OJOQkz01+4glC^xp{u#TJt!ewZi;6y zU(l0fq_Vj&ARBE*ZHZ=3QcO+6c~h`!w9KP?ezCw=V|<%!RGW8SQZ-O@Q$c@RNVIra zyl6mpov&U&GPs6jmuH-GskzL8S!hc-gkXs|4=;C9dQUPaTR>d={{5|cwwraNzIMg8 zguz%HONYJ5!-C7ck;-Ftjzzr5{94aK0000zbW%=J009F1{QUkt^WW2~=54zyAEQvu zX$4zPjfOlFF6sw-+yk5Z84|(%4Z|b(`3&{xql-TAS1ZvTG7vpjC~|ZO&X2ML>I2o-MHS6m2$}!ODfN z%SF*AiYLb`*&ylqTFAM#y~!4xm4M|-$1K~WYHYq!IX$;-c8#(d=a-`IvXrsatGoXn z5X+^5d;#ysn8TDg7ez5&F5ovC@3cJpWY+G^&Xyh(eO$T}#o`7_wRYFKkrDZa8|;wF zKZ94Vb1RSYz-~Fis+w!Tg{!7TJ_@qLXgTeTJCja%7C~Gh(~G`G13cnrXN>p`-b`+=-S8iAE`$E z6R$>VWI@iCujsCxht>M=OEdb0ik;0@I9Ata1*zP*W=y;0w$bz?MmpBCx-o-Z-%)jS z6*MC2A0#s;Am?LRbpsv-^4Yo7atkXIML=ACw-#+I!R;O&d*HAs7ZiT~n%iiz8A)n+ z=)stw?ZYP!7^@={yuM;)w5R2>a4WiybpqnNC~laWG&P4<5}khhs}+ZF%0g`)@gdJn zfOO&trE=0pdHZx@_w@tSWuev00^YNXMDLeTbjVs7a6p_d7#n+Qtbj;x4O*|eZjM}; zi7*70+up@6=Rx)xKV?=bG0HLAr&G5;ik`g`Pgt_XM2*M> z)O`+PU7gLnln60s_AS0(ac=J<9zUK4re6mrg<{f1Qq-;HR)^`8=quj5+ac?a{}<2k zhC5mY8{-ZAwUpFysV8Cznl^)e-)${<&A5AIK7u}e^yty!i5Nj7;{ObgN~LL>s?!cx51A?Y@&&TFhA5UJFleS}YX4ws7q^&b8Y7Kwas*KU{Agk#CJ>$w zpUS@Q?~+(1@4IY(*(>YU415^W$d@{IM{xZ3iG~o;BwLxoX@!lH;@eQIWWTs^KoJFsPxBdftxycBfYz;8^u&Zj45;J+=D8F@ zcsxwvaLG?i(lpIly>IMCs3&EOo}7xa(M^J5LLn}=`rR_FP?#aUm$kZfP5-Ahj{Gzs zTCJ}{@gI|o-L>Ijh^#fti4sgIoE3N?XrnhhoMc*#@d%N1*QNubl}Z}h-S;odil?Qh zGevL&!Ev6D@ULet{ElghMQL<9hP3MTam~-mH;ktGLeY0_8m^JL1(8K5_8Ec8+E)e6 z^Tc9)oj?IBAqYZ@V;CZF^knlL#?rJYdIr*@_pS(r<9SXv+Iznkk1wvL4+2yji;+&p zTAxm5(CB|cG$QLAWJ|t$##kGghGjrYf}(jeIde$xyx%hY;#qigF@^Gs2b?SL41h;> zmNrTHm9z<1H=i6Pn2;y2p3c6{uIJz(dALY++iL~KPXpC!eXWKz_UCBZA@Q35H8itoOOy?)tm^*~`NeFwIEv<_XVL4u=t3%$Oxl@4j_q_ZI)U zp3R{=2M6YKh)bonSB#bg6K5&~3fUSPHs%COu<{Rdtn1eu9-ue@105`cZMGyL-4f)J5P zzxKOEOVux>Qe0$|AKJ{!Ks5omHg=i!-6)(sgA4J37#PR`HRT{GdA zagcZDh^-K8O6tFhpq;=J)T%9{>}Fktu)z;^-T@Uoh_Q#xi)dgc7!jD*-uu1xutX6aA(#B)GHx;{mCCy1 z{d7uz!opQ@j3*?iqeu45deyC-J1TaAvJH*3b$^`G$nTcXjwvU~6FGZ0L3n~T(ukW) z7VO=(*~)eZEKi^W!3f^y`Dwm@Z$F=hk+rzGx0i!C&BNphF+wi9wQN&@zH#dN7wHIJ z@w9|kif<1Qi9dPUG-@*8(8z`drSjeTYb(e9kl+EGTX_JjW6z%E`HC0$P{bTW%=`iX5vx{Ix27wJOV%flgYt{h?R|2#) zU%`qzmL(Gj2~{}C15q$YStSpNQYjBn_hI|chsuA)-rwM4le`jj9*~S=F=y_%=bU?g zBW9L7cj5+ugHr1kSbw6?2AbvyFeW|x)|k&p#a6?eSzUFS&9zvzf-5M;P=@EFY^@M5 zS#kIlqlsn_q`(JB1|pdR=_QwwnSfL(1L+m;m^E^3>JP7Uui}<~B9e=5n{9Q5&yt3? z&8b_8BJFtJXG5p7q(N_02NR8yxcu_fYdcru0hp_pbkiAdEHiBjuViJP1|b7}GmO*< z??xzqY$s1(C4=A$(Kwl^&pbFKK zdr=X>w@OI`HasmH*RZt%vn2^0jRw-m&m5>b$$Eu>>Ddt+$v$$*?j6I|%!du5=(#pP zQElF(aobY7)B$kC$MYdfZ?n@0 z{B}3nb$QZoNKj6kGn0RfEE<>Lnin?0SuA8Sy|IEr4s}5ejf#tE(>$KDW=Ai7OvH+J z2%~t9Jdip9${>SNN4~B6tAluifBjgkR38*-tx)}^ z{c;iKNdTzH?k8~!^?2wS&>KPr1Hf=g`!uq8<8VTWk21kG{o|;&b6E4<2P^Ky7YlE_V$c(s#TwZZ_*98504%dbhBK<{;tRI^`wq}dS{^v1-Wru_mJx%lPu*Ec8MS-bV($q+%_(B$bB z6LJOh?6z)h?mo#5D#rf*#Kw9CNvzR}SI(X5*Agbf0)miH%YswZs1A*Qnf%yo8sw|x^@_ZI8_yMmjPHNA*lkyckH-^wlNFDGk^z_-m zPflsQ1mwwsjeJW3dW3oE;K7tbGHCE=vn||gfluRNI`HNsCuPHXL9ZR;8?li+PsC0N zg=z@?m3I_ZlI3t*Nqo}%9=Er17Rc(CiNZ=2q|stml3YOcz)3mn40EdOT!@d!sl#cU zFU-x&t$K;K3r2i8Jl1YE9Jf`+YZF&JR&%edg_8j#H{NZD@%hPh zkx-lbdGJ`6abQH>4M3$0a@v7dJ9(W#DIX)%=oGkR+{l&y@B`#MIVE^=s(V`f?BT25 z<_BT0J$cg*+GO+!i1#AQh$=1$*5kt}ER!1&NIeDWoyj;e<{DHQ@~0155?-Cin?aT! z!01q9yIyS8*1~YDB}*oD`cij@0q?9pAT_}zQboLHl}B``>Ws6XoF-1ZGW@kjJ_75k z=Yi2o-2#jTO2<)0qj8tdOic9kjm$3Kt)u$Qd06L!zh~z1=*6i|cCzFJj29oK4E75w!3v`Fm)pO- z`1JF48~fzgfAZ|`^njN0l1e3te4lSu&lrDayK) zt2FlnJ7sYuF<8KyjH5tzfO~sJ1XDT%Vb8P9>6e%F7HGGMivcf@Do>ssZL?}MNzCsw zic-1O=0p6Boht$02TJxVuCC(rxdD!&xPYKm-+c+AaQ_FlBftKE*?oCry#FgNOBnRf z(>wW;hWB;0e+*DKRB1QEd7MMd`EnJ)36r~TC!vpk-K!KOxK9dPjv}Eez<3T~l~tbx zlc@-!x@ZN^+(tvHv{%aIqo;fGdXg|2m-TGCP%7q*H6j_Low~W;!lzg9dr8AumV`$V z_pI%i?5}VC$A1337d1}aVtMm^$7J8&l>vcckyV9+9UG{3Ct2YD;*c$iHp}58maV7l zL3s(r9D5Mj#U+L&c1OtWDhvg_xDb)D+l^KWY!OEB>gu{^TsE%9%15!-^Ndv=&1T)Z zeo}8mmh?`zL|S%fk51<`od?I7>Sq@%!DujzEDyGrWn#vY6F5XcouN&^}K0k_;lYXlRVG@GDo_mn@NM$2nyT*a@26TY)42BR>7%0> zy)9a|p0wpDC&w3439px>#FE*$v||~9d;eU!DBS=4$AA6!_xs<7$ol)kU!R@tyy1X> z&0wjTo*ljR-rDs>hwmfrD1&Xo~uLetZ0n$4yKZo7)Ne|Uq$ny* zd(3IedAY#qZys5qfAhci%Xj~z^uK{-T^KtVJ#LI*?KT)p93=h^NO*!-F*z|Z4m~Y; zDK$dr9U8|;xu-+EhQVmdIYMDi9SD{%a;uq4#$($UyQ^+;H+{8l#xs@~=_xE7F9X;K%oGT8m^zuoKmm); z8Ze=-aG?}Jpdq0qB!(2SF>SP(bTc-=*yN_(sELj7#ZBMZKVi?*AFuAUo3!%^UO>+0 zJm);;`+d$cBRAr+uMqHo?msH`{wI$gLKXtBenBJv`@?VC`F@J@S!GXOGAP+wqhho7 z6Wh@Gf)QJ=Zy)`7i#ddgNT5tl+a#5%Yj?_W8<;IN>J>zqD6TYOwMO>gk}OL^IHSwa zOezuO$FB^>YIRvMvJtDUL~vs^KiuA{N5IncU@R-?R#No8IOFr5pEERZ>d3k7t}`6Q zbPyz$nsUs!F!qPjzgeud-fkc)Oi2{$Qi*n(%$w2jZdJnn9q2T#$JHTnoR+= zQYvyc6}d{O4CpJXm72G)E)PPzp;xBV~d)Lx=eL!~DFb|JL)Tb1uU( z2s;=;M5n@{r96K>Q*+yY${E3=$vKTCTQzv*!tQ~MJ_KBeElUO=UNsdfv0}7^fD0y* zkMuPfQ8_hkk}Hil+>k2)H3N~E>Y%#LqLCiK?vCl8DKBxYCSgGC{~ds-nc}k40SiuR zB$c`v*Mw}kFvJdh|D~1qsp$TfQ&*n9gH*@h0I4=0_4CA(S#aAu0>LWxc2=G66}4OI zpa83UiAIH&v+?yieWB{IT&dSuEx}l@r71jHMEhEfHKH+Dz$K@PpnaH8qoNF*C)$s- zeT`Lf!Kyc(g@F6$i6LO75Dcr; zwnnrTL%BQJ@D7sMIkncSpLvv$Afq~retr0$;m5jP7>3f64;1_WO@|BQrsj+QsvT&vVR0Q-z=tJU}byfTMl=>jeWqB9n^9vR5a!~4JarklF! zXocfy(WM3NxBB|}?ubNgDunxPUS3HwW&(8e-c6#KOmc%4ciwx)_H`TIY`kE6tLrSu zB02qd2pLw-3H!-=JaF6`s!#=39kop8~wpLclcy9G<3(S-Bqj2rOxQ&cc8% z|0uusNKyCYg!dm^2T8aToR;8#27uMA2Y2rDiRyG-!i67sF9pU|g5c(`3ns6{)tx&7 zx3zuEyxVxi_}=$Y_~cI>{o>G6+2g*iA;ON_<%lVQQo(n71RUjgd73Lcc>BH@ZlEQt z0GFC15scMi+fRds17T~E(2C)RV=C|UF4h)2W=~=cK;#58E>m-MRf%xlC4vfZq>u`5 zQtIJEQMfWJ64#S)^3smL?fXu4&c57uIV-ICw9I3(eLkbB>B|lMiACyJT5i%;1@%ZWKG^x)&cHqF?5mBJMrqxp7Cd(IV^e0v zVqX}zC%}R%Pw^<0V{28vDaO~8s7f%*YDz^OscQsWEBb*j7Kx;) zTBYnl0?t2YTF$5942g@*dAHqC`V6c~i6pX^gdvxuTP1i|B1e*(_FQ}WzMX-MuQuKp z{k>YF)2z2FW)>|Ta|RzMIGQ6@gAqvWL6_b(uvoM`T?({n4LQ(4G3DzhGfUhOBptE> zT(4E4c>~N^b7Ny`q|~wi6;`yMTYRI|k_-l>6jQ#UCnkp&!J4L#R{|lCaIL@3 ziomEQj4(ctQ;N>^azaGQcmN+A?YAqI-80k33nUHV!)t(xgM%lOx0~8_0(L!o@%jzm zFpbG=@hqBnz1?j!q8#1-?|w(K1D^kW54v5HdHsrAF3R;l0A((|&bKb#8j@Qfn#O{y z3cvltrXW$UrUe=;W-S0zuMc2`aP;w{01zfy${=57_x6W`&|JWRf}>Q;z01?c@(qvE z6or8^pBsqVzc`X-FJAvjKs7dFu`D22X|{VLhMOKrj^rFd$L=fAAIr3V#s;XYZdoqM z+kCC-$SuV}G!R<+2jD;>Wu#NNbb3;P1_Q*jYJ!3}&*tW)K9LD?qobpwFv;6Dk*zc9 zk%OP9EPAJ=@(zlWBpNbmKpDjIUB`RQesV}{Uj(M`3A4w-n-rrIB|s>c8o6!C0dT2j zPu^>4Yd?LC8dP;~6A^E#(LlRacg@;rp^-M_K>Y)7(U1Ga(qmSg1pIzTK@4bcf}6-Z zE^luBROVCC;OMBva3hmYS{LS{2sWReayVUsT9Sd;5QgIeS|q>EbR9i;;dgKIQI>W+ z{pIzq0ODS^z3kOh%pP~n2%EwFk!CQg!p@xCoBpk~eWwo}IzV**24nq`j{mW4cD-q% zSsV{ZNjj5ESDn60+nKG>rrUP5?P$B2-OfeUeXQCJJQ#sn+a$399)d~;3~~|~FblyT zHM0VDA!A$+q6mpZid02}Do~!Yvo@^Z&g1oTF@= zZ0+~^EC&O9C8Q{bH`HUORHUS*6C%unp%f*UVih2NC1VY_?RxiazI?PO!Ic#vp}lMl za4Si7#LGGyY(`$5-#N;r*|Agh!2)(T z;wvwplBN!eN5UJ4-hQ&8bq%y`U^s7OWYkz7yd z9VyS+Bj%$ijj|#RIC{G`U`Y2t?vOAE1Y8Cn}N$t3%mCNO}rj*jG zV9WF>6%iOtqaKAo2s@oVnq`4tw^Lj=6G70GuJXFPw7ojs^rqpdfzq+*v-bxJXsG>1 zz#NCuFkd{{Y08!L_0=vanNRKq@l+2*AN8S( zT(~OaRW)yGJQ&RRXaD3AWUbphsU@qDqz4WT_O$Y`)}b)DC%2#>cX$#=RWjnw6PZ#y z6$Kb!x6`cDDO42Y=qU7hXK8i5-0j3jb3AEo=nRA&9ldfFS}_X8ZKNpa$(OF}M*>tm zo8K0bDQoc3P*-db@HED9BuN5}4Ni{S{>WZba&oTHGb4})5=JS8Gx{ICo}K;O>^Gv2 z)3#1dQY$#j13V_zwklcWIBLr#JuQgLk4=zjkuXePcfr$uOgPHW6op!jbf=K(p+-wN zD6hMg+qs6sjh!+#bf(UJh7@jLWVq=?CeQN%7S#=rjCs_B=WeHn4KZZBpQ%mJ-*2=4 z7lh2|iIEFu95K@fbLgX#0IVBo9V2kSPJj8wZ)BFtY5ND#ih~u4lARK~$JzMWab~3& z@Q0zZ@uE@S)gpyb36iy{)Cv_NOKaLygE8E@mm6g8y!LS z`gKxNOGP6=Vphjj=dMpqo4_^0#&8_WhT$Q{%SW5L2skqU?(ju}OvOx-24+ph0_@h? zAI;99Ium6XTA%sIUJ z=f61<1Qo3oB@r(_7qd&7Ks-=FT6HCp41}S(gO^Hn9M=7wJjjcHB54|9V>8vaKmPpl z06GY1g22$@^dhCZ-J>@7MFd>#0)iq5-)@|s7Y=QD zEmW(n=*d6}y6b$EW%NM6>4^&U2nt#b#7EGW?~W<734LqE#{dGysi4$xR*=v~O>Jjs ze%%8I?+GgAd%fn~oA%4)G$o3JgU}mx>&5n#DqrwK z$&9n$ZxEOiMO_w6g5WREgU0y|f~M>|j8@fvvaj@%W?L70sCwVetv#yrL!kp1ko7_a z^-g(f@VsT%0+V(B8VNQxT8?vr+3v(?km%(qNkeUme-L*_lukH$VsSqpyOgB;#3(2h7I>x4fSPrNmM6Dw&>a>=eUIQ9UZyET8mv- zju?KFn9`7KIG;LoB6txc;t7UGYJQYjqfZd<;-g|FhBs=O8MF|vk@Th@E=F0W#|KcN z1uP8Sdi&m#qd|;tkYE7)3-zrqi_sKs2eEnzkV&L$-yPqSw?fVRmXL5G;yMADvzDRq zesPC}CBMOt@wG$JoP$~{x6fetwiZg3j6hQMqu$u*jd&yynfvI-XUtggpcf=qzExa& zIJc27++;=@0Q+O4R|Pnek+Yn~4%ZVRw4R_?JJ4;xHd2xy!-4`E&F%(lJzO}X?(vufVfhQPV-~Kl%*VdCr zwuMEcy-+XzYfnwpyw3D~ zxsMU!SJuR+-7#l|0*pbolNP=K#LBjr{;xvXx7YmVAC0Ht zPE`~+MSpp+5?1TIpT<~y)agmF)aS{`EMz5oT=lHi%Hzpdr*jv)I7>&s&f8Y(10(qO z8Urr-KPAlkFTcij9L(vYWU3vt0dLBwpx{t8oHEB#Ie)RJeZ)g)yC13%7MvvL$YxDG zV;Zq6i+6zFPm&NV@A(g!n=A)fT@_Fg80HE{@4h5ii4(#1f0JlK$tiCTbm-;eDbeg? z0NLT|O1NfS+uX_QqRTr6nphPJZho}+Wj7Mi{($%?GUt9(jgxN{6mz`14S@3)u)1M1 zW~;JC-V8BhBrm2ltf-)@x@Hegsv+bs(i9;@&O=$j;SEIdNKnh$XMp`$eZ?H{LPR?g zxPkUE7{(U#@n6OWy*u^JbIe$Rlas3qx`E$ir>>vq`rw+{3UeP_TQeauk>pe^LHX`O zj_&LjMnbK&yjm{@=jhT3bDA~tTEo8%b+`#QDOGG9j~Hc*G}t7p)oew7?WCx!0n-?? zLO9G8yb16_i|GVTS`1=+S_U6`;GI*gktQr9448|smy)tDyHBq5`DiNYm2**w&ITp} zcGT>F!Z$9M_}#^sL!0wG#5HM&cysV%x)=9H!?$BpYS(7l&K%|GIkV9`7cABr{kFP9P9)6GL~J9q_tAWIT2)PWd_9P3kn#c}~nF4QwC2 z;tOQiC~ruX*UOoMvX)FF@?54-OE=1;Gr%@EcbuiDNUl~Z_;yZ%h#@FJ|Ga1Am`j8I zI?e#b)bS*w3RIA*28AcS;xp#LhoBig%9fMpWYv? zANdbbuf=j4Yv~a9;XF^%1QYh;)rJVa3>G45XYP)UKBsYb<2}ujZM@sFc(^~A6}lk2 z3F%Q#h%~e|V1Fh?(TN-Y7Tql0eZh9&Cfq)sNiXGXVL2J1X#4VXV2QW(w%Q~+btAo& z?AF%II)@{A9g!g?ltYDXKN-^20FNAIN-E`QN9DCWf8(G3nl49S4SPcNe2_Mxl8?!Q z#5!)QFw-IOx8cFt)$+ENK zr$>gY1R6@Phbqou9YQ-?kIz+#@{XOL$f)X(So=!xm@KNo>N54}b=C_1e0})lt)Z)T zDawuqUhrnh2XMbLv%7vZ?k1^lqg{fx8=6F6S*hAWxCnP%h~8(gc1`+Vh4VN!LZro% zU7nm=Pz*Sp&pO$k^&|b7YwF57ZrinWB}BVbuj=Zs!P%3hx z4h$!%ZpO}J^ISMAnRW_Anc~9a@)G5K?PhOvv<6^)>>j*7#0uCjv`*EF+lPzuTZ%by zh8I%SUcDeNDLlEjiA)zd1jmK zG3(zZT9I)?lfIjOdh%0Kw45nxVl^%iP-PsNn6%a2Z(y??}qL%)U;|`C9SYEaM z`r2;At(olumOD3xdI?D1D1t#lc<+TNb0wp-89pdS9l;y`&fy|_0VmuE(L6>_!Bi|9 z3r88+Vh;tTr(GudDhNtB&yO?7V8GDmIbZWQc(;$L~0g>zzmbnkQD= zq?m|8I(~Ps071z`jnx+u6BCj(xym>ajVc-VOzr$)cz(YATaBh8z;h2`X+5Fj#qdGY zL59DG%8Z8Sl1&uVT!g;BWkkTwcMp74a58BRcN{U%D6|k*SW*}(>i_s?= z&ChJ@b8Azxvx*t1In|k<(ETsD1K!+#6{Xi}&X^o!sA@fyin<9llG-me0gq3BiHL1_ zwX`q$GE4raHES>w;Q~mVoL9_kkk+1W zcEa$$Yhy%LL@e6mO+_%GpO+Q_s|qp^eS;6b^l*3Tu1S=#HG_3D&e5br;Y6=3Lh*kV zv>>Q^tjO8$Y)F(rNF2RbJJQnO*u?mF$=n1S(U&~wbS_A(uG$e#Ty2eXUGEs~C3>Dd z#?xQT9~&7Q9rFI1!>v-Hp7(TjYuIzIk z=ci^?n)MJoDf++a037#$)j>nIZ{M{NrCf0@b954ZmqM%zcqe!Q0gCMD|2Vtar>4>? zP6)Svf`|_2zO=4Oce~Cy9Y1tBwKKhWvbpz$+}xO4@&Y063CR`mGI@dUQWA*c3Ivb< z8zh4;0!!KHYPP6DQIrp8%c4>;PX) zetrpBH*%|U`qhHGox!>#Z_dnp&^vLPcl_ZX1{LwZ= z3y`*4RM9vN-5KXhd&=|C9met>GkKV1{>7}GJ$9hv|1a*7LY!_(52EW=A>LIq^+!jf zdQE&T%SssL)*^;8Ho4YVDIE<2!cl-1Zm-9EEz*mm{G|q@I+kkuevi&%bfjz2Bd@1_ z-pkczWjTjbWccyMMl;O>J){H>XrQ8ESwK*fL?WRWmMh#_dZL0lzOK_JZahrbofU=q z@?R16dl0avJ<@jT+$uEhtyP$WUGaF#F`CC@xl9ZxBOL2I(-sEAbXU};rKJUj_wTeX zHkWsxK;p4&d$ghEHbsQ2F6hx1mqEcpet=xGoJs{K_R*tHaVd;nbl8Vfg+_-v+P!Af zpkq0+nZNnpb^Fxpzmj%FN-+nw!xLJ#h@qjJ2A+=I3#;euUQmP4URQ6gQAOg#*&6sA zmfz9TG{?ZQS8H*SczS;-Z>LX|vU!~ietD~6(PkTrod$CphJ5u)aGfZ!89bsWOp1K4N`C{-emPa`v$^lDGP;0 z*=}djIi1($Z|FlCXlj;7NeK1Pg5;YNJTIr@!@b4jCr+NMP)Vf(Gu>II7??qA`dai| zTpKX63<2~jXV3oU^3@wjdtx?WFG5prURmxTPB(nFezndOi**E9SbhLN(n(PZ)s{9rOW}JPepLWyRO6FQ;cq;~y8Y7=6$Lm> z0du#uKj0S6%%s7J#R8GoEM(ZRG5yq8_;vNp?8;yXQF29}E|;Hf4z|cMR-5!ECx-ujmmoFjJ zA^#kpo3ln)kmBCZWkFB;sJIYkq>Oi9$PuZn6@{z1Pkm;Am?2^q#%lyMc7OKjowL6? zJNc|<%04x@q9=uwlArJU@F1t_931~*$l*A1;Yv(GwbwBmn`-PDk@8r-gte=(%E)K_2cl`sP` zpElAq4De|)U3&Db0Yf)E#+YFir`WJ>nkm|!A)lfxVf-bV{L8fJ_>qS)8y&9dQ6%s( z9fHltdG~M;%VL-_A~VhiP|ylQBDYc`aEJRX-Q8VPVdcz#mt-XR`Q+@1{{GhU=VSAm zBUPzXIukzu#pI0zA9`vM@1}yhT*V6^|0Ex}VON$zeU9 z%5y>;eLJnAKi3fDMB6c^Aem4lq^NVmd(BX8b@ zEP#;R5VTlnQ3G=1-rtrtbQ=Ab)V`ju_awNH#}M$hjlZb`eNfp|SNF+n<7jNB2x|wd z?K&K2-P$Y`vq0^n!X_N;l~6*f@FN*w5lS9dnD(W&gu(jIZxWh&jjrkzK3BYR5>B`C z5nMh$I#cba4M6|>3}F_C1>2AzU|5H)sYsx?w{b1im|Fg=j)3LElSB`r`_p%g5byHM z%8(81fBo8TX!wdEymbTG=H6ZvNs{J3z)XMqwtK^nSma_xuh8P_$Jr8IZbo+FuNnN? zm0`mYNwmmZMn_ayP_oloel?o{d`a}80>-XDTq_7yb)7oZT@?_dxf!~(Km_YEiSXuA z2)8knx^|B-+vi{0Nv`*3Hok)fPCd|7%4+;y*2yl_*!~$4g1I`eMQoj;G#0hSqFhMN z!gF)q4lwSCyMmH_G~LhCAj;*Ha+;wA8|3K48g$tJOgLPMh!o%H22T3lWF6ka;TY)= zIUF-0C9){wE5uXhyDj};CF15svkg`^n3bW4gco1p&5b z*l+yuJ~YLW+k%u?p$J!XTAh-S#~YO}ilfp`f05}J{Z4@Q0;e%%0%4Jp%yilbT?_;C zH>e*i>w_Ncgq5MuSluGoo+!S?%xI#I2NDBa%^6$)W@v$m_dyQ*0kmV$&tAM5u z0tE;Anjl933yiA<1VU5+N~Oe*C_xtll!U1H8}2=5Pf0Ivf3lGfW;`>W`F!Th&a1=J z^Ru(_e20D+fQu!^|M12!`qvxWsO#fsSDBM6%F!@E2i-)g)`}i+JZ$OtH%Yca)?u%r zj(+fePw=DDi(&`H&-BlOA#RN0oDE+z022S`lp1GywC~Fon3vjIy)cUK8p{p zkHu4>Z6YOy?|uJu(+_JI{4*HQXv>eKr=7<_HHvccr7IN3+#6pC)86`^60lXpB_~h) z<--Fn-HejcV63s*?=X1MQZ534);nXWn>`lmqz?WIwF5leZm-m}li;maF?#P9y`;&38xV{X9~&Dt{e zroU?%Z^2{eF;b)PdWGeB6&W;E`n-MBP6`NHj~{nJr1fC{T`R#A7N0qB{G(qgVQzz2 zsVhxm27@8j0AJpLw+jIVBl@~g2if#tj%?7Oh|YgCqW7*YflvjZnY*kp`bW$PuL*LC zE?<2a^sat?kV&ENN&r@(j*zSK?%MeLg(@FlY#S>nS&k&e0*R$8GtS9c|mat)knZ^dBg2oX$J=tEmA{Kss6>BT=}__34-sQst3lgOXR` zonCK{)~v$Nh} z$&1x>Ua!5nf#kZn=JX+U`|L+c@eeV{b_@dkDjDEexRn~~}VNxnH!`JLKz|XfR z)$(Et!&#QdZqRs4i0#jf;-Cz5aTHT5^QF2((&%1T@%su14Fp_RL?=?9)JoO4Er8?`v@UpZZZ0>G| ztPdl&r@Pxi(7oz2@xc z*Xu2W(JVPxYt_PUTkGx_AV7QN-ue3_#s?ln# z{akaiUt9Zyns8Q!Og^24qqs7&Vq}?wE?y1eUC_li!(OX1YBU<7PIH?Dpzufz^gVY5 za9&@KOuNqGsNMR`8#EP$R?4DYhsAn0NOAVMI$fh0Lw^}Et-SY&95^^cx9WQP{L3Ub z>%jlUs2qCPJfj#jm~a31KvGrPf{ht+DsIRr@K0CJtakD}VpN9zgu= z7=$V{+16LnU20wygpC&wAW`Desd&l;81#B$L1cwZXm~(oY>PXA8;J=Ryz$YQ!A;pV zoR}J$gLt(tI5d&T%s+YX`0?7jfCD9ctKSNR(ya=McA0nBE4}6Z8WhoHSk2+*+`T{k zh=)JLh(=>~t3;|PtgTPe+fEiZPUfgLr;3>tuTLVo(qtot6`y)d?LK($7^|B6(PEi2 zBGz%6fUE;zGy~AV{eidNKgwc&Zu1*Mu-qzWViRU4<{%b#9&5ex>$>IWZu&@fvUD`tJSX^~k>vkid1hooHw$-4B zhC)d~$^*GLyxO!Rkj@bw#kFDtI>ca9Y@e~FEgviXI0tOKj}@4pi#ZQ1EgP`n6X$+y zGU{A27y-{UFv=ADS9^_LRJgB7R6}K5ou(O$qCs;DVe-;Jz2D!4Q?x!!NH5l5hT_Pv z&|qqDcPt(VBxO08OpFfB%!s0#jL*y@H)JsqSeY7242v`H8(5I#Y(x%k&CV`uWk{U! zHUeT2En&BRrFGZ(1t^phCUffJ}jSo zFW0$1A&_7eQyoq+oi4z+GsjVTG%xR!UWck#ixw-~ggp8)f&473U54%CuLly-zoQVaMKmi-8iMvAegf)E86RHaB=2$U9Sr9cFh{;x__GXs4VU zd_I%|__EFn3=9=y21G^~ff26) z5oyJ1kwU^om@d-FX(X&tm{T`sY?czLG;NKhA<=riu>MF|ld5f6HSXVV&ye2Rwv(9Q zg^7N6p6By?KJ$FP-`PD7;-%uk`r#4K)7<*{^70%fl!}EyG%ees)jUyIdA0q_VznZT zKQh$+T}yq^eaoy*fn_dc4g*D?WrfYI=n z6bM-OI(ftPg>&!x$Ej22>y=}UUdQA%r!N2iAOJ~3K~#@AC+O&h1RlJ3>*h@xGSq25 z>6_4xO!@-;NYLauS}qAleE_|uP!<4Yw0c~cehUFDEg#l^a&klq(-~p$-5qi{TZ)r3 zC#Xlam$sikg75*A3jDK5B)qjl*md&SG*EVCcfj9n?;|uSMK{7pioem`uYP%D!%B-0 z4`M*r%g6VAcM++1dncv@o__DJ%LRX^COwc82!-ZB?$Yv65L>&zD~+LYLc*>&3W#i1_#ip|H-E2t!88i$h3yAna}*3DQ8*0GOZrm1S;5y7_^2^TpvF zwKnNwPJduEL=7=(3560orX~16h;k?tnD3E9o@Fr%1J&VZx}(3^+tTz4fpvRH0w=vG zk8JM)UP{>8Q-V-DDMItf>_LDR_xE$V&}ZdYQK1NtJ&qPZKSB}D&CX`C8B@Ai&1TCS z(!@Do^wqPajUf|sQHZX`ALxh;4K{}iL_bLPN4l9$fjcl|pE=wdlD9f(Faq^N+-`Tu zMdCHUI6tXkZDSxiD9PSA+^;4loHjM~f#n}xL7ifCQo!#|NE!mi_`!Mj43~^t!b}ft zw>f5=&(1C`PQN+XjdT0iDTwSs zS;#}JE>`m-L^fNCtbEHr8ydAf*;?9&RjfEJ`V~fxM}8g@XzsW~n`UmyuN8OzLT6Y_ z-Y_dH3TFgq5aFEwxD#HAq>=LZl?@Zgw>CmlaM4B+5%X|RjYk0R2iLoRZ8~G18H?ZN z2i8J3j#!&CXlV|Fz+)r!KP@eH&2GA@%gZ2)u-bmr=_4MaVr@;;W3nABR|Q&tj4niT z`8b!)%`P0C94j0<}VG@6EkSRBzLPIy1VJFRWx$)uBP$2AtgdXo z*m<$EwPFI^2(_v_p4#|Dp1`O)EET*gTmPByww6Z}-eXU81UlU=LcG=435pIpprN6I zH9fue(nLy;kj6kHop!G>Xtwus(If4Ivi|(Xtfl?zf(sF>G#~OAeTl^UpvWQ?xY(c` z1|R-lW}s_tPfar^y*>$ilA z!u4reOD&_@#%*d>dwVG0HhW1dG9i+r7F{2T#bVcGf4D**e=Dd=QRfV6iLkvLQ+uza zPLpT8{>TN>-+uP_S$QLzhUF!x!=Uji)(|z|XuH!-An*fw0v`LJVNjmxs#w$NURtX> zI0R({9w?uis+M+-PL4oH<+HQJ;%qju<&MA0Ev&Ir-1eGur?H?wp{M{Z|K0)8{^Z$HS>VetGU6f2zmi%V3aGT6yhJkgy^|yN;$g z(jT(K8e$!xd0GQ7X+d`N(K!$rncOURg7xVIkRRpj6wq=8EN*soQ(!L%`KjXiLOP$% zaHh)E&VOIrd-k$oC2`5Gs#R?()NpM=4&SA5QlhQ@yzTfOXV>@BMs|f8IUx^`HV;*m zttuYUm#x(O7c!m+GyXBY1RUD~cqZ5wOuX1eHpt)r4#p4>0|7z|iDSTv0XrgIXG_cv zXS<1AD~lQzQJPwbNLa-I3JWItFZ5go^=Vs$&I3q(nosB4?|k36XYOrsls>4ua@#Fa zOd4#AL?)w2q=5B4*b8t6Z}u_I-#*@E7@KyLW|2g#mdGa`wmB(!$e zclNhu3Qw_uM1)f!qxr^0Af}ig0i}x@uQ&Aw1psFDFpUAnw*o>a7+y<6=I1womH{q^ zM(;{6Iu)W3$%D26CkRg_WPI}S?{D9M1!VL9*IL*p7HhkPK}(E|-nwwH_cFn^Ow=|M zPT8-VXry$8J90VcbSXKCMufWk;>}*jXbu1B@yjPHYnAp|-WGMah9V8EazW7snU#@@Yp!IOl#18jf_??|XDcnII&@Oav!P=wnR zveC+kvad^*YMX9<#WNjsdP?`#RO;J@ooY%(DOgSWU~eC@jm;rx*4kNmvcH8UoQBdi z@D80Gmk1qur~Jf6oAj&m=e|@RL?dVY!#f<>-HZ#YSX&}ix%xDS_U`5a-klsEdH+7M z25|*x1$8$ZTm-)mj3@ml5T9BMjBUSs#RoF6sTF_dAd1^tus#UQg{sT9@CJDy!n4(d zL+u@-))H%pUQRu2YAOO**-+bXhu2sp8Q$X?xS#U*?mNaCX^tnzB!5~17uR!JH$;E&$_TuI1*U!hi0g#c#_dyURUWR0CC8`2X zRkeb0RyEs43)dDnX03LsouJGUMYXv9UU|i*VOrW+C}-;aq~2M5Ej2Tf`fAk0@)DA5 zZyf}gsTKF}`19rZ`u=`H3r*egIOXX0=O1FY=TDyjrOuHZ!`kb5ic(OyL~N1M*)-S9 z6k*lV!d_y<8&G3`oB*;4&u<2dft4`SAwNXeYxM7A1x~@gfqMJw)%KsDKqMwK1mQ%87}k@};t}h7peWlbd0_E3%m6CKGKN z{n|G%;k!}Zf$!B|2sCY7CL7Bw_~s(e(G;btvmnv2uNud2|8VY%Qb~$PSM`WxwnQnp zDi~uuu>gsWbzsrno?y)E#});DE(BqXtj$H|6NxRb^eYER0PF{6w7b8*>E#g?7|))) zz07;JHm9QLSS%7ZUj4Y4i0e^v)lF-u!{cz8sK|Gt^q!8U7<^oa+FiIuvZ4bF$gsmELdU$J=4g|MKuAP5 zEp7E$yR{if_A+1v;sU|%e2KWN{Qw6F3nWD-;uiwRscM5gOrSYPk&&~qlLaoq9LhZl60ndBIjBwSt9r9(JA8GaRPj_ zNkk}+h;dar%DbeHWvlYQZlWg|4I$&shpeD<>jh)(r|60x#O7mwIv$+^ym;|qUxcNV zUzmcQ(rde0S-~{+=Ivje^UGO5*vff@om_xx#ha&Kj7o+K5Z!uhYgNtQrAxhd9MQ(| zBe$Dsm&)%pHQla|F+(=Wmh!o{e{6Iw*_+3g2!bJmt&ErO?p&1@09&ovQjf=Dn6=xX zG&SFIb5xESs?{eIpZ(_36Z&YA2q8JmUc3wy7Y~1GXIpVQ1NUTP*$-*RVS;0YY$Cd1 zG{zup*D`|Ok78wY1Q_p^kR)&!$gE&A8rjPjCE!Cyy$0#@q=w-Mav6-Ty~F~Qx6W;~ zW9(Mo=aTDc#1PDCSy|nwQmvfvo3V!N48C?uptTU$Btu;Kw&{~;OCiKyzLyYj333*8HVHUHxR zBz8W(v9Zzp!xw69j4_XY{b*q3+GrzF^oExR;Ejy$ zV`(5WIcv5{t>RU^SuEA6nt_^CrEQ+NVLW?@l_kI}v5~ZdB05&9#NYTHfUIn2D$;Q+ zW|?ka#wq&(2l!)7!ymxiy|A#cvG8+e3pu8~F)-k%C{-hg2o<0HFVpS62>18J5M7bk z-DYr$!2yE8umhWDc>9Mf{PKd)yBJGCf!#fv4})Yx$eRpC6Y06wA_lw&uAcK|1IDp; z;D_G6B9YM>Slmpcf!C;MsoknlVM0*BI?;~SVQTJe2b#hS4wY2fs-$QM?HV;SS4r_X zpWii={cDLMp%S@g?n;ypV2Ci}bROq)98LzcQu34wn7iSaEAaDQG5 zR3&e8xBZ*3^Z!XBPvdxRwU;!PYc9ET_mWHY?zh_?F8d!a%p^ju2RQ&vpVLWygxwBg*A-L74{h&9z3?Z0r(XTWVQ*?yTs zLev;vzMtp$JkK-FGfn_5-)Ip7bYz{z4^IyZ*4)cIKnhxQ3#TVBE((RdA8LJ+JxAx^ zeF(njKWNm^s2aNesBt%Ul< zXsPegM~3F#{pkY6RMFV1%Q7y6@WGFgfp-aI8eQQuAM+PZU&bKbT0K^4ArXy(sNoz~ zc`gC<&=%@2!+8U#M!{(O{@IWB|NG#fHF=nV!x?}RejPpxDJ1WxV!NRkVwJfcVilU2 zhFq(u-l3)?aJn|vH21eN*yHBz0sirGCaFVXTb{g&Gp{Sa*SEL9rXhB1@16mFrjC1&@uEULJ9aP!tsoi(wm7zFXE{ zD3J>?P~)RIkXnrKpC@c^fx-5P z?Py#L#-jjyp(#q}_c}4K>}ae~d`A3k-HAbIs;BNpQXH;ogRaKp1-3?cVdwP6m;*}rrg^w1(x_`35&8Ls}4*J2X zqf{)6M&;P#0mLXB32`}u>Oj9W83j3yimdM8lfVD8VLaIPmtv`{a15ebqqxJ{Boc`P z^EKgpRNPZR=v4TzS zZl6iKGfVVNj`kU9^zGDzFFtqxBXuu*`g_Q#z<*rbZT(Q(dx2S0>qPf=#4?AD-8)Ie zxqOOSjHXMuS4S~8kJ)gVWoRgVJO#|^2f4vrAyCJ;EEi4fd8Rl2^M zYdxbyM=_8hoy!t~2g@y!cZE~tF`D`Ct_e~XaLMXw)11qNW_(cut#j56t00(Mg#fA{ za;K?iG=G#`jL*kjo*b=2xhUr^rIV~DT-Yz}v+PzVjY7*3O)~JrI~$uCX|%5AB*D+)JNc0QaeGneBG9L0ym9X+pGC z1I$Cf?7gZr79poxIa+~4I(2fC0$MH_o!^>|6^r?N7MvR0%nlkUmNByEkm43&kcUW( zQYmJ_u)ivBI=JsAdO=YcYHKtbuDZQLs14X5jK~}kg0kV)YLt3#UXsCjhJ9f6JqnzW z;Ep-hGO3XM?bl=S&bN3$;@zFHP4jWO-PVw~E9^5*Y2UvOSjPSAB9!+O-CE;nFgW?{ zA-{rA_Ft!W$S1&CmsEb27-daPjT$>Ve) z=9gfKo2p@rLV=qUWDp}C8-)Gc11@Dhiikp#(TIlY*VPn+sNN~Pa{LNL(YRIwxBv{u zT8yTQrcPYfHM!J?fBXNyXEqH^yz?z_b1Bd)ZkXtjzIPt&9Ix(02CJep3qwsqUbhpC zaLdZmBIWUX(79uQVT*axQ;){Ms=36fKNt*#kGa@e)6kxpMQ1{W5{aL zvc=M0TMo@)*&B_~4u(){a5B1F5#2@Lvf#a7z*d?KCKK5#S2_;)fi9c_YWe2VC(k!G zwJMX*liOP@ZLNB6C&IFOYk@(~<3?>EevGz`A0n1Tgq;jc5{+|uy>hulPEb+}64%E| zj5LNomRJja)y=wsw&*Zv*#^!*(k(80lAzsA>G2z~~;*#V_fqYW1zJ zzftz{4l9${-A#g?o?&lCWdRadwXBtp9~9EueEj%0g<7!(r+ZK!t)y0>(Zp&vnTYKt zN$JDyfBIp1+P_!ai|s=unMG3@5)(iB&X2##>MDD8b+U*7wX(#$I3*!lW|WqRwq6M# zp#fL*hPSLSTneHyFt~ItletmTg~{bi=ge!Kc@2dvc-h?^kuY~N6ZSUqE#mw;75?Ph zr9XU0lGV)2#5Yji3oCF)4a6)Ma@Upj0mZUb%64#)T7(D<=MC^O5n0fhNTm|TrPAu^ zaU#ma3exFE&z@{f7Y_FlDKxQ?f^YUSl67I37j3S{_&+D4T8Py)hUF+#Ahg2FH6Dn~iGx3owJiATa{IWt+(i8X`5y zM#*{R&!3ymz66KsQR~YayFA$Rg_!`N1nbWGybnR!dg6 zY^6{(1!V*4IZme&iLIbc{^HsFM;rKlp_I<2R#K_OIQX{YChx6fhpp^(psuW7xc0FL ziUc@$&3f~kiIUK`NdWTlv}eS8uvV!QggOwoJXJGr`(7ZhG&*{x;ffmlXMt<4Aw=&W z!`?wpvhKD|ndeBHc?U4?USz7O6{6uGH`?ud16=;~P#e+PaBP3~{F=QA^?+jDU8#rrOZqunM~@Juat*|GNn?PPdm|rx`JYy|8 z1eVtGeA3TAE7IsD6xFLbe8q%@k1)>6l*Y#X$+t~SMIu)UgM~r?k~`C%^)aoq!IS;@ zXCDzN^?M`MD1|Su(i7^w+6D}~EV0_3SPpTMU=%>*pp3v{zzdE*6@Ie6zqyZgSK*kpvgdFeE^ZBL&4IkJ7e)qBQ}ts0 zh*iP{79WHL8*JiveIG`EY!1t~UsNoKJB%@rKTnU1jg@EkWZO{T-XL7)%>rZPj;xPp zC7L~!&eB*?-Nbj&zCPwu;4>F4Vp=9Uqqo_nxaE*O+8aZ2qh3@#tCvi1PEf?+CxGNlE;8-sMd zcMoC=Uj<0+nTWpp*DK3~zn8fDs1PI!*1pq#(UZ@qv!-RMS}Y)ecCT&Hoj}ZSnOPdk zy*}3UK6kJ<@Zi?xUqAilqhnwVln0vD0I9M5s0h5d+@8atd->+BiX=^SAS`-!Hv+pP z#n?xxQzfD{728o=LvN3UBnfTWZH2*0F>s4?6URmQWLzhz`~ZwQn>BXAGLjx0DwQ$? zz(vf1ONCLD>aL~@?!#C)KgA`TOm`@K8t|E#bKrZU9eJ<*xns#lduE)_HjRlQ4 zhHSJf)uhtg-aa12DV9z2)v2hOz^#He2 z*3c6sZmt}-J}+JeUG-=fbR#q)5!Th(YR7S$WEk9!(gMD~PEIIdEnOn4sAa_B42|B_ z?xa|T@+1=p?y7-iD3!BL@kMQ}{x~5=93mgD!^m`p5E{+;4HQiinqcyFX&ME(ka6OD zkr8_`Qz%bPa!m#%$XibXzEJZg14~VEV!M!Cpu`-ghs;3ZIk2H&h*rh@7J447YYyDZ z9n9x$+!#PUVHoX;oO7h;lzk`O)d-%I!aYW{QTl5(Bp_qxplrL`+!3&`-WC|4D3WfD z3Vb3u!id={)n!RNOJnw=kd`7GpD<%&cZlPxf-o8l`Y^l2qUo8M$&@mKW7$@-6F1M4 z3mwWfAIdvmVV)WbKgp4@+;v#VHk0ZgA!d3+(w0v$NM5kQBdQKW749+Dx^b_qd{2CpWp zlXQkol#|D9;V9tJeF$Tifl3Q*J*U@md`LhKX;Ik;%9rgZOixD^!V4UFFRo3mzeIR= zcj7eQn%{rN)_UR(!-@_0h_<*DtV*pkswX`pb@%@B%a`vwv};|ixh?SeHwIQ_!37o% zi}Qf{;q{CCw=Y;nfFb|@5?e_`K~!5T7Lqju@-{T_cx0eO4+0$+RAFob-Beq?+dwch z8EoSdP2J#mDGid%N5pd3I!2Yr0at^wDh+XUJH)vGnA39!U{3qAG~H?+S?!;m2C=!6 z=~qXrS(04k_57(t>mpWbXj4KE!n&CB_zYQ7)=N4o?_)?D@jl*vu|qnM-JLBK*M}jS z4-aP@sniFw!|KJmyC53Ox_AuSq*^}CFF(jY^wHklh6x3#`rb|`&4dT7RwJQ-Y8Aqi zdat4yR$WzBqU@f4y9GuV^q9eq(N{Cuw`T-k=By~ULp(6!8r(vTK%7BnMz1wlN62PQ zZ|yiW!ryDp_P7J$3GF(aB0dgr2I*|9mu0VQm(hyxef)Iu?K?VL`8eyKT`t-+TRHqV zXP0pIUx}r;*A^I$i^usvSSag{(^{N(t@pNfHGqF7DXMeQ%5&G8G(n;TG{qN#6;-L< z=q!ju7!(Xed77duk87ZyjSH%ns|lX()@rrq zzT~}do~`wJd!@CUu9iA+Z4gyhq;ugq_7gVa zv>v5AzX{ST&>+QGsXjR!W;1T!j25{BE8i5vKvK${SJsa*UsYutwJ4nqIyOmQeW1tM z{3HTK@BUp{8Y~RSxedT(u0t4I?Q!BH1e$2(raUSZ`}X2jyfuHsYPF5xHQwb#{~>zJ zL|g&Py#WaDF2U^F0p7oDZa#*z-rRh*lS;W9!^7YKchE-PJFo5%47*@N5>#$G;`(C4 zgbu}7En5)y*@rR- z*B(J70lyqo$BvFmh_pC|etmlQ{=?_jTdJzQKKuCl;mhCN3^Ci(%y6H6@1!@e`Cw?! zj=nv^J7UA25{Rlx{1743=F8`6Jsg+SZlsgk=BR+P@NYi_hB1{6;6`)q}a-Y@(tFjBcVFcD;=e~{oIl$}Lbi3WD zWmy5Zpc1siYzWxA5iG~V zv@Om?ctJC?ul42u4i65_=Cb+xnukCTf=X}#(U2%}xa@lL)+*3d>1zcSa8O3r68zly zxTJ!40kAL~dx5*=pFZVSpFMl}2myOIU}xaL$g&yWw%q%>>G~T70POcxPY^MCb0JIj!P@qmv%f-^%@uw~3N#Bpf_ZRYr8AZl9V7{jV7|8zqE4=q)pc0(% zZKgt1Pv*6Tm1azr^+M3r6ob_sW|Z_VPe86(CZI0r|J>5j!E8kEEKdCJ(@#G`s(z#y zdS{7vM$n^Ug_U}NF-vFr|6J<;F73~GbKaVbtKb(vC%#kEvr@5G#9ad%Xa)Vd$OF1` ze6mITll$wB1J4mNHcE0c0|UmsJ(p)`T1d^yK*k(UWoi}HWxb5rTCKvqenlbSgF>N@ni3?6Hq$V4w{~@iA|x_0 zy7c1@Kkux9Q=>~y1Y#58lG)?wYX0J$lD~*D_4|_c2wFLK39(J#9u1ZSGZ*2b#49rQ z@0V{?@@{}P)+YxC(vJfV0CSGF?KvsOCU)i%pzm5 zAb6%Xe);#x(CC2=N|B|p+w`>w!zM<<-iS5ncL3&d6C>a~Mfx;{A$*8bW^gzaFBB$*7x;kW;Jgexa(*3T4Zf3vKXj6}8g;Ior5UV$vshNB8*Tx$<855xxW%6ide=-~7 z;motw8W}o4z!<{CVuITl{Ts}D{7F)#hbMKwe9#3H^z6(eVvdgNFJ!EuLh&Nd=}b>S zz)`>6Pp=80nvtuO7-=N7+9^ZxVoL|?&6kwD9@Ir)^^j) z#*=O{b3cb0i9~`K^liY5zMV^wt@ky+mD5wrvCDvW?ntG;4RL*+4p`sehkX)ke5O4L z8BN8afo+>AQ`rZM%XO{^44kTZv;^j}F`0}%j>X*DOduLt`ocm!&CY-A?rv`Gt~1?i z+OE!Yr-PdP5rm6~Baw^?!kPQ_8j+4F6+LHFG{@#1U7X8Gws%3Rc6+SdnVjEIAZ8}1 zP$;77A--2aX4>VL927l8jU# z!^vbZh50jK09;Na5(dB!!kR~usWi?qxqxo)9!FnaP4vT`6kZre9BdH~k$-X?#R+pHw zmYI_-$W^!*X%Tp2d{GvtR!{%PSIb^py=EO`(%5#?xjaVH<#++|bS!0)<)lH{SGB%=iDY&8WtkADyiqrH zsX4LwL*K)B`Kjm@CNf8OFLL1oGi)f!0qY0inJ4Mu+?mQyLND;Crd~e1z^4QTcn2Po zP1qUOR3&w#5XWbS;;~Ia9d_-8TkqJN>m(>ez_y|Cg4jui1c7$6-PD`^Z^YegqFB$U z=(9$G=b;=FmA8k93#XhY1G%1asiMYCyr`=neg|h?Wvn=-hj&pnLekLgh-o0fqeN)7ydo57FshI-Zis<2-0Xe*NlDK>z zo=a~eM^$0*J_%o2s5RV!v*SdXjs~X2)bYXyzN0Sgj>n_hM9*870OK4IFe?;+@g*uC zJG^VEt22G$1M$+{*wjNA#|$OOi}fk9M%2&c#8t>20dUU9LURz%eK?z#i`es&(p2m~ zC2RtaVpkDhyVn&C*lHNL;u*`wWJL;N2WNK_L|{i}`?vMx3qyDfjZGau(I{n}wJeaY zY}O`iy*K)^c1}OOB@UyWM3=~ENvwBrPbTw)Xh?JD!0z&L+6|69;!zxVd#wiZgl$J2 zsi?k9cQl%w0_*nNzVpp;^YxEW-o4X#+oEFzt{+KWZ{ou@!~=k}KYFJmf_;+7-r;nJ zkSF)o71iT*yPb<5dnA17(HWA(tldC1fQPso0GBSsvHfaR724E#>$|}5WKq(yayo+-nQoN57%_QueYwDu>~-*`g%ZoQoaPaayDf1hm-lp z#Rdr;@0v{h+uilNG?GN&#BK=^`~gi=2tn}VDgQx=^pX@+sj8tnbZEjrn>0;FFf?9v z5R4>qN?_S235tgympRVK>s;nEmu2zd;x5c#=inc*uc|ZZ4i2-IbrS0!9{qT)-uu4q zJ*zJuqiN)ZLC3V);+Gcbx?yNjdoV2AO#+Lc@p%_6Z|@b+OZoKw--^C}pHrI#l9;vg z+dn`23-Z;?Id8<4LGvrfRR`nAy9+`)E~K&9@jb6a#=<&dv^Jd1@`r^QD-n8dCfxS1 zrk9wpzVM`%_nY^5EwyDVLT>BlzkT}s0p_dgm#k6!>BuP-rL3wxtk?*WIRF-YDXcca z6A_-4&50!@Caw*nS{M$7j_kbAYu63Cazv0@&i<%u{t9y@HVKzFD^Z2{uP?cM_0i;# zaD!m4)qtK=fBj;6);X~~HzKTOiBn#B?FJVFg`V3XxFLtVgTWDp+T6I5m)9X?tco(p z#7vxNoE2tG(_eo4e0}w|#C-L64-}<@+#eKZTiYv!VdQUb zAkLrmuv3pc138@m%$15$=tD}Ikq8}vJOMNd1QVeX^?(4RbQ0+rk2`NNF{?-Ac5)}RzG>vJNh%e}%KfTRR&}K`;O7upRf+Ce?y-p>uAmCT zwh-!NiIR<|+T9Uy&roN4<}UBI{t-mxw!tvf`68E7sVS+P2D&+flH~balXe2&(xH@w zoorQot{Y)I4r07+TW)kvdn|nSWS7A2>3PWJN9y+4 zdOD}xoG&_eE=jWvit=FOUQ&*Ffv_w$I6(xPSgyQ#$5f|SPDGM(1$P=|`8Tg_t?>Ar z{l-v*xnA#=DGJ4CpR(e(ZCR&>a>Xh-@%`|^o?05og~VJQ73UqG+c^f1HPq;<6_FY? z$AN6crde#YIHeRilkWnL)OhT!Clhmf7GhZyOAJ=7GzQJ;TZ|poa=m>5$E4KmiXb?< z^CRaO?mZ>RrnZ-)WuPrqj+zd~hjPXDGP1cHb-m!7xLAZ%v;?haUbFcTy1lcpscaIB zBgW0n$nu2iwL{?t{@B(Se>y+%$s7ZS^QMuP-;m`c^FYm5{2=frkq$B;y*mHd{HT1E z`zD-mZlX@l3%%2E1R{_tcl}wxeMrML(z0yp1X?lDpz-2(aVqgFPcoBH?Rg0@8^<46 zP-}6SWa8N#MM;2#8Bn+LODpSZiRYNjkG1F3_2rFha>k Date: Tue, 7 Apr 2026 23:25:22 -0400 Subject: [PATCH 024/434] feat: per-plate iXex mode selection with undo, dark mode icons, and menu fixes - Add per-plate iXex mode icon to the plate toolbar (normal, hover, dark, dark-hover SVG variants) - Left-click cycles through available modes; right-click shows a popup menu with all modes as radio items - Mode changes are recorded in the undo/redo snapshot system - Fix double context menu: suppress EVT_GLCANVAS_PLATE_RIGHT_CLICK and EVT_GLCANVAS_RIGHT_CLICK when the iXex icon popup was already shown - Remove ixex_parallel_mode combo from Print Settings > Other > Special mode (superseded by per-plate icon) - Remove dead code: refresh_ixex_mode_combo(), m_ixex_mode_combo member, related Tab reload hook - iXex mode persisted in 3MF project files via existing plate metadata serialization Co-Authored-By: Claude Sonnet 4.6 --- resources/images/plate_ixex_mode.svg | 10 ++ resources/images/plate_ixex_mode_dark.svg | 10 ++ resources/images/plate_ixex_mode_hover.svg | 10 ++ .../images/plate_ixex_mode_hover_dark.svg | 10 ++ src/libslic3r/Format/bbs_3mf.cpp | 10 ++ src/libslic3r/PrintConfig.cpp | 16 ---- src/libslic3r/PrintConfig.hpp | 2 - src/slic3r/GUI/GCodeViewer.cpp | 8 +- src/slic3r/GUI/GLCanvas3D.cpp | 7 +- src/slic3r/GUI/Jobs/ArrangeJob.cpp | 17 +++- src/slic3r/GUI/PartPlate.cpp | 96 +++++++++++++++++-- src/slic3r/GUI/PartPlate.hpp | 11 ++- src/slic3r/GUI/Plater.cpp | 54 +++++++++++ src/slic3r/GUI/Tab.cpp | 54 ----------- src/slic3r/GUI/Tab.hpp | 2 - 15 files changed, 226 insertions(+), 91 deletions(-) create mode 100644 resources/images/plate_ixex_mode.svg create mode 100644 resources/images/plate_ixex_mode_dark.svg create mode 100644 resources/images/plate_ixex_mode_hover.svg create mode 100644 resources/images/plate_ixex_mode_hover_dark.svg diff --git a/resources/images/plate_ixex_mode.svg b/resources/images/plate_ixex_mode.svg new file mode 100644 index 0000000000..30a67cdf35 --- /dev/null +++ b/resources/images/plate_ixex_mode.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/images/plate_ixex_mode_dark.svg b/resources/images/plate_ixex_mode_dark.svg new file mode 100644 index 0000000000..981fd88447 --- /dev/null +++ b/resources/images/plate_ixex_mode_dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/images/plate_ixex_mode_hover.svg b/resources/images/plate_ixex_mode_hover.svg new file mode 100644 index 0000000000..9463e767df --- /dev/null +++ b/resources/images/plate_ixex_mode_hover.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/images/plate_ixex_mode_hover_dark.svg b/resources/images/plate_ixex_mode_hover_dark.svg new file mode 100644 index 0000000000..301fa995ab --- /dev/null +++ b/resources/images/plate_ixex_mode_hover_dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index dcbf638483..b7513ea240 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -333,6 +333,7 @@ static constexpr const char* FIRST_LAYER_PRINT_SEQUENCE_ATTR = "first_layer_prin static constexpr const char* OTHER_LAYERS_PRINT_SEQUENCE_ATTR = "other_layers_print_sequence"; static constexpr const char* OTHER_LAYERS_PRINT_SEQUENCE_NUMS_ATTR = "other_layers_print_sequence_nums"; static constexpr const char* SPIRAL_VASE_MODE = "spiral_mode"; +static constexpr const char* IXEX_PARALLEL_MODE_ATTR = "ixex_parallel_mode"; static constexpr const char* FILAMENT_MAP_MODE_ATTR = "filament_map_mode"; static constexpr const char* FILAMENT_MAP_ATTR = "filament_maps"; static constexpr const char* LIMIT_FILAMENT_MAP_ATTR = "limit_filament_maps"; @@ -4292,6 +4293,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) std::istringstream(value) >> std::boolalpha >> spiral_mode; m_curr_plater->config.set_key_value("spiral_mode", new ConfigOptionBool(spiral_mode)); } + else if (key == IXEX_PARALLEL_MODE_ATTR) { + m_curr_plater->config.set_key_value("ixex_parallel_mode", new ConfigOptionString(value)); + } else if (key == FILAMENT_MAP_MODE_ATTR) { FilamentMapMode map_mode = FilamentMapMode::fmmAutoForFlush; @@ -7773,6 +7777,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) if (spiral_mode_opt) stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << SPIRAL_VASE_MODE << "\" " << VALUE_ATTR << "=\"" << spiral_mode_opt->getBool() << "\"/>\n"; + { + auto* ixex_mode_opt = plate_data->config.option("ixex_parallel_mode"); + if (ixex_mode_opt && !ixex_mode_opt->value.empty() && ixex_mode_opt->value != "primary") + stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << IXEX_PARALLEL_MODE_ATTR << "\" " << VALUE_ATTR << "=\"" << ixex_mode_opt->value << "\"/>\n"; + } + //filament map related ConfigOption* filament_map_mode_opt = plate_data->config.option("filament_map_mode"); t_config_enum_names filament_map_mode_names = ConfigOptionEnum::get_enum_names(); diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index cf4fba50d8..0b287b1dff 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -5506,22 +5506,6 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionInt(2)); - def = this->add("ixex_primary_col", coInt); - def->label = L("Primary Tool Column"); - def->tooltip = L("X index (0-based) of the primary toolhead. 0 = leftmost column. Determines which zone is the active print area."); - def->min = 0; - def->max = 3; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionInt(0)); - - def = this->add("ixex_primary_row", coInt); - def->label = L("Primary Tool Row"); - def->tooltip = L("Y index (0-based) of the primary toolhead's gantry. 0 = front gantry. Determines which zone is the active print area."); - def->min = 0; - def->max = 3; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionInt(0)); - def = this->add("ixex_tool_layout", coString); def->label = L("Tool 0 Corner"); def->tooltip = L("Physical corner of the bed where tool T0 (index 0) is located. " diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 1e89eba383..a2739f1c9a 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1339,8 +1339,6 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionBool, is_ixex)) ((ConfigOptionInt, ixex_gantry_count)) ((ConfigOptionInt, ixex_tools_per_gantry)) - ((ConfigOptionInt, ixex_primary_col)) - ((ConfigOptionInt, ixex_primary_row)) ((ConfigOptionFloat, ixex_nozzle_clearance_x)) ((ConfigOptionFloat, ixex_nozzle_clearance_y)) ((ConfigOptionStrings, ixex_mode_names)) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index b4e2a9627c..5163580c30 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1533,7 +1533,13 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) if (is_ixex_opt && is_ixex_opt->value) { const DynamicPrintConfig& process_cfg = preset_bundle->prints.get_edited_preset().config; auto* mode_opt = process_cfg.opt("ixex_parallel_mode"); - const std::string mode = mode_opt ? mode_opt->value : "primary"; + std::string mode = mode_opt ? mode_opt->value : "primary"; + // Per-plate mode overrides the process preset. + if (auto* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate()) { + std::string plate_mode = plate->get_ixex_mode(); + if (plate_mode != "primary") + mode = plate_mode; + } if (mode != m_ixex_last_mode) { m_sequential_view.m_ixex_secondary_markers.clear(); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 5e23a9d4ae..66dc76661d 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -4648,9 +4648,10 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) } //BBS change plate selection + bool plate_icon_popup_shown = false; if (!m_hover_plate_idxs.empty() && (m_canvas_type == CanvasView3D) && !m_mouse.dragging) { int hover_idx = m_hover_plate_idxs.front(); - wxGetApp().plater()->select_plate_by_hover_id(hover_idx, true); + plate_icon_popup_shown = (wxGetApp().plater()->select_plate_by_hover_id(hover_idx, true) == 1); if (m_hover_volume_idxs.empty()) deselect_all(); render(); @@ -4670,10 +4671,10 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) if (!m_mouse.ignore_right_up && m_gizmos.get_current_type() == GLGizmosManager::EType::Undefined) { //BBS post right click event - if (!m_hover_plate_idxs.empty()) { + if (!m_hover_plate_idxs.empty() && !plate_icon_popup_shown) { post_event(RBtnPlateEvent(EVT_GLCANVAS_PLATE_RIGHT_CLICK, { logical_pos, m_hover_plate_idxs.front() })); } - else { + else if (!plate_icon_popup_shown) { // do not post the event if the user is panning the scene // or if right click was done over the wipe tower bool post_right_click_event = m_hover_volume_idxs.empty() || !m_volumes.volumes[get_first_hover_volume_idx()]->is_wipe_tower; diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.cpp b/src/slic3r/GUI/Jobs/ArrangeJob.cpp index 9d1d778504..b4ec570d00 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.cpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.cpp @@ -542,9 +542,16 @@ void ArrangeJob::process(Ctl &ctl) // When an iXex parallel mode is active, constrain auto-arrange to the primary zone only // and treat carriage collision strips as hard excluded regions. + // NOTE: m_ixex_primary_zone_box and ixex_collision_zones() are in global (world) coordinates + // because they are derived from m_shape which includes the plate origin offset. The arranger + // always works in plate-local space (origin = 0,0), so we subtract the plate origin here. if (PartPlate* curr_plate = partplate_list.get_curr_plate()) { if (auto pz = curr_plate->ixex_primary_zone()) { - BoundingBox scaled_pz = scaled(*pz); + Vec3d plate_origin = curr_plate->get_origin(); + double ox = plate_origin.x(), oy = plate_origin.y(); + BoundingBoxf pz_local(Vec2d(pz->min.x() - ox, pz->min.y() - oy), + Vec2d(pz->max.x() - ox, pz->max.y() - oy)); + BoundingBox scaled_pz = scaled(pz_local); bedpts = { { scaled_pz.min.x(), scaled_pz.min.y() }, { scaled_pz.max.x(), scaled_pz.min.y() }, @@ -553,10 +560,10 @@ void ArrangeJob::process(Ctl &ctl) }; for (const BoundingBoxf3& cz : curr_plate->ixex_collision_zones()) { Polygon poly({ - { scaled(cz.min.x()), scaled(cz.min.y()) }, - { scaled(cz.max.x()), scaled(cz.min.y()) }, - { scaled(cz.max.x()), scaled(cz.max.y()) }, - { scaled(cz.min.x()), scaled(cz.max.y()) }, + { scaled(cz.min.x() - ox), scaled(cz.min.y() - oy) }, + { scaled(cz.max.x() - ox), scaled(cz.min.y() - oy) }, + { scaled(cz.max.x() - ox), scaled(cz.max.y() - oy) }, + { scaled(cz.min.x() - ox), scaled(cz.max.y() - oy) }, }); arrangement::ArrangePolygon ap; ap.poly.contour = poly; diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 16a87009a1..0f2f45d919 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -365,6 +365,34 @@ std::vector PartPlate::get_plate_wrapping_detection_area() const return std::vector(); } +std::string PartPlate::get_ixex_mode() const +{ + if (m_config.has("ixex_parallel_mode")) { + auto* opt = m_config.option("ixex_parallel_mode"); + if (opt && !opt->value.empty()) + return opt->value; + } + return "primary"; +} + +void PartPlate::set_ixex_mode(const std::string& mode) +{ + if (mode.empty() || mode == "primary") { + m_config.erase("ixex_parallel_mode"); + } else { + m_config.set_key_value("ixex_parallel_mode", new ConfigOptionString(mode)); + } + update_slice_result_valid_state(false); + m_ixex_zones_mode_cache = "\x01"; // force zone rebuild +} + +void PartPlate::reset_ixex_mode() +{ + m_config.erase("ixex_parallel_mode"); + update_slice_result_valid_state(false); + m_ixex_zones_mode_cache = "\x01"; +} + void PartPlate::set_spiral_vase_mode(bool spiral_mode, bool as_global) { std::string key = "spiral_mode"; @@ -475,9 +503,14 @@ void PartPlate::calc_ixex_zones() if (!is_ixex_opt || !is_ixex_opt->value) return; - const DynamicPrintConfig& process_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; - auto* mode_opt = process_cfg.option("ixex_parallel_mode"); - std::string active_mode = mode_opt ? mode_opt->value : "primary"; + // Per-plate mode takes priority over the process preset. + std::string active_mode = get_ixex_mode(); + if (active_mode == "primary") { + const DynamicPrintConfig& process_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto* mode_opt = process_cfg.option("ixex_parallel_mode"); + if (mode_opt && !mode_opt->value.empty()) + active_mode = mode_opt->value; + } if (active_mode == "primary" || active_mode.empty()) return; @@ -767,7 +800,8 @@ void PartPlate::calc_ixex_zones() } // Build a cache key from the current iXex config options, or "" if iXex is off. -static std::string build_ixex_cache_key() +// Reads per-plate mode from m_config first, falling back to the process preset. +std::string PartPlate::build_ixex_cache_key() const { if (!wxGetApp().preset_bundle) return ""; @@ -775,14 +809,20 @@ static std::string build_ixex_cache_key() auto* is_ixex_opt = printer_cfg.option("is_ixex"); if (!is_ixex_opt || !is_ixex_opt->value) return ""; - const DynamicPrintConfig& process_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; - auto* mode_opt = process_cfg.option("ixex_parallel_mode"); + // Per-plate mode takes priority over process preset. + std::string active_mode = get_ixex_mode(); + if (active_mode == "primary") { + const DynamicPrintConfig& process_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto* mode_opt = process_cfg.option("ixex_parallel_mode"); + if (mode_opt && !mode_opt->value.empty()) + active_mode = mode_opt->value; + } auto* n_col_opt = printer_cfg.option("ixex_tools_per_gantry"); auto* n_row_opt = printer_cfg.option("ixex_gantry_count"); auto* cw_opt = printer_cfg.option("ixex_nozzle_clearance_x"); auto* ch_opt = printer_cfg.option("ixex_nozzle_clearance_y"); auto* mgn_opt = printer_cfg.option("ixex_carriage_margin"); - return (mode_opt ? mode_opt->value : "primary") + return active_mode + "|" + std::to_string(n_col_opt ? n_col_opt->value : 2) + "x" + std::to_string(n_row_opt ? n_row_opt->value : 1) + "|cw" + std::to_string(cw_opt ? (int)cw_opt->value : 0) @@ -1643,6 +1683,20 @@ void PartPlate::render_icons(bool bottom, bool only_name, int hover_id) } else render_icon_texture(m_move_front_icon.model, m_partplate_list->m_move_front_texture); + // iXex mode icon — only when is_ixex is active + { + PresetBundle* pb = wxGetApp().preset_bundle; + auto* is_ixex_opt = pb ? pb->printers.get_edited_preset().config.option("is_ixex") : nullptr; + if (is_ixex_opt && is_ixex_opt->value) { + if (hover_id == (int)PLATE_IXEX_MODE_ID) { + render_icon_texture(m_ixex_mode_icon.model, m_partplate_list->m_ixex_mode_hovered_texture); + std::string cur = get_ixex_mode(); + show_tooltip(_u8L("iXex mode: ") + cur + _u8L(" (left-click to cycle, right-click for menu)")); + } else { + render_icon_texture(m_ixex_mode_icon.model, m_partplate_list->m_ixex_mode_texture); + } + } + } if (m_partplate_list->render_plate_settings) { bool has_plate_settings = get_bed_type() != BedType::btDefault || get_print_seq() != PrintSequence::ByDefault || !get_first_layer_print_sequence().empty() || !get_other_layers_print_sequence().empty() || has_spiral_mode_config(); @@ -1952,6 +2006,13 @@ void PartPlate::register_raycasters_for_picking(GLCanvas3D &canvas) bool dual_bbl = (preset && preset->is_bbl_vendor() && preset->get_printer_extruder_count() == 2); if (dual_bbl) register_model_for_picking(canvas, m_plate_filament_map_icon, picking_id_component(PLATE_FILAMENT_MAP_ID)); + + // Register iXex mode icon only when iXex is active on the current printer preset. + if (preset) { + auto* is_ixex_opt = preset->printers.get_edited_preset().config.option("is_ixex"); + if (is_ixex_opt && is_ixex_opt->value) + register_model_for_picking(canvas, m_ixex_mode_icon, picking_id_component(PLATE_IXEX_MODE_ID)); + } } int PartPlate::picking_id_component(int idx) const @@ -3605,6 +3666,11 @@ bool PartPlate::set_shape(const Pointfs& shape, const Pointfs& exclude_areas, co dual_bbl = (preset->is_bbl_vendor() && preset->get_printer_extruder_count() == 2); calc_vertex_for_icons(dual_bbl ? 5 : 6, m_plate_filament_map_icon); calc_vertex_for_icons(dual_bbl ? 6 : 5, m_move_front_icon); + { + auto* is_ixex_opt = preset->printers.get_edited_preset().config.option("is_ixex"); + if (is_ixex_opt && is_ixex_opt->value) + calc_vertex_for_icons(dual_bbl ? 7 : 6, m_ixex_mode_icon); + } calc_vertex_for_number(0, false, m_plate_idx_icon); // calc vertex for plate name @@ -4474,6 +4540,20 @@ void PartPlateList::generate_icon_textures() } } + // iXex mode icon textures (fall back gracefully if SVG not present yet) + { + file_name = path + (m_is_dark ? "plate_ixex_mode_dark.svg" : "plate_ixex_mode.svg"); + if (!m_ixex_mode_texture.load_from_svg_file(file_name, true, false, false, icon_size)) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":load file %1% failed (iXex mode icon)") % file_name; + } + } + { + file_name = path + (m_is_dark ? "plate_ixex_mode_hover_dark.svg" : "plate_ixex_mode_hover.svg"); + if (!m_ixex_mode_hovered_texture.load_from_svg_file(file_name, true, false, false, icon_size)) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":load file %1% failed (iXex mode hover icon)") % file_name; + } + } + std::string text_str = "01"; // ORCA also scale font size to prevent low res texture int size = wxGetApp().em_unit() * PARTPLATE_ICON_SIZE; @@ -4517,6 +4597,8 @@ void PartPlateList::release_icon_textures() m_plate_set_filament_map_hovered_texture.reset(); m_plate_name_edit_texture.reset(); m_plate_name_edit_hovered_texture.reset(); + m_ixex_mode_texture.reset(); + m_ixex_mode_hovered_texture.reset(); for (int i = 0;i < MAX_PLATE_COUNT; i++) { m_idx_textures[i].reset(); } diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index 4954e6f3b5..773524c77c 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -155,6 +155,7 @@ private: PickingModel m_plate_filament_map_icon; PickingModel m_plate_name_edit_icon; PickingModel m_move_front_icon; + PickingModel m_ixex_mode_icon; GLModel m_plate_idx_icon; GLTexture m_texture; @@ -188,6 +189,7 @@ private: void calc_height_limit(); void calc_ixex_zones(); void ensure_ixex_zones(); + std::string build_ixex_cache_key() const; void render_ixex_zones(bool force_default_color); void calc_vertex_for_number(int index, bool one_number, GLModel &buffer); void calc_vertex_for_plate_name_edit_icon(GLTexture *texture, int index, PickingModel &model); @@ -220,7 +222,8 @@ private: public: static constexpr unsigned int PLATE_NAME_HOVER_ID = 6; static constexpr unsigned int PLATE_FILAMENT_MAP_ID = 8; - static constexpr unsigned int GRABBER_COUNT = 9; + static constexpr unsigned int PLATE_IXEX_MODE_ID = 9; + static constexpr unsigned int GRABBER_COUNT = 10; static ColorRGBA SELECT_COLOR; static ColorRGBA UNSELECT_COLOR; @@ -280,6 +283,10 @@ public: bool get_spiral_vase_mode() const; void set_spiral_vase_mode(bool spiral_mode, bool as_global); + std::string get_ixex_mode() const; + void set_ixex_mode(const std::string& mode); + void reset_ixex_mode(); + std::vector get_plate_wrapping_detection_area() const; //static const int plate_x_offset = 20; //mm @@ -627,6 +634,8 @@ class PartPlateList : public ObjectBase GLTexture m_plate_set_filament_map_hovered_texture; GLTexture m_plate_name_edit_texture; GLTexture m_plate_name_edit_hovered_texture; + GLTexture m_ixex_mode_texture; + GLTexture m_ixex_mode_hovered_texture; GLTexture m_idx_textures[MAX_PLATE_COUNT]; // set render option bool render_bedtype_logo = true; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 2b07bc94a8..176b46d535 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -17640,7 +17640,61 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi update(); p->partplate_list.select_plate(0); } + else if (action == (int)PartPlate::PLATE_IXEX_MODE_ID) + { + ret = select_plate(plate_index); + if (!ret) { + PartPlate* curr_plate = p->partplate_list.get_curr_plate(); + // Build ordered mode list: "primary" first, then all named modes. + std::vector modes; + modes.push_back("primary"); + const DynamicPrintConfig& printer_cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + auto* names_opt = printer_cfg.option("ixex_mode_names"); + if (names_opt) { + for (const auto& n : names_opt->values) + if (!n.empty()) modes.push_back(n); + } + if (right_click) { + // Show a popup menu with all modes. + wxMenu menu; + std::string current = curr_plate->get_ixex_mode(); + for (size_t i = 0; i < modes.size(); ++i) { + wxMenuItem* item = menu.AppendRadioItem(wxID_HIGHEST + (int)i, from_u8(modes[i])); + if (modes[i] == current) + item->Check(true); + } + menu.Bind(wxEVT_MENU, [this, curr_plate, &modes](wxCommandEvent& e) { + int idx = e.GetId() - wxID_HIGHEST; + if (idx >= 0 && idx < (int)modes.size()) { + take_snapshot("set ixex mode"); + curr_plate->set_ixex_mode(modes[idx]); + update_project_dirty_from_presets(); + set_plater_dirty(true); + update(); + } + }); + p->view3D->get_canvas3d()->get_wxglcanvas()->PopupMenu(&menu); + ret = 1; // signal to caller: popup was shown, suppress plate context menu + } else { + // Left-click: cycle to next mode. + std::string current = curr_plate->get_ixex_mode(); + auto it = std::find(modes.begin(), modes.end(), current); + size_t next_idx = (it == modes.end()) ? 0 : ((it - modes.begin() + 1) % modes.size()); + std::string next_mode = modes[next_idx]; + if (next_mode != current) { + take_snapshot("set ixex mode"); + curr_plate->set_ixex_mode(next_mode); + update_project_dirty_from_presets(); + set_plater_dirty(true); + update(); + } + } + } else { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "can not select plate %1%" << plate_index; + ret = -1; + } + } else { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "invalid action %1%, with right_click=%2%" << action << right_click; diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 77283cfd8a..d6355a3a5d 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -2674,26 +2674,6 @@ void TabPrint::build() optgroup->append_single_option_line("timelapse_type", "others_settings_special_mode#timelapse"); optgroup->append_single_option_line("enable_wrapping_detection"); - // Dynamic combo box — choices come from the printer preset's ixex_mode_names. - // Use create_line_with_widget so the undo bitmaps are set (has_undo_ui()=true), - // ensuring the widget x-position matches other option fields in OG_CustomCtrl. - create_line_with_widget(optgroup.get(), "ixex_parallel_mode", "", [this](wxWindow* parent) -> wxSizer* { - m_ixex_mode_combo = new ComboBox(parent, wxID_ANY, wxEmptyString, - wxDefaultPosition, - wxSize(12 * wxGetApp().em_unit(), -1), - 0, nullptr, wxCB_READONLY); - m_ixex_mode_combo->GetDropDown().SetUseContentWidth(true); - refresh_ixex_mode_combo(); - m_ixex_mode_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { - if (!m_ixex_mode_combo || !m_config) return; - std::string val = m_ixex_mode_combo->GetStringSelection().ToStdString(); - m_config->set_key_value("ixex_parallel_mode", new ConfigOptionString(val)); - on_value_change("ixex_parallel_mode", val); - }); - auto* s = new wxBoxSizer(wxHORIZONTAL); - s->Add(m_ixex_mode_combo, 0, wxALIGN_CENTER_VERTICAL); - return s; - }); optgroup = page->new_optgroup(L("Fuzzy Skin"), L"fuzzy_skin"); optgroup->append_single_option_line("fuzzy_skin", "others_settings_fuzzy_skin"); @@ -2752,36 +2732,6 @@ void TabPrint::reload_config() { this->compatible_widget_reload(m_compatible_printers); Tab::reload_config(); - refresh_ixex_mode_combo(); -} - -void TabPrint::refresh_ixex_mode_combo() -{ - if (!m_ixex_mode_combo) return; - // Rebuild choices: "primary" + names from the active printer preset - m_ixex_mode_combo->Clear(); - m_ixex_mode_combo->Append("primary"); - if (m_preset_bundle) { - auto* names = m_preset_bundle->printers.get_edited_preset().config - .option("ixex_mode_names"); - if (names) { - for (const auto& n : names->values) - if (!n.empty()) m_ixex_mode_combo->Append(wxString::FromUTF8(n)); - } - } - // Restore current selection from process config (safe: option may not exist in older presets) - std::string cur = "primary"; - if (m_config) { - auto* opt = m_config->option("ixex_parallel_mode"); - if (opt) cur = opt->value; - } - if (!m_ixex_mode_combo->SetStringSelection(cur)) { - m_ixex_mode_combo->SetSelection(0); // fall back to "primary" - // The active mode no longer exists — reset config so stale name doesn't crash downstream. - if (m_config) - m_config->set_key_value("ixex_parallel_mode", new ConfigOptionString("primary")); - on_value_change("ixex_parallel_mode", std::string("primary")); - } } void TabPrint::update_description_lines() @@ -2903,7 +2853,6 @@ void TabPrint::clear_pages() m_recommended_thin_wall_thickness_description_line = nullptr; m_top_bottom_shell_thickness_explanation = nullptr; - m_ixex_mode_combo = nullptr; } //BBS: GUI refactor @@ -5059,9 +5008,6 @@ void TabPrinter::build_fff() m_config->set_key_value("ixex_mode_gcodes", new ConfigOptionStrings(gcodes)); update_dirty(); on_value_change("ixex_mode_names", std::string("")); - // Refresh the process tab's mode dropdown immediately - if (auto* print_tab = dynamic_cast(wxGetApp().get_tab(Preset::TYPE_PRINT))) - print_tab->refresh_ixex_mode_combo(); }; auto* sizer = new wxBoxSizer(wxHORIZONTAL); sizer->Add(m_ixex_modes_ctrl, 1, wxEXPAND); diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index 687cfeea73..7d86278db7 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -464,12 +464,10 @@ public: void update() override; void clear_pages() override; bool supports_printer_technology(const PrinterTechnology tech) const override { return tech == ptFFF; } - void refresh_ixex_mode_combo(); private: ogStaticText* m_recommended_thin_wall_thickness_description_line = nullptr; ogStaticText* m_top_bottom_shell_thickness_explanation = nullptr; - ComboBox* m_ixex_mode_combo {nullptr}; }; class TabPrintModel : public TabPrint From 5fa6480f6188f94a2789b122347048818b85a6f4 Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Tue, 7 Apr 2026 23:25:22 -0400 Subject: [PATCH 025/434] feat: per-plate iXex mode selection with undo, dark mode icons, and menu fixes - Add per-plate iXex mode icon to the plate toolbar (normal, hover, dark, dark-hover SVG variants) - Left-click cycles through available modes; right-click shows a popup menu with all modes as radio items - Mode changes are recorded in the undo/redo snapshot system - Fix double context menu: suppress EVT_GLCANVAS_PLATE_RIGHT_CLICK and EVT_GLCANVAS_RIGHT_CLICK when the iXex icon popup was already shown - Remove ixex_parallel_mode combo from Print Settings > Other > Special mode (superseded by per-plate icon) - Remove dead code: refresh_ixex_mode_combo(), m_ixex_mode_combo member, related Tab reload hook - iXex mode persisted in 3MF project files via existing plate metadata serialization Co-Authored-By: Claude Sonnet 4.6 --- resources/images/plate_ixex_mode.svg | 10 ++ resources/images/plate_ixex_mode_dark.svg | 10 ++ resources/images/plate_ixex_mode_hover.svg | 10 ++ .../images/plate_ixex_mode_hover_dark.svg | 10 ++ src/libslic3r/Format/bbs_3mf.cpp | 10 ++ src/libslic3r/PrintConfig.cpp | 16 ---- src/libslic3r/PrintConfig.hpp | 2 - src/slic3r/GUI/GCodeViewer.cpp | 8 +- src/slic3r/GUI/GLCanvas3D.cpp | 7 +- src/slic3r/GUI/Jobs/ArrangeJob.cpp | 17 +++- src/slic3r/GUI/PartPlate.cpp | 96 +++++++++++++++++-- src/slic3r/GUI/PartPlate.hpp | 11 ++- src/slic3r/GUI/Plater.cpp | 54 +++++++++++ src/slic3r/GUI/Tab.cpp | 54 ----------- src/slic3r/GUI/Tab.hpp | 2 - 15 files changed, 226 insertions(+), 91 deletions(-) create mode 100644 resources/images/plate_ixex_mode.svg create mode 100644 resources/images/plate_ixex_mode_dark.svg create mode 100644 resources/images/plate_ixex_mode_hover.svg create mode 100644 resources/images/plate_ixex_mode_hover_dark.svg diff --git a/resources/images/plate_ixex_mode.svg b/resources/images/plate_ixex_mode.svg new file mode 100644 index 0000000000..30a67cdf35 --- /dev/null +++ b/resources/images/plate_ixex_mode.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/images/plate_ixex_mode_dark.svg b/resources/images/plate_ixex_mode_dark.svg new file mode 100644 index 0000000000..981fd88447 --- /dev/null +++ b/resources/images/plate_ixex_mode_dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/images/plate_ixex_mode_hover.svg b/resources/images/plate_ixex_mode_hover.svg new file mode 100644 index 0000000000..9463e767df --- /dev/null +++ b/resources/images/plate_ixex_mode_hover.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/images/plate_ixex_mode_hover_dark.svg b/resources/images/plate_ixex_mode_hover_dark.svg new file mode 100644 index 0000000000..301fa995ab --- /dev/null +++ b/resources/images/plate_ixex_mode_hover_dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index dcbf638483..b7513ea240 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -333,6 +333,7 @@ static constexpr const char* FIRST_LAYER_PRINT_SEQUENCE_ATTR = "first_layer_prin static constexpr const char* OTHER_LAYERS_PRINT_SEQUENCE_ATTR = "other_layers_print_sequence"; static constexpr const char* OTHER_LAYERS_PRINT_SEQUENCE_NUMS_ATTR = "other_layers_print_sequence_nums"; static constexpr const char* SPIRAL_VASE_MODE = "spiral_mode"; +static constexpr const char* IXEX_PARALLEL_MODE_ATTR = "ixex_parallel_mode"; static constexpr const char* FILAMENT_MAP_MODE_ATTR = "filament_map_mode"; static constexpr const char* FILAMENT_MAP_ATTR = "filament_maps"; static constexpr const char* LIMIT_FILAMENT_MAP_ATTR = "limit_filament_maps"; @@ -4292,6 +4293,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) std::istringstream(value) >> std::boolalpha >> spiral_mode; m_curr_plater->config.set_key_value("spiral_mode", new ConfigOptionBool(spiral_mode)); } + else if (key == IXEX_PARALLEL_MODE_ATTR) { + m_curr_plater->config.set_key_value("ixex_parallel_mode", new ConfigOptionString(value)); + } else if (key == FILAMENT_MAP_MODE_ATTR) { FilamentMapMode map_mode = FilamentMapMode::fmmAutoForFlush; @@ -7773,6 +7777,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) if (spiral_mode_opt) stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << SPIRAL_VASE_MODE << "\" " << VALUE_ATTR << "=\"" << spiral_mode_opt->getBool() << "\"/>\n"; + { + auto* ixex_mode_opt = plate_data->config.option("ixex_parallel_mode"); + if (ixex_mode_opt && !ixex_mode_opt->value.empty() && ixex_mode_opt->value != "primary") + stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << IXEX_PARALLEL_MODE_ATTR << "\" " << VALUE_ATTR << "=\"" << ixex_mode_opt->value << "\"/>\n"; + } + //filament map related ConfigOption* filament_map_mode_opt = plate_data->config.option("filament_map_mode"); t_config_enum_names filament_map_mode_names = ConfigOptionEnum::get_enum_names(); diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 67d2340a9a..30b2ead516 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -5510,22 +5510,6 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionInt(2)); - def = this->add("ixex_primary_col", coInt); - def->label = L("Primary Tool Column"); - def->tooltip = L("X index (0-based) of the primary toolhead. 0 = leftmost column. Determines which zone is the active print area."); - def->min = 0; - def->max = 3; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionInt(0)); - - def = this->add("ixex_primary_row", coInt); - def->label = L("Primary Tool Row"); - def->tooltip = L("Y index (0-based) of the primary toolhead's gantry. 0 = front gantry. Determines which zone is the active print area."); - def->min = 0; - def->max = 3; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionInt(0)); - def = this->add("ixex_tool_layout", coString); def->label = L("Tool 0 Corner"); def->tooltip = L("Physical corner of the bed where tool T0 (index 0) is located. " diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 83d8b1ab97..bdc384b023 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1338,8 +1338,6 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionBool, is_ixex)) ((ConfigOptionInt, ixex_gantry_count)) ((ConfigOptionInt, ixex_tools_per_gantry)) - ((ConfigOptionInt, ixex_primary_col)) - ((ConfigOptionInt, ixex_primary_row)) ((ConfigOptionFloat, ixex_nozzle_clearance_x)) ((ConfigOptionFloat, ixex_nozzle_clearance_y)) ((ConfigOptionStrings, ixex_mode_names)) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index b4e2a9627c..5163580c30 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1533,7 +1533,13 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) if (is_ixex_opt && is_ixex_opt->value) { const DynamicPrintConfig& process_cfg = preset_bundle->prints.get_edited_preset().config; auto* mode_opt = process_cfg.opt("ixex_parallel_mode"); - const std::string mode = mode_opt ? mode_opt->value : "primary"; + std::string mode = mode_opt ? mode_opt->value : "primary"; + // Per-plate mode overrides the process preset. + if (auto* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate()) { + std::string plate_mode = plate->get_ixex_mode(); + if (plate_mode != "primary") + mode = plate_mode; + } if (mode != m_ixex_last_mode) { m_sequential_view.m_ixex_secondary_markers.clear(); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 5e23a9d4ae..66dc76661d 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -4648,9 +4648,10 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) } //BBS change plate selection + bool plate_icon_popup_shown = false; if (!m_hover_plate_idxs.empty() && (m_canvas_type == CanvasView3D) && !m_mouse.dragging) { int hover_idx = m_hover_plate_idxs.front(); - wxGetApp().plater()->select_plate_by_hover_id(hover_idx, true); + plate_icon_popup_shown = (wxGetApp().plater()->select_plate_by_hover_id(hover_idx, true) == 1); if (m_hover_volume_idxs.empty()) deselect_all(); render(); @@ -4670,10 +4671,10 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) if (!m_mouse.ignore_right_up && m_gizmos.get_current_type() == GLGizmosManager::EType::Undefined) { //BBS post right click event - if (!m_hover_plate_idxs.empty()) { + if (!m_hover_plate_idxs.empty() && !plate_icon_popup_shown) { post_event(RBtnPlateEvent(EVT_GLCANVAS_PLATE_RIGHT_CLICK, { logical_pos, m_hover_plate_idxs.front() })); } - else { + else if (!plate_icon_popup_shown) { // do not post the event if the user is panning the scene // or if right click was done over the wipe tower bool post_right_click_event = m_hover_volume_idxs.empty() || !m_volumes.volumes[get_first_hover_volume_idx()]->is_wipe_tower; diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.cpp b/src/slic3r/GUI/Jobs/ArrangeJob.cpp index 9d1d778504..b4ec570d00 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.cpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.cpp @@ -542,9 +542,16 @@ void ArrangeJob::process(Ctl &ctl) // When an iXex parallel mode is active, constrain auto-arrange to the primary zone only // and treat carriage collision strips as hard excluded regions. + // NOTE: m_ixex_primary_zone_box and ixex_collision_zones() are in global (world) coordinates + // because they are derived from m_shape which includes the plate origin offset. The arranger + // always works in plate-local space (origin = 0,0), so we subtract the plate origin here. if (PartPlate* curr_plate = partplate_list.get_curr_plate()) { if (auto pz = curr_plate->ixex_primary_zone()) { - BoundingBox scaled_pz = scaled(*pz); + Vec3d plate_origin = curr_plate->get_origin(); + double ox = plate_origin.x(), oy = plate_origin.y(); + BoundingBoxf pz_local(Vec2d(pz->min.x() - ox, pz->min.y() - oy), + Vec2d(pz->max.x() - ox, pz->max.y() - oy)); + BoundingBox scaled_pz = scaled(pz_local); bedpts = { { scaled_pz.min.x(), scaled_pz.min.y() }, { scaled_pz.max.x(), scaled_pz.min.y() }, @@ -553,10 +560,10 @@ void ArrangeJob::process(Ctl &ctl) }; for (const BoundingBoxf3& cz : curr_plate->ixex_collision_zones()) { Polygon poly({ - { scaled(cz.min.x()), scaled(cz.min.y()) }, - { scaled(cz.max.x()), scaled(cz.min.y()) }, - { scaled(cz.max.x()), scaled(cz.max.y()) }, - { scaled(cz.min.x()), scaled(cz.max.y()) }, + { scaled(cz.min.x() - ox), scaled(cz.min.y() - oy) }, + { scaled(cz.max.x() - ox), scaled(cz.min.y() - oy) }, + { scaled(cz.max.x() - ox), scaled(cz.max.y() - oy) }, + { scaled(cz.min.x() - ox), scaled(cz.max.y() - oy) }, }); arrangement::ArrangePolygon ap; ap.poly.contour = poly; diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 9d7b6d4e5f..9ea79e3643 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -365,6 +365,34 @@ std::vector PartPlate::get_plate_wrapping_detection_area() const return std::vector(); } +std::string PartPlate::get_ixex_mode() const +{ + if (m_config.has("ixex_parallel_mode")) { + auto* opt = m_config.option("ixex_parallel_mode"); + if (opt && !opt->value.empty()) + return opt->value; + } + return "primary"; +} + +void PartPlate::set_ixex_mode(const std::string& mode) +{ + if (mode.empty() || mode == "primary") { + m_config.erase("ixex_parallel_mode"); + } else { + m_config.set_key_value("ixex_parallel_mode", new ConfigOptionString(mode)); + } + update_slice_result_valid_state(false); + m_ixex_zones_mode_cache = "\x01"; // force zone rebuild +} + +void PartPlate::reset_ixex_mode() +{ + m_config.erase("ixex_parallel_mode"); + update_slice_result_valid_state(false); + m_ixex_zones_mode_cache = "\x01"; +} + void PartPlate::set_spiral_vase_mode(bool spiral_mode, bool as_global) { std::string key = "spiral_mode"; @@ -475,9 +503,14 @@ void PartPlate::calc_ixex_zones() if (!is_ixex_opt || !is_ixex_opt->value) return; - const DynamicPrintConfig& process_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; - auto* mode_opt = process_cfg.option("ixex_parallel_mode"); - std::string active_mode = mode_opt ? mode_opt->value : "primary"; + // Per-plate mode takes priority over the process preset. + std::string active_mode = get_ixex_mode(); + if (active_mode == "primary") { + const DynamicPrintConfig& process_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto* mode_opt = process_cfg.option("ixex_parallel_mode"); + if (mode_opt && !mode_opt->value.empty()) + active_mode = mode_opt->value; + } if (active_mode == "primary" || active_mode.empty()) return; @@ -767,7 +800,8 @@ void PartPlate::calc_ixex_zones() } // Build a cache key from the current iXex config options, or "" if iXex is off. -static std::string build_ixex_cache_key() +// Reads per-plate mode from m_config first, falling back to the process preset. +std::string PartPlate::build_ixex_cache_key() const { if (!wxGetApp().preset_bundle) return ""; @@ -775,14 +809,20 @@ static std::string build_ixex_cache_key() auto* is_ixex_opt = printer_cfg.option("is_ixex"); if (!is_ixex_opt || !is_ixex_opt->value) return ""; - const DynamicPrintConfig& process_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; - auto* mode_opt = process_cfg.option("ixex_parallel_mode"); + // Per-plate mode takes priority over process preset. + std::string active_mode = get_ixex_mode(); + if (active_mode == "primary") { + const DynamicPrintConfig& process_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto* mode_opt = process_cfg.option("ixex_parallel_mode"); + if (mode_opt && !mode_opt->value.empty()) + active_mode = mode_opt->value; + } auto* n_col_opt = printer_cfg.option("ixex_tools_per_gantry"); auto* n_row_opt = printer_cfg.option("ixex_gantry_count"); auto* cw_opt = printer_cfg.option("ixex_nozzle_clearance_x"); auto* ch_opt = printer_cfg.option("ixex_nozzle_clearance_y"); auto* mgn_opt = printer_cfg.option("ixex_carriage_margin"); - return (mode_opt ? mode_opt->value : "primary") + return active_mode + "|" + std::to_string(n_col_opt ? n_col_opt->value : 2) + "x" + std::to_string(n_row_opt ? n_row_opt->value : 1) + "|cw" + std::to_string(cw_opt ? (int)cw_opt->value : 0) @@ -1643,6 +1683,20 @@ void PartPlate::render_icons(bool bottom, bool only_name, int hover_id) } else render_icon_texture(m_move_front_icon.model, m_partplate_list->m_move_front_texture); + // iXex mode icon — only when is_ixex is active + { + PresetBundle* pb = wxGetApp().preset_bundle; + auto* is_ixex_opt = pb ? pb->printers.get_edited_preset().config.option("is_ixex") : nullptr; + if (is_ixex_opt && is_ixex_opt->value) { + if (hover_id == (int)PLATE_IXEX_MODE_ID) { + render_icon_texture(m_ixex_mode_icon.model, m_partplate_list->m_ixex_mode_hovered_texture); + std::string cur = get_ixex_mode(); + show_tooltip(_u8L("iXex mode: ") + cur + _u8L(" (left-click to cycle, right-click for menu)")); + } else { + render_icon_texture(m_ixex_mode_icon.model, m_partplate_list->m_ixex_mode_texture); + } + } + } if (m_partplate_list->render_plate_settings) { bool has_plate_settings = get_bed_type() != BedType::btDefault || get_print_seq() != PrintSequence::ByDefault || !get_first_layer_print_sequence().empty() || !get_other_layers_print_sequence().empty() || has_spiral_mode_config(); @@ -1952,6 +2006,13 @@ void PartPlate::register_raycasters_for_picking(GLCanvas3D &canvas) bool dual_bbl = (preset && preset->is_bbl_vendor() && preset->get_printer_extruder_count() == 2); if (dual_bbl) register_model_for_picking(canvas, m_plate_filament_map_icon, picking_id_component(PLATE_FILAMENT_MAP_ID)); + + // Register iXex mode icon only when iXex is active on the current printer preset. + if (preset) { + auto* is_ixex_opt = preset->printers.get_edited_preset().config.option("is_ixex"); + if (is_ixex_opt && is_ixex_opt->value) + register_model_for_picking(canvas, m_ixex_mode_icon, picking_id_component(PLATE_IXEX_MODE_ID)); + } } int PartPlate::picking_id_component(int idx) const @@ -3604,6 +3665,11 @@ bool PartPlate::set_shape(const Pointfs& shape, const Pointfs& exclude_areas, co dual_bbl = (preset->is_bbl_vendor() && preset->get_printer_extruder_count() == 2); calc_vertex_for_icons(dual_bbl ? 5 : 6, m_plate_filament_map_icon); calc_vertex_for_icons(dual_bbl ? 6 : 5, m_move_front_icon); + { + auto* is_ixex_opt = preset->printers.get_edited_preset().config.option("is_ixex"); + if (is_ixex_opt && is_ixex_opt->value) + calc_vertex_for_icons(dual_bbl ? 7 : 6, m_ixex_mode_icon); + } calc_vertex_for_number(0, false, m_plate_idx_icon); // calc vertex for plate name @@ -4473,6 +4539,20 @@ void PartPlateList::generate_icon_textures() } } + // iXex mode icon textures (fall back gracefully if SVG not present yet) + { + file_name = path + (m_is_dark ? "plate_ixex_mode_dark.svg" : "plate_ixex_mode.svg"); + if (!m_ixex_mode_texture.load_from_svg_file(file_name, true, false, false, icon_size)) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":load file %1% failed (iXex mode icon)") % file_name; + } + } + { + file_name = path + (m_is_dark ? "plate_ixex_mode_hover_dark.svg" : "plate_ixex_mode_hover.svg"); + if (!m_ixex_mode_hovered_texture.load_from_svg_file(file_name, true, false, false, icon_size)) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":load file %1% failed (iXex mode hover icon)") % file_name; + } + } + std::string text_str = "01"; // ORCA also scale font size to prevent low res texture int size = wxGetApp().em_unit() * PARTPLATE_ICON_SIZE; @@ -4516,6 +4596,8 @@ void PartPlateList::release_icon_textures() m_plate_set_filament_map_hovered_texture.reset(); m_plate_name_edit_texture.reset(); m_plate_name_edit_hovered_texture.reset(); + m_ixex_mode_texture.reset(); + m_ixex_mode_hovered_texture.reset(); for (int i = 0;i < MAX_PLATE_COUNT; i++) { m_idx_textures[i].reset(); } diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index 4954e6f3b5..773524c77c 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -155,6 +155,7 @@ private: PickingModel m_plate_filament_map_icon; PickingModel m_plate_name_edit_icon; PickingModel m_move_front_icon; + PickingModel m_ixex_mode_icon; GLModel m_plate_idx_icon; GLTexture m_texture; @@ -188,6 +189,7 @@ private: void calc_height_limit(); void calc_ixex_zones(); void ensure_ixex_zones(); + std::string build_ixex_cache_key() const; void render_ixex_zones(bool force_default_color); void calc_vertex_for_number(int index, bool one_number, GLModel &buffer); void calc_vertex_for_plate_name_edit_icon(GLTexture *texture, int index, PickingModel &model); @@ -220,7 +222,8 @@ private: public: static constexpr unsigned int PLATE_NAME_HOVER_ID = 6; static constexpr unsigned int PLATE_FILAMENT_MAP_ID = 8; - static constexpr unsigned int GRABBER_COUNT = 9; + static constexpr unsigned int PLATE_IXEX_MODE_ID = 9; + static constexpr unsigned int GRABBER_COUNT = 10; static ColorRGBA SELECT_COLOR; static ColorRGBA UNSELECT_COLOR; @@ -280,6 +283,10 @@ public: bool get_spiral_vase_mode() const; void set_spiral_vase_mode(bool spiral_mode, bool as_global); + std::string get_ixex_mode() const; + void set_ixex_mode(const std::string& mode); + void reset_ixex_mode(); + std::vector get_plate_wrapping_detection_area() const; //static const int plate_x_offset = 20; //mm @@ -627,6 +634,8 @@ class PartPlateList : public ObjectBase GLTexture m_plate_set_filament_map_hovered_texture; GLTexture m_plate_name_edit_texture; GLTexture m_plate_name_edit_hovered_texture; + GLTexture m_ixex_mode_texture; + GLTexture m_ixex_mode_hovered_texture; GLTexture m_idx_textures[MAX_PLATE_COUNT]; // set render option bool render_bedtype_logo = true; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 6c19a0ae43..f1b985120a 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -17640,7 +17640,61 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi update(); p->partplate_list.select_plate(0); } + else if (action == (int)PartPlate::PLATE_IXEX_MODE_ID) + { + ret = select_plate(plate_index); + if (!ret) { + PartPlate* curr_plate = p->partplate_list.get_curr_plate(); + // Build ordered mode list: "primary" first, then all named modes. + std::vector modes; + modes.push_back("primary"); + const DynamicPrintConfig& printer_cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + auto* names_opt = printer_cfg.option("ixex_mode_names"); + if (names_opt) { + for (const auto& n : names_opt->values) + if (!n.empty()) modes.push_back(n); + } + if (right_click) { + // Show a popup menu with all modes. + wxMenu menu; + std::string current = curr_plate->get_ixex_mode(); + for (size_t i = 0; i < modes.size(); ++i) { + wxMenuItem* item = menu.AppendRadioItem(wxID_HIGHEST + (int)i, from_u8(modes[i])); + if (modes[i] == current) + item->Check(true); + } + menu.Bind(wxEVT_MENU, [this, curr_plate, &modes](wxCommandEvent& e) { + int idx = e.GetId() - wxID_HIGHEST; + if (idx >= 0 && idx < (int)modes.size()) { + take_snapshot("set ixex mode"); + curr_plate->set_ixex_mode(modes[idx]); + update_project_dirty_from_presets(); + set_plater_dirty(true); + update(); + } + }); + p->view3D->get_canvas3d()->get_wxglcanvas()->PopupMenu(&menu); + ret = 1; // signal to caller: popup was shown, suppress plate context menu + } else { + // Left-click: cycle to next mode. + std::string current = curr_plate->get_ixex_mode(); + auto it = std::find(modes.begin(), modes.end(), current); + size_t next_idx = (it == modes.end()) ? 0 : ((it - modes.begin() + 1) % modes.size()); + std::string next_mode = modes[next_idx]; + if (next_mode != current) { + take_snapshot("set ixex mode"); + curr_plate->set_ixex_mode(next_mode); + update_project_dirty_from_presets(); + set_plater_dirty(true); + update(); + } + } + } else { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "can not select plate %1%" << plate_index; + ret = -1; + } + } else { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "invalid action %1%, with right_click=%2%" << action << right_click; diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 6a3b0a7444..7e02c63114 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -2675,26 +2675,6 @@ void TabPrint::build() optgroup->append_single_option_line("timelapse_type", "others_settings_special_mode#timelapse"); optgroup->append_single_option_line("enable_wrapping_detection"); - // Dynamic combo box — choices come from the printer preset's ixex_mode_names. - // Use create_line_with_widget so the undo bitmaps are set (has_undo_ui()=true), - // ensuring the widget x-position matches other option fields in OG_CustomCtrl. - create_line_with_widget(optgroup.get(), "ixex_parallel_mode", "", [this](wxWindow* parent) -> wxSizer* { - m_ixex_mode_combo = new ComboBox(parent, wxID_ANY, wxEmptyString, - wxDefaultPosition, - wxSize(12 * wxGetApp().em_unit(), -1), - 0, nullptr, wxCB_READONLY); - m_ixex_mode_combo->GetDropDown().SetUseContentWidth(true); - refresh_ixex_mode_combo(); - m_ixex_mode_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { - if (!m_ixex_mode_combo || !m_config) return; - std::string val = m_ixex_mode_combo->GetStringSelection().ToStdString(); - m_config->set_key_value("ixex_parallel_mode", new ConfigOptionString(val)); - on_value_change("ixex_parallel_mode", val); - }); - auto* s = new wxBoxSizer(wxHORIZONTAL); - s->Add(m_ixex_mode_combo, 0, wxALIGN_CENTER_VERTICAL); - return s; - }); optgroup = page->new_optgroup(L("Fuzzy Skin"), L"fuzzy_skin"); optgroup->append_single_option_line("fuzzy_skin", "others_settings_fuzzy_skin"); @@ -2753,36 +2733,6 @@ void TabPrint::reload_config() { this->compatible_widget_reload(m_compatible_printers); Tab::reload_config(); - refresh_ixex_mode_combo(); -} - -void TabPrint::refresh_ixex_mode_combo() -{ - if (!m_ixex_mode_combo) return; - // Rebuild choices: "primary" + names from the active printer preset - m_ixex_mode_combo->Clear(); - m_ixex_mode_combo->Append("primary"); - if (m_preset_bundle) { - auto* names = m_preset_bundle->printers.get_edited_preset().config - .option("ixex_mode_names"); - if (names) { - for (const auto& n : names->values) - if (!n.empty()) m_ixex_mode_combo->Append(wxString::FromUTF8(n)); - } - } - // Restore current selection from process config (safe: option may not exist in older presets) - std::string cur = "primary"; - if (m_config) { - auto* opt = m_config->option("ixex_parallel_mode"); - if (opt) cur = opt->value; - } - if (!m_ixex_mode_combo->SetStringSelection(cur)) { - m_ixex_mode_combo->SetSelection(0); // fall back to "primary" - // The active mode no longer exists — reset config so stale name doesn't crash downstream. - if (m_config) - m_config->set_key_value("ixex_parallel_mode", new ConfigOptionString("primary")); - on_value_change("ixex_parallel_mode", std::string("primary")); - } } void TabPrint::update_description_lines() @@ -2904,7 +2854,6 @@ void TabPrint::clear_pages() m_recommended_thin_wall_thickness_description_line = nullptr; m_top_bottom_shell_thickness_explanation = nullptr; - m_ixex_mode_combo = nullptr; } //BBS: GUI refactor @@ -5060,9 +5009,6 @@ void TabPrinter::build_fff() m_config->set_key_value("ixex_mode_gcodes", new ConfigOptionStrings(gcodes)); update_dirty(); on_value_change("ixex_mode_names", std::string("")); - // Refresh the process tab's mode dropdown immediately - if (auto* print_tab = dynamic_cast(wxGetApp().get_tab(Preset::TYPE_PRINT))) - print_tab->refresh_ixex_mode_combo(); }; auto* sizer = new wxBoxSizer(wxHORIZONTAL); sizer->Add(m_ixex_modes_ctrl, 1, wxEXPAND); diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index 687cfeea73..7d86278db7 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -464,12 +464,10 @@ public: void update() override; void clear_pages() override; bool supports_printer_technology(const PrinterTechnology tech) const override { return tech == ptFFF; } - void refresh_ixex_mode_combo(); private: ogStaticText* m_recommended_thin_wall_thickness_description_line = nullptr; ogStaticText* m_top_bottom_shell_thickness_explanation = nullptr; - ComboBox* m_ixex_mode_combo {nullptr}; }; class TabPrintModel : public TabPrint From f540cf87a88ce88468f12f15f0f826f8ee9a0d3f Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Wed, 8 Apr 2026 23:35:32 -0400 Subject: [PATCH 026/434] fix: preserve iXex tool assignments across gantry count changes - Add all_tool_states map to IXexModesCtrl Row, storing all tool assignments including those not currently visible due to grid size - active_tools_string() now serializes from all_tool_states so off-screen assignments survive the round trip through a smaller grid - Button clicks keep all_tool_states in sync with visible btn_states - Display anchors to the gantry row containing the Primary assignment so reducing gantry count keeps the meaningful row visible rather than always defaulting to row 0 Co-Authored-By: Claude Sonnet 4.6 --- src/slic3r/GUI/Tab.cpp | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 7e02c63114..d85a1278df 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4519,6 +4519,9 @@ private: std::vector btn_states; // parallel to btns // tool_idx for btns[i]: computed during add_row, stored here for quick lookup std::vector btn_tool_idx; + // Full assignment map including tools not currently visible due to grid size. + // Preserved across grid resizes so assignments restore when grid expands again. + std::map all_tool_states; }; void add_row(const std::string& name = "", @@ -4536,6 +4539,7 @@ private: auto* grid_sizer = new wxGridSizer(m_n_rows, m_n_cols, 2, 2); auto tool_states = parse_tool_states(active_tools); + r.all_tool_states = tool_states; // preserve all assignments, including off-screen tools wxPanel* this_panel = r.panel; // Buttons rendered top=rear (high Y), bottom=front (low Y). @@ -4545,11 +4549,21 @@ private: // Display iterates: row from n_rows-1 down to 0 (rear→front = top→bottom). // raw_row = flip_y ? (n_rows-1-row) : row // raw_col = flip_x ? (n_cols-1-col) : col + // + // row_start: anchor displayed rows to the gantry row containing the Primary + // assignment, so reducing gantry count keeps the meaningful row visible. + int primary_gantry_row = 0; + for (const auto& [idx, state] : tool_states) { + if (state == 1) { primary_gantry_row = idx / m_n_cols; break; } + } + int row_start = primary_gantry_row; // display rows [row_start .. row_start+m_n_rows-1] + bool flip_x = (m_layout == 1 || m_layout == 3); bool flip_y = (m_layout == 2 || m_layout == 3); for (int row = m_n_rows - 1; row >= 0; --row) { for (int col = 0; col < m_n_cols; ++col) { int raw_row = flip_y ? (m_n_rows - 1 - row) : row; + raw_row += row_start; // anchor to Primary's gantry row int raw_col = flip_x ? (m_n_cols - 1 - col) : col; int tool_idx = raw_row * m_n_cols + raw_col; int state = 0; @@ -4581,6 +4595,13 @@ private: if (st == 1 && other_primary) st = 2; // skip Primary → go straight to Copy + // Keep all_tool_states in sync so off-screen tools are preserved + int tidx = row_ref.btn_tool_idx[btn_pos]; + if (st == 0) + row_ref.all_tool_states.erase(tidx); + else + row_ref.all_tool_states[tidx] = st; + apply_btn(row_ref.btns[btn_pos], row_ref.btn_tool_idx[btn_pos], st); notify(); break; @@ -4635,11 +4656,13 @@ private: } std::string active_tools_string(const Row& r) const { + // Serialize from all_tool_states (not just visible buttons) so assignments + // for off-screen tools are preserved across grid size changes. std::string s; - for (size_t i = 0; i < r.btn_states.size(); ++i) { - if (r.btn_states[i] == 0) continue; + for (const auto& [idx, state] : r.all_tool_states) { + if (state == 0) continue; if (!s.empty()) s += ','; - s += std::to_string(r.btn_tool_idx[i]) + ':' + state_role(r.btn_states[i]); + s += std::to_string(idx) + ':' + state_role(state); } return s; } From 461c69c83e76f7a92f44a8d9e598bf6ff18c09fa Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Thu, 9 Apr 2026 14:11:06 -0400 Subject: [PATCH 027/434] =?UTF-8?q?refactor:=20rename=20iXex=20=E2=86=92?= =?UTF-8?q?=20IMEX=20throughout;=20use=20IDEX/IQEX=20as=20user-facing=20la?= =?UTF-8?q?bel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - All config keys: ixex_* → imex_* (is_imex, imex_gantry_count, etc.) - All C++ identifiers: IXexModesCtrl → IMEXModesCtrl, m_ixex_* → m_imex_*, etc. - 3MF serialization key: ixex_parallel_mode → imex_parallel_mode - UI strings: iXex → IDEX/IQEX - SVG icons: plate_ixex_mode*.svg → plate_imex_mode*.svg - Remove unused ixex_mode.svg Breaking change for existing printer configs — acceptable pre-merge. Co-Authored-By: Claude Sonnet 4.6 --- resources/images/plate_imex_mode.svg | 10 + resources/images/plate_imex_mode_dark.svg | 10 + resources/images/plate_imex_mode_hover.svg | 10 + .../images/plate_imex_mode_hover_dark.svg | 10 + src/libslic3r/Format/bbs_3mf.cpp | 12 +- src/libslic3r/GCode.cpp | 20 +- src/libslic3r/Preset.cpp | 12 +- src/libslic3r/PrintConfig.cpp | 46 ++-- src/libslic3r/PrintConfig.hpp | 22 +- src/slic3r/GUI/GCodeViewer.cpp | 98 ++++---- src/slic3r/GUI/GCodeViewer.hpp | 6 +- src/slic3r/GUI/Jobs/ArrangeJob.cpp | 10 +- src/slic3r/GUI/PartPlate.cpp | 218 +++++++++--------- src/slic3r/GUI/PartPlate.hpp | 54 ++--- src/slic3r/GUI/Plater.cpp | 24 +- src/slic3r/GUI/Tab.cpp | 144 ++++++------ src/slic3r/GUI/Tab.hpp | 8 +- 17 files changed, 377 insertions(+), 337 deletions(-) create mode 100644 resources/images/plate_imex_mode.svg create mode 100644 resources/images/plate_imex_mode_dark.svg create mode 100644 resources/images/plate_imex_mode_hover.svg create mode 100644 resources/images/plate_imex_mode_hover_dark.svg diff --git a/resources/images/plate_imex_mode.svg b/resources/images/plate_imex_mode.svg new file mode 100644 index 0000000000..30a67cdf35 --- /dev/null +++ b/resources/images/plate_imex_mode.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/images/plate_imex_mode_dark.svg b/resources/images/plate_imex_mode_dark.svg new file mode 100644 index 0000000000..981fd88447 --- /dev/null +++ b/resources/images/plate_imex_mode_dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/images/plate_imex_mode_hover.svg b/resources/images/plate_imex_mode_hover.svg new file mode 100644 index 0000000000..9463e767df --- /dev/null +++ b/resources/images/plate_imex_mode_hover.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/images/plate_imex_mode_hover_dark.svg b/resources/images/plate_imex_mode_hover_dark.svg new file mode 100644 index 0000000000..301fa995ab --- /dev/null +++ b/resources/images/plate_imex_mode_hover_dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index b7513ea240..4fbb00018d 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -333,7 +333,7 @@ static constexpr const char* FIRST_LAYER_PRINT_SEQUENCE_ATTR = "first_layer_prin static constexpr const char* OTHER_LAYERS_PRINT_SEQUENCE_ATTR = "other_layers_print_sequence"; static constexpr const char* OTHER_LAYERS_PRINT_SEQUENCE_NUMS_ATTR = "other_layers_print_sequence_nums"; static constexpr const char* SPIRAL_VASE_MODE = "spiral_mode"; -static constexpr const char* IXEX_PARALLEL_MODE_ATTR = "ixex_parallel_mode"; +static constexpr const char* IMEX_PARALLEL_MODE_ATTR = "imex_parallel_mode"; static constexpr const char* FILAMENT_MAP_MODE_ATTR = "filament_map_mode"; static constexpr const char* FILAMENT_MAP_ATTR = "filament_maps"; static constexpr const char* LIMIT_FILAMENT_MAP_ATTR = "limit_filament_maps"; @@ -4293,8 +4293,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) std::istringstream(value) >> std::boolalpha >> spiral_mode; m_curr_plater->config.set_key_value("spiral_mode", new ConfigOptionBool(spiral_mode)); } - else if (key == IXEX_PARALLEL_MODE_ATTR) { - m_curr_plater->config.set_key_value("ixex_parallel_mode", new ConfigOptionString(value)); + else if (key == IMEX_PARALLEL_MODE_ATTR) { + m_curr_plater->config.set_key_value("imex_parallel_mode", new ConfigOptionString(value)); } else if (key == FILAMENT_MAP_MODE_ATTR) { @@ -7778,9 +7778,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << SPIRAL_VASE_MODE << "\" " << VALUE_ATTR << "=\"" << spiral_mode_opt->getBool() << "\"/>\n"; { - auto* ixex_mode_opt = plate_data->config.option("ixex_parallel_mode"); - if (ixex_mode_opt && !ixex_mode_opt->value.empty() && ixex_mode_opt->value != "primary") - stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << IXEX_PARALLEL_MODE_ATTR << "\" " << VALUE_ATTR << "=\"" << ixex_mode_opt->value << "\"/>\n"; + auto* imex_mode_opt = plate_data->config.option("imex_parallel_mode"); + if (imex_mode_opt && !imex_mode_opt->value.empty() && imex_mode_opt->value != "primary") + stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << IMEX_PARALLEL_MODE_ATTR << "\" " << VALUE_ATTR << "=\"" << imex_mode_opt->value << "\"/>\n"; } //filament map related diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 87a0ee1342..ec249a6ae5 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -3054,18 +3054,18 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.write(m_writer.set_chamber_temperature(max_chamber_temp, true)); // set chamber_temperature } - // iXex: inject mode G-code before machine_start_gcode so the carriage mode + // IDEX/IQEX: inject mode G-code before machine_start_gcode so the carriage mode // macro is active before any motion, homing, or probing in start G-code. - if (print.config().is_ixex.value && !print.objects().empty()) { - const std::string& ixex_mode = print.objects().front()->config().ixex_parallel_mode.value; - if (!ixex_mode.empty() && ixex_mode != "primary") { - const auto& mode_names = print.config().ixex_mode_names.values; - const auto& mode_gcodes = print.config().ixex_mode_gcodes.values; + if (print.config().is_imex.value && !print.objects().empty()) { + const std::string& imex_mode = print.objects().front()->config().imex_parallel_mode.value; + if (!imex_mode.empty() && imex_mode != "primary") { + const auto& mode_names = print.config().imex_mode_names.values; + const auto& mode_gcodes = print.config().imex_mode_gcodes.values; for (size_t i = 0; i < mode_names.size() && i < mode_gcodes.size(); ++i) { - if (mode_names[i] == ixex_mode && !mode_gcodes[i].empty()) { - std::string ixex_gcode = this->placeholder_parser_process( - "ixex_mode_gcode", mode_gcodes[i], initial_extruder_id); - file.writeln(ixex_gcode); + if (mode_names[i] == imex_mode && !mode_gcodes[i].empty()) { + std::string imex_gcode = this->placeholder_parser_process( + "imex_mode_gcode", mode_gcodes[i], initial_extruder_id); + file.writeln(imex_gcode); break; } } diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index adb56b90ac..fc28d8e5ca 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -961,8 +961,8 @@ static std::vector s_Preset_print_options { "enable_wrapping_detection", "seam_slope_type", "seam_slope_conditional", "scarf_angle_threshold", "scarf_joint_speed", "scarf_joint_flow_ratio", "seam_slope_start_height", "seam_slope_entire_loop", "seam_slope_min_length", "seam_slope_steps", "seam_slope_inner_walls", "scarf_overhang_threshold", "interlocking_beam", "interlocking_orientation", "interlocking_beam_layer_count", "interlocking_depth", "interlocking_boundary_avoidance", "interlocking_beam_width","calib_flowrate_topinfill_special_order", - // iXex parallel print mode (per-print selection) - "ixex_parallel_mode", + // IDEX/IQEX parallel print mode (per-print selection) + "imex_parallel_mode", }; static std::vector s_Preset_filament_options {/*"filament_colour", */ "default_filament_colour", "required_nozzle_HRC", "filament_diameter", "pellet_flow_coefficient", "volumetric_speed_coefficients", "filament_type", @@ -1040,10 +1040,10 @@ static std::vector s_Preset_printer_options { "z_offset", "disable_m73", "preferred_orientation", "emit_machine_limits_to_gcode", "pellet_modded_printer", "support_multi_bed_types", "default_bed_type", "bed_mesh_min","bed_mesh_max","bed_mesh_probe_distance", "adaptive_bed_mesh_margin", "enable_long_retraction_when_cut","long_retractions_when_cut","retraction_distances_when_cut", "bed_temperature_formula", "nozzle_flush_dataset", - // iXex (independent X extruder) — printer capability and user-defined modes - "is_ixex", "ixex_gantry_count", "ixex_tools_per_gantry", "ixex_tool_layout", - "ixex_nozzle_clearance_x", "ixex_nozzle_clearance_y", "ixex_carriage_margin", "ixex_viz_theme", - "ixex_mode_names", "ixex_mode_active_tools", "ixex_mode_gcodes" + // IDEX/IQEX (independent X extruder) — printer capability and user-defined modes + "is_imex", "imex_gantry_count", "imex_tools_per_gantry", "imex_tool_layout", + "imex_nozzle_clearance_x", "imex_nozzle_clearance_y", "imex_carriage_margin", "imex_viz_theme", + "imex_mode_names", "imex_mode_active_tools", "imex_mode_gcodes" }; static std::vector s_Preset_sla_print_options { diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 30b2ead516..b4978b1263 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -5487,14 +5487,14 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionBool(true)); - // iXex (independent X extruder) — parallel printing support for IDEX/IQEX printers - def = this->add("is_ixex", coBool); - def->label = L("iXex Printer"); - def->tooltip = L("Enable iXex (independent X extruder) support for IDEX/IQEX printers with multiple independent carriages."); + // IDEX/IQEX (independent X extruder) — parallel printing support for IDEX/IQEX printers + def = this->add("is_imex", coBool); + def->label = L("IDEX/IQEX Printer"); + def->tooltip = L("Enable IDEX/IQEX (independent X extruder) support for IDEX/IQEX printers with multiple independent carriages."); def->mode = comAdvanced; def->set_default_value(new ConfigOptionBool(false)); - def = this->add("ixex_gantry_count", coInt); + def = this->add("imex_gantry_count", coInt); def->label = L("Gantry Count"); def->tooltip = L("Number of independent Y-axis gantries. 1 for IDEX/single-rail IQEX. 2 for dual-gantry systems (divides bed into rows)."); def->min = 1; @@ -5502,7 +5502,7 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionInt(1)); - def = this->add("ixex_tools_per_gantry", coInt); + def = this->add("imex_tools_per_gantry", coInt); def->label = L("Tools per Gantry"); def->tooltip = L("Number of independent toolheads per gantry along X. 2 for IDEX-style."); def->min = 1; @@ -5510,7 +5510,7 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionInt(2)); - def = this->add("ixex_tool_layout", coString); + def = this->add("imex_tool_layout", coString); def->label = L("Tool 0 Corner"); def->tooltip = L("Physical corner of the bed where tool T0 (index 0) is located. " "Determines how tool indices map to bed zones. " @@ -5518,7 +5518,7 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionString("front-left")); - def = this->add("ixex_nozzle_clearance_x", coFloat); + def = this->add("imex_nozzle_clearance_x", coFloat); def->label = L("Nozzle Clearance X"); def->tooltip = L("Distance (mm) from the nozzle to the collision-side carriage edge in X. Used to calculate the width of the collision exclusion strip at each X boundary."); def->sidetext = L("mm"); @@ -5526,7 +5526,7 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(30.0)); - def = this->add("ixex_nozzle_clearance_y", coFloat); + def = this->add("imex_nozzle_clearance_y", coFloat); def->label = L("Nozzle Clearance Y"); def->tooltip = L("Distance (mm) from the nozzle to the collision-side carriage edge in Y. Used to calculate the width of the collision exclusion strip at each Y boundary."); def->sidetext = L("mm"); @@ -5534,7 +5534,7 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(30.0)); - def = this->add("ixex_carriage_margin", coFloat); + def = this->add("imex_carriage_margin", coFloat); def->label = L("Safety Margin"); def->tooltip = L("Non-blocking advisory clearance strip (mm) drawn inside the primary zone at each carriage boundary. Parts placed within this strip will still slice; it is a visual reminder to leave extra clearance near the zone edge."); def->sidetext = L("mm"); @@ -5543,32 +5543,32 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(0.0)); - def = this->add("ixex_viz_theme", coString); + def = this->add("imex_viz_theme", coString); def->label = L("Visualization Theme"); - def->tooltip = L("Color theme for iXex bed zone visualization. Choose a colorblind-friendly theme if needed."); + def->tooltip = L("Color theme for IDEX/IQEX bed zone visualization. Choose a colorblind-friendly theme if needed."); def->mode = comAdvanced; def->set_default_value(new ConfigOptionString("standard")); - def = this->add("ixex_parallel_mode", coString); - def->label = L("iXex Print Mode"); - def->tooltip = L("Name of the active iXex parallel print mode, or \"primary\" for single-carriage printing. Requires iXex Printer enabled in the Printer preset (Printer \u2192 iXex tab)."); + def = this->add("imex_parallel_mode", coString); + def->label = L("IDEX/IQEX Print Mode"); + def->tooltip = L("Name of the active IDEX/IQEX parallel print mode, or \"primary\" for single-carriage printing. Requires IDEX/IQEX Printer enabled in the Printer preset (Printer \u2192 IDEX/IQEX tab)."); def->mode = comAdvanced; def->set_default_value(new ConfigOptionString("primary")); - def = this->add("ixex_mode_names", coStrings); - def->label = L("iXex Mode Names"); - def->tooltip = L("Display names for each user-defined iXex parallel print mode."); + def = this->add("imex_mode_names", coStrings); + def->label = L("IDEX/IQEX Mode Names"); + def->tooltip = L("Display names for each user-defined IDEX/IQEX parallel print mode."); def->mode = comAdvanced; def->set_default_value(new ConfigOptionStrings()); - def = this->add("ixex_mode_active_tools", coStrings); - def->label = L("iXex Mode Active Tools"); - def->tooltip = L("Tool role assignments for each mode. Format: \"idx:P,idx:C,idx:M\" where P=Primary, C=Copy, M=Mirror (e.g. \"0:P,1:C,2:M,3:M\"). Managed by the iXex Modes editor in the Printer preset."); + def = this->add("imex_mode_active_tools", coStrings); + def->label = L("IDEX/IQEX Mode Active Tools"); + def->tooltip = L("Tool role assignments for each mode. Format: \"idx:P,idx:C,idx:M\" where P=Primary, C=Copy, M=Mirror (e.g. \"0:P,1:C,2:M,3:M\"). Managed by the IDEX/IQEX Modes editor in the Printer preset."); def->mode = comAdvanced; def->set_default_value(new ConfigOptionStrings()); - def = this->add("ixex_mode_gcodes", coStrings); - def->label = L("iXex Mode G-codes"); + def = this->add("imex_mode_gcodes", coStrings); + def->label = L("IDEX/IQEX Mode G-codes"); def->tooltip = L("G-code or macro call to inject at print start for each mode."); def->mode = comAdvanced; def->set_default_value(new ConfigOptionStrings()); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index bdc384b023..f0b414e44f 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1035,8 +1035,8 @@ PRINT_CONFIG_CLASS_DEFINE( // Orca: internal use only ((ConfigOptionBool, calib_flowrate_topinfill_special_order)) // ORCA: special flag for flow rate calibration - // iXex parallel print mode (per-print selection, stores mode name or "primary") - ((ConfigOptionString, ixex_parallel_mode)) + // IDEX/IQEX parallel print mode (per-print selection, stores mode name or "primary") + ((ConfigOptionString, imex_parallel_mode)) ) // This object is mapped to Perl as Slic3r::Config::PrintRegion. @@ -1334,15 +1334,15 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionStrings, filament_start_gcode)) ((ConfigOptionBool, single_extruder_multi_material)) ((ConfigOptionBool, manual_filament_change)) - // iXex (independent X extruder) — parallel printing support for IDEX/IQEX printers - ((ConfigOptionBool, is_ixex)) - ((ConfigOptionInt, ixex_gantry_count)) - ((ConfigOptionInt, ixex_tools_per_gantry)) - ((ConfigOptionFloat, ixex_nozzle_clearance_x)) - ((ConfigOptionFloat, ixex_nozzle_clearance_y)) - ((ConfigOptionStrings, ixex_mode_names)) - ((ConfigOptionStrings, ixex_mode_active_tools)) - ((ConfigOptionStrings, ixex_mode_gcodes)) + // IDEX/IQEX (independent X extruder) — parallel printing support for IDEX/IQEX printers + ((ConfigOptionBool, is_imex)) + ((ConfigOptionInt, imex_gantry_count)) + ((ConfigOptionInt, imex_tools_per_gantry)) + ((ConfigOptionFloat, imex_nozzle_clearance_x)) + ((ConfigOptionFloat, imex_nozzle_clearance_y)) + ((ConfigOptionStrings, imex_mode_names)) + ((ConfigOptionStrings, imex_mode_active_tools)) + ((ConfigOptionStrings, imex_mode_gcodes)) ((ConfigOptionBool, single_extruder_multi_material_priming)) ((ConfigOptionBool, wipe_tower_no_sparse_layers)) ((ConfigOptionString, change_filament_gcode)) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 5163580c30..f64d9f5e4b 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -913,8 +913,8 @@ void GCodeViewer::SequentialView::render(const bool has_render_path, float legen // marker.set_world_offset(current_offset); marker.render(canvas_width, canvas_height, view_type); marker.render_position_window(viewer, canvas_width, canvas_height, view_type); - // iXex secondary carriage markers - for (auto& sec : m_ixex_secondary_markers) + // IDEX/IQEX secondary carriage markers + for (auto& sec : m_imex_secondary_markers) sec.render(canvas_width, canvas_height, view_type); } @@ -1511,7 +1511,7 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) m_sequential_view.marker.set_world_position(libvgcode::convert(curr_vertex.position)); m_sequential_view.marker.set_z_offset(m_z_offset + 0.5f); - // iXex: compute all carriage positions; update secondary nozzle markers; + // IDEX/IQEX: compute all carriage positions; update secondary nozzle markers; // carriage_box_draws is populated for toolhead footprint rendering after sequential_view.render(). // Okabe-Ito colorblind-safe palette — excludes orange (#E69F00) and sky blue (#56B4E9) // which are used by the bed zone fills, ensuring the markers contrast against the background. @@ -1523,41 +1523,41 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) }}; struct CarriageDraw { Vec3f pos; ColorRGBA color; float box_offset_x = 0.0f; float box_offset_y = 0.0f; }; std::vector carriage_box_draws; - float ixex_box_wx = 0.0f, ixex_box_wy = 0.0f; + float imex_box_wx = 0.0f, imex_box_wy = 0.0f; { PresetBundle* preset_bundle = wxGetApp().preset_bundle; - bool ixex_active = false; + bool imex_active = false; if (preset_bundle && m_sequential_view.m_show_marker) { const DynamicPrintConfig& printer_cfg = preset_bundle->printers.get_edited_preset().config; - auto* is_ixex_opt = printer_cfg.opt("is_ixex"); - if (is_ixex_opt && is_ixex_opt->value) { + auto* is_imex_opt = printer_cfg.opt("is_imex"); + if (is_imex_opt && is_imex_opt->value) { const DynamicPrintConfig& process_cfg = preset_bundle->prints.get_edited_preset().config; - auto* mode_opt = process_cfg.opt("ixex_parallel_mode"); + auto* mode_opt = process_cfg.opt("imex_parallel_mode"); std::string mode = mode_opt ? mode_opt->value : "primary"; // Per-plate mode overrides the process preset. if (auto* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate()) { - std::string plate_mode = plate->get_ixex_mode(); + std::string plate_mode = plate->get_imex_mode(); if (plate_mode != "primary") mode = plate_mode; } - if (mode != m_ixex_last_mode) { - m_sequential_view.m_ixex_secondary_markers.clear(); - m_ixex_last_mode = mode; + if (mode != m_imex_last_mode) { + m_sequential_view.m_imex_secondary_markers.clear(); + m_imex_last_mode = mode; } if (!mode.empty() && mode != "primary") { - auto* mode_names_opt = printer_cfg.opt("ixex_mode_names"); - auto* active_tools_opt = printer_cfg.opt("ixex_mode_active_tools"); + auto* mode_names_opt = printer_cfg.opt("imex_mode_names"); + auto* active_tools_opt = printer_cfg.opt("imex_mode_active_tools"); - auto* tpg_opt = printer_cfg.opt("ixex_tools_per_gantry"); - auto* wx_opt = printer_cfg.opt("ixex_nozzle_clearance_x"); - auto* wy_opt = printer_cfg.opt("ixex_nozzle_clearance_y"); + auto* tpg_opt = printer_cfg.opt("imex_tools_per_gantry"); + auto* wx_opt = printer_cfg.opt("imex_nozzle_clearance_x"); + auto* wy_opt = printer_cfg.opt("imex_nozzle_clearance_y"); int tools_per_gantry = tpg_opt ? std::max(1, tpg_opt->value) : 1; - ixex_box_wx = wx_opt ? (float)wx_opt->value : 30.0f; - ixex_box_wy = wy_opt ? (float)wy_opt->value : 30.0f; + imex_box_wx = wx_opt ? (float)wx_opt->value : 30.0f; + imex_box_wy = wy_opt ? (float)wy_opt->value : 30.0f; - // Parse "idx:P/C/M" format — matches PartPlate::calc_ixex_zones() exactly. + // Parse "idx:P/C/M" format — matches PartPlate::calc_imex_zones() exactly. // Primary = state 1 (P), Copy = 2 (C), Mirror = 3 (M). // Inactive tools (state 0) are not included in the stored string. int pri_tool = -1; @@ -1594,21 +1594,21 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) int sec_count = (int)sec_tool_ids.size(); if (sec_count > 0) { - ixex_active = true; + imex_active = true; // Lazy init secondary nozzle markers - if ((int)m_sequential_view.m_ixex_secondary_markers.size() != sec_count) { - m_sequential_view.m_ixex_secondary_markers.resize(sec_count); + if ((int)m_sequential_view.m_imex_secondary_markers.size() != sec_count) { + m_sequential_view.m_imex_secondary_markers.resize(sec_count); for (int i = 0; i < sec_count; ++i) { - m_sequential_view.m_ixex_secondary_markers[i].init(m_marker_filename); - m_sequential_view.m_ixex_secondary_markers[i].set_color( + m_sequential_view.m_imex_secondary_markers[i].init(m_marker_filename); + m_sequential_view.m_imex_secondary_markers[i].set_color( s_carriage_colors[(i + 1) % s_carriage_colors.size()]); } } // Bed X and Y bounds — read from the current plate's shape, which is // in world/GL coordinates (same space as curr_vertex.position), and is - // the exact same source used by PartPlate::calc_ixex_zones(). + // the exact same source used by PartPlate::calc_imex_zones(). float bed_x_min, bed_x_max, bed_y_min, bed_y_max; { PartPlate* curr_plate = wxGetApp().plater()->get_partplate_list().get_curr_plate(); @@ -1633,12 +1633,12 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) } } - auto* gc_opt = printer_cfg.opt("ixex_gantry_count"); + auto* gc_opt = printer_cfg.opt("imex_gantry_count"); int gantry_count = gc_opt ? std::max(1, gc_opt->value) : 1; - // Apply the same flip logic as PartPlate::calc_ixex_zones() so physical + // Apply the same flip logic as PartPlate::calc_imex_zones() so physical // grid positions match the bed zone visualization. - auto* layout_opt = printer_cfg.opt("ixex_tool_layout"); + auto* layout_opt = printer_cfg.opt("imex_tool_layout"); const std::string layout_str = layout_opt ? layout_opt->value : "front-left"; const bool flip_x = (layout_str == "front-right" || layout_str == "rear-right"); const bool flip_y = (layout_str == "rear-left" || layout_str == "rear-right"); @@ -1673,15 +1673,15 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) // Primary carriage box: nozzle sits at the collision-side edge. // Default to centered; override once we know which side the secondaries are on. - float pri_box_offset_x = (pri_phys_col == 0) ? 0.0f : -ixex_box_wx; // nozzle at inner edge (facing center of bed) - float pri_box_offset_y = -ixex_box_wy; // nozzle at high-Y edge by default (gantry always behind nozzle); override if secondary is behind + float pri_box_offset_x = (pri_phys_col == 0) ? 0.0f : -imex_box_wx; // nozzle at inner edge (facing center of bed) + float pri_box_offset_y = -imex_box_wy; // nozzle at high-Y edge by default (gantry always behind nozzle); override if secondary is behind for (int i = 0; i < sec_count; ++i) { int sc = phys_col_of(sec_tool_ids[i]); int sr = phys_row_of(sec_tool_ids[i]); if (sc > pri_phys_col) { pri_box_offset_x = 0.0f; } - else if (sc < pri_phys_col) { pri_box_offset_x = -ixex_box_wx; } + else if (sc < pri_phys_col) { pri_box_offset_x = -imex_box_wx; } if (sr > pri_phys_row) { pri_box_offset_y = 0.0f; } - else if (sr < pri_phys_row) { pri_box_offset_y = -ixex_box_wy; } + else if (sr < pri_phys_row) { pri_box_offset_y = -imex_box_wy; } } carriage_box_draws.push_back({ prim_pos, s_carriage_colors[0], pri_box_offset_x, pri_box_offset_y }); @@ -1719,8 +1719,8 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) } Vec3f sec_pos{ sec_x, sec_y, prim_pos.z() }; - m_sequential_view.m_ixex_secondary_markers[i].set_world_position(sec_pos); - m_sequential_view.m_ixex_secondary_markers[i].set_z_offset(m_z_offset + 0.5f); + m_sequential_view.m_imex_secondary_markers[i].set_world_position(sec_pos); + m_sequential_view.m_imex_secondary_markers[i].set_z_offset(m_z_offset + 0.5f); // X: copy tools move in sync with primary so they share the same // X-facing orientation. Mirror tools approach from the opposite side // so the nozzle sits at the collision-side edge. @@ -1728,16 +1728,16 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) if (sec_state == 2) { sec_box_offset_x = pri_box_offset_x; } else { - if (sec_phys_col > pri_phys_col) sec_box_offset_x = -ixex_box_wx; + if (sec_phys_col > pri_phys_col) sec_box_offset_x = -imex_box_wx; else if (sec_phys_col < pri_phys_col) sec_box_offset_x = 0.0f; else sec_box_offset_x = pri_box_offset_x; } // Y: always row-based — gantry is always behind nozzle regardless of // copy/mirror (a back-row copy still has its gantry at the back). float sec_box_offset_y; - if (sec_phys_row > pri_phys_row) sec_box_offset_y = -ixex_box_wy; + if (sec_phys_row > pri_phys_row) sec_box_offset_y = -imex_box_wy; else if (sec_phys_row < pri_phys_row) sec_box_offset_y = 0.0f; - else sec_box_offset_y = -ixex_box_wy; + else sec_box_offset_y = -imex_box_wy; carriage_box_draws.push_back({ sec_pos, s_carriage_colors[(i + 1) % s_carriage_colors.size()], sec_box_offset_x, sec_box_offset_y }); @@ -1746,28 +1746,28 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) } } } - if (!ixex_active) - m_sequential_view.m_ixex_secondary_markers.clear(); + if (!imex_active) + m_sequential_view.m_imex_secondary_markers.clear(); } // BBS fixed buttom margin. m_moves_slider.pos_y m_sequential_view.render(!m_no_render_path, legend_height, &m_viewer, m_viewer.get_current_vertex().gcode_id, canvas_width, canvas_height - bottom_margin * m_scale, right_margin * m_scale, m_viewer.get_view_type()); - // iXex: render toolhead footprint boxes for each active carriage. - // Each box is ixex_nozzle_clearance_x × ixex_nozzle_clearance_y, sitting above the nozzle tip. - if (!carriage_box_draws.empty() && ixex_box_wx > 0.0f && ixex_box_wy > 0.0f) { + // IDEX/IQEX: render toolhead footprint boxes for each active carriage. + // Each box is imex_nozzle_clearance_x × imex_nozzle_clearance_y, sitting above the nozzle tip. + if (!carriage_box_draws.empty() && imex_box_wx > 0.0f && imex_box_wy > 0.0f) { // Rebuild box mesh every frame — dimensions can change via config edit without a // G-code reload, so dimension-based caching isn't safe. // Mesh origin: nozzle at x=0, centered in Y, Z starts at nozzle tip level. // Per-carriage box_offset_x shifts the mesh left or right so the nozzle lands // at the correct (collision-side) edge. { - const float box_h = std::max(ixex_box_wx, ixex_box_wy); - indexed_triangle_set its = its_make_cube((double)ixex_box_wx, (double)ixex_box_wy, (double)box_h); + const float box_h = std::max(imex_box_wx, imex_box_wy); + indexed_triangle_set its = its_make_cube((double)imex_box_wx, (double)imex_box_wy, (double)box_h); // No vertex pre-shifting — box_offset_x/y in the per-carriage transform // positions the nozzle at the correct collision-side edge. - m_ixex_toolhead_box.reset(); - m_ixex_toolhead_box.init_from(its); + m_imex_toolhead_box.reset(); + m_imex_toolhead_box.init_from(its); } GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); @@ -1787,8 +1787,8 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); shader->set_uniform("view_normal_matrix", view_normal_matrix); - m_ixex_toolhead_box.set_color(draw.color); - m_ixex_toolhead_box.render(); + m_imex_toolhead_box.set_color(draw.color); + m_imex_toolhead_box.render(); } shader->stop_using(); glsafe(::glDisable(GL_BLEND)); diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index ee3c1e1ec7..a8e5607048 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -150,7 +150,7 @@ public: }; Marker marker; - std::vector m_ixex_secondary_markers; // one per active secondary carriage in iXex mode + std::vector m_imex_secondary_markers; // one per active secondary carriage in IDEX/IQEX mode GCodeWindow gcode_window; float m_scale = 1.0; bool m_show_marker = false; @@ -205,8 +205,8 @@ private: ConfigOptionMode m_user_mode; bool m_fold = {false}; std::string m_marker_filename; // cached for lazy secondary marker init - std::string m_ixex_last_mode; // detect mode changes for secondary marker rebuild - GLModel m_ixex_toolhead_box; // shared box mesh for all carriage footprint overlays + std::string m_imex_last_mode; // detect mode changes for secondary marker rebuild + GLModel m_imex_toolhead_box; // shared box mesh for all carriage footprint overlays size_t m_extruders_count; std::vector m_filament_diameters; diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.cpp b/src/slic3r/GUI/Jobs/ArrangeJob.cpp index b4ec570d00..218889a2f7 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.cpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.cpp @@ -540,13 +540,13 @@ void ArrangeJob::process(Ctl &ctl) Points bedpts = get_shrink_bedpts(m_plater->config(),params); - // When an iXex parallel mode is active, constrain auto-arrange to the primary zone only + // When an IDEX/IQEX parallel mode is active, constrain auto-arrange to the primary zone only // and treat carriage collision strips as hard excluded regions. - // NOTE: m_ixex_primary_zone_box and ixex_collision_zones() are in global (world) coordinates + // NOTE: m_imex_primary_zone_box and imex_collision_zones() are in global (world) coordinates // because they are derived from m_shape which includes the plate origin offset. The arranger // always works in plate-local space (origin = 0,0), so we subtract the plate origin here. if (PartPlate* curr_plate = partplate_list.get_curr_plate()) { - if (auto pz = curr_plate->ixex_primary_zone()) { + if (auto pz = curr_plate->imex_primary_zone()) { Vec3d plate_origin = curr_plate->get_origin(); double ox = plate_origin.x(), oy = plate_origin.y(); BoundingBoxf pz_local(Vec2d(pz->min.x() - ox, pz->min.y() - oy), @@ -558,7 +558,7 @@ void ArrangeJob::process(Ctl &ctl) { scaled_pz.max.x(), scaled_pz.max.y() }, { scaled_pz.min.x(), scaled_pz.max.y() }, }; - for (const BoundingBoxf3& cz : curr_plate->ixex_collision_zones()) { + for (const BoundingBoxf3& cz : curr_plate->imex_collision_zones()) { Polygon poly({ { scaled(cz.min.x() - ox), scaled(cz.min.y() - oy) }, { scaled(cz.max.x() - ox), scaled(cz.min.y() - oy) }, @@ -572,7 +572,7 @@ void ArrangeJob::process(Ctl &ctl) ap.is_virt_object = true; ap.bed_idx = current_plate_index; ap.height = 1; - ap.name = "IXexCollisionZone"; + ap.name = "IMEXCollisionZone"; m_unselected.emplace_back(std::move(ap)); } } diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 88d3b4cf63..17447a6707 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -365,32 +365,32 @@ std::vector PartPlate::get_plate_wrapping_detection_area() const return std::vector(); } -std::string PartPlate::get_ixex_mode() const +std::string PartPlate::get_imex_mode() const { - if (m_config.has("ixex_parallel_mode")) { - auto* opt = m_config.option("ixex_parallel_mode"); + if (m_config.has("imex_parallel_mode")) { + auto* opt = m_config.option("imex_parallel_mode"); if (opt && !opt->value.empty()) return opt->value; } return "primary"; } -void PartPlate::set_ixex_mode(const std::string& mode) +void PartPlate::set_imex_mode(const std::string& mode) { if (mode.empty() || mode == "primary") { - m_config.erase("ixex_parallel_mode"); + m_config.erase("imex_parallel_mode"); } else { - m_config.set_key_value("ixex_parallel_mode", new ConfigOptionString(mode)); + m_config.set_key_value("imex_parallel_mode", new ConfigOptionString(mode)); } update_slice_result_valid_state(false); - m_ixex_zones_mode_cache = "\x01"; // force zone rebuild + m_imex_zones_mode_cache = "\x01"; // force zone rebuild } -void PartPlate::reset_ixex_mode() +void PartPlate::reset_imex_mode() { - m_config.erase("ixex_parallel_mode"); + m_config.erase("imex_parallel_mode"); update_slice_result_valid_state(false); - m_ixex_zones_mode_cache = "\x01"; + m_imex_zones_mode_cache = "\x01"; } void PartPlate::set_spiral_vase_mode(bool spiral_mode, bool as_global) @@ -484,30 +484,30 @@ void PartPlate::calc_exclude_triangles(const ExPolygon &poly) // Forward declaration (defined later in this file) static bool init_model_from_lines(GLModel &model, const Lines &lines, float z); -void PartPlate::calc_ixex_zones() +void PartPlate::calc_imex_zones() { - m_ixex_copy_zones.clear(); - m_ixex_mirror_zones.clear(); + m_imex_copy_zones.clear(); + m_imex_mirror_zones.clear(); // Clear blocking vectors at the top so early returns don't leave stale data. - m_ixex_secondary_zone_boxes.clear(); - m_ixex_collision_zones.clear(); - m_ixex_collision_overlay.clear(); - m_ixex_margin_overlay.clear(); - m_ixex_primary_zone_box = std::nullopt; + m_imex_secondary_zone_boxes.clear(); + m_imex_collision_zones.clear(); + m_imex_collision_overlay.clear(); + m_imex_margin_overlay.clear(); + m_imex_primary_zone_box = std::nullopt; if (!wxGetApp().preset_bundle) return; const DynamicPrintConfig& printer_cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; - auto* is_ixex_opt = printer_cfg.option("is_ixex"); - if (!is_ixex_opt || !is_ixex_opt->value) + auto* is_imex_opt = printer_cfg.option("is_imex"); + if (!is_imex_opt || !is_imex_opt->value) return; // Per-plate mode takes priority over the process preset. - std::string active_mode = get_ixex_mode(); + std::string active_mode = get_imex_mode(); if (active_mode == "primary") { const DynamicPrintConfig& process_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; - auto* mode_opt = process_cfg.option("ixex_parallel_mode"); + auto* mode_opt = process_cfg.option("imex_parallel_mode"); if (mode_opt && !mode_opt->value.empty()) active_mode = mode_opt->value; } @@ -515,9 +515,9 @@ void PartPlate::calc_ixex_zones() return; // Grid dimensions and tool layout from printer config - auto* gantry_opt = printer_cfg.option("ixex_gantry_count"); - auto* tpg_opt = printer_cfg.option("ixex_tools_per_gantry"); - auto* layout_opt = printer_cfg.option("ixex_tool_layout"); + auto* gantry_opt = printer_cfg.option("imex_gantry_count"); + auto* tpg_opt = printer_cfg.option("imex_tools_per_gantry"); + auto* layout_opt = printer_cfg.option("imex_tool_layout"); int n_cols = tpg_opt ? std::max(1, tpg_opt->value) : 2; int n_rows = gantry_opt ? std::max(1, gantry_opt->value) : 1; @@ -541,8 +541,8 @@ void PartPlate::calc_ixex_zones() return; // nothing to dim with a single zone // Look up secondary tool indices (active in mode, but NOT the primary tool) - auto* names_opt = printer_cfg.option("ixex_mode_names"); - auto* tools_opt = printer_cfg.option("ixex_mode_active_tools"); + auto* names_opt = printer_cfg.option("imex_mode_names"); + auto* tools_opt = printer_cfg.option("imex_mode_active_tools"); std::string active_tools_str; if (names_opt && tools_opt) { for (size_t i = 0; i < names_opt->values.size(); ++i) { @@ -668,18 +668,18 @@ void PartPlate::calc_ixex_zones() for (const auto& b : make_boxes(cells)) push_zone_fill(vec, b.x0, b.x1, b.y0, b.y1); }; - push_expanded_fills(m_ixex_copy_zones, copy_cells); - push_expanded_fills(m_ixex_mirror_zones, mirror_cells); + push_expanded_fills(m_imex_copy_zones, copy_cells); + push_expanded_fills(m_imex_mirror_zones, mirror_cells); // --- Secondary zone blocking --- // Collect the expanded bounding boxes for all secondary (copy+mirror) zones. // check_outside() uses these to prevent objects being placed outside the primary zone. for (const auto& b : make_boxes(copy_cells)) - m_ixex_secondary_zone_boxes.push_back( + m_imex_secondary_zone_boxes.push_back( BoundingBoxf3(Vec3d(b.x0, b.y0, -1.0), Vec3d(b.x1, b.y1, 1e4))); for (const auto& b : make_boxes(mirror_cells)) - m_ixex_secondary_zone_boxes.push_back( + m_imex_secondary_zone_boxes.push_back( BoundingBoxf3(Vec3d(b.x0, b.y0, -1.0), Vec3d(b.x1, b.y1, 1e4))); // --- Carriage collision danger strips --- @@ -696,9 +696,9 @@ void PartPlate::calc_ixex_zones() // !has_row_sep → full bed height; has_row_sep → primary row only // !has_col_sep → full bed width; has_col_sep → primary column only - auto* cw_opt = printer_cfg.option("ixex_nozzle_clearance_x"); - auto* ch_opt = printer_cfg.option("ixex_nozzle_clearance_y"); - auto* mgn_opt = printer_cfg.option("ixex_carriage_margin"); + auto* cw_opt = printer_cfg.option("imex_nozzle_clearance_x"); + auto* ch_opt = printer_cfg.option("imex_nozzle_clearance_y"); + auto* mgn_opt = printer_cfg.option("imex_carriage_margin"); double carriage_w = cw_opt ? cw_opt->value : 0.0; double carriage_h = ch_opt ? ch_opt->value : 0.0; double margin = mgn_opt ? mgn_opt->value : 0.0; @@ -714,7 +714,7 @@ void PartPlate::calc_ixex_zones() if (!clipped.empty()) { GLModel m; if (init_model_from_poly(m, clipped.front(), GROUND_Z)) - m_ixex_margin_overlay.push_back(std::move(m)); + m_imex_margin_overlay.push_back(std::move(m)); } }; @@ -723,14 +723,14 @@ void PartPlate::calc_ixex_zones() double pz_x1 = has_col_sep ? x_min + (pri_col + 1) * zone_w : x_max; double pz_y0 = has_row_sep ? y_min + pri_row * zone_h : y_min; double pz_y1 = has_row_sep ? y_min + (pri_row + 1) * zone_h : y_max; - m_ixex_primary_zone_box = BoundingBoxf(Vec2d(pz_x0, pz_y0), Vec2d(pz_x1, pz_y1)); + m_imex_primary_zone_box = BoundingBoxf(Vec2d(pz_x0, pz_y0), Vec2d(pz_x1, pz_y1)); // Helper: build a BoundingBoxf3 strip, clip to bed, add to members auto add_strip = [&](double sx0, double sx1, double sy0, double sy1) { BoundingBoxf3 box; box.min = Vec3d(sx0, sy0, -1.0); box.max = Vec3d(sx1, sy1, 1e4); - m_ixex_collision_zones.push_back(box); + m_imex_collision_zones.push_back(box); ExPolygon cell; cell.contour.append({ scale_(sx0), scale_(sy0) }); @@ -741,7 +741,7 @@ void PartPlate::calc_ixex_zones() if (!clipped.empty()) { GLModel m; if (init_model_from_poly(m, clipped.front(), GROUND_Z)) - m_ixex_collision_overlay.push_back(std::move(m)); + m_imex_collision_overlay.push_back(std::move(m)); } }; @@ -799,29 +799,29 @@ void PartPlate::calc_ixex_zones() } } -// Build a cache key from the current iXex config options, or "" if iXex is off. +// Build a cache key from the current IDEX/IQEX config options, or "" if IDEX/IQEX is off. // Reads per-plate mode from m_config first, falling back to the process preset. -std::string PartPlate::build_ixex_cache_key() const +std::string PartPlate::build_imex_cache_key() const { if (!wxGetApp().preset_bundle) return ""; const DynamicPrintConfig& printer_cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; - auto* is_ixex_opt = printer_cfg.option("is_ixex"); - if (!is_ixex_opt || !is_ixex_opt->value) + auto* is_imex_opt = printer_cfg.option("is_imex"); + if (!is_imex_opt || !is_imex_opt->value) return ""; // Per-plate mode takes priority over process preset. - std::string active_mode = get_ixex_mode(); + std::string active_mode = get_imex_mode(); if (active_mode == "primary") { const DynamicPrintConfig& process_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; - auto* mode_opt = process_cfg.option("ixex_parallel_mode"); + auto* mode_opt = process_cfg.option("imex_parallel_mode"); if (mode_opt && !mode_opt->value.empty()) active_mode = mode_opt->value; } - auto* n_col_opt = printer_cfg.option("ixex_tools_per_gantry"); - auto* n_row_opt = printer_cfg.option("ixex_gantry_count"); - auto* cw_opt = printer_cfg.option("ixex_nozzle_clearance_x"); - auto* ch_opt = printer_cfg.option("ixex_nozzle_clearance_y"); - auto* mgn_opt = printer_cfg.option("ixex_carriage_margin"); + auto* n_col_opt = printer_cfg.option("imex_tools_per_gantry"); + auto* n_row_opt = printer_cfg.option("imex_gantry_count"); + auto* cw_opt = printer_cfg.option("imex_nozzle_clearance_x"); + auto* ch_opt = printer_cfg.option("imex_nozzle_clearance_y"); + auto* mgn_opt = printer_cfg.option("imex_carriage_margin"); return active_mode + "|" + std::to_string(n_col_opt ? n_col_opt->value : 2) + "x" + std::to_string(n_row_opt ? n_row_opt->value : 1) @@ -832,19 +832,19 @@ std::string PartPlate::build_ixex_cache_key() const // Ensure zone geometry (secondary boxes, collision strips, visual meshes) is up to date. // Called before any collision check AND before rendering so both paths share the same data. -void PartPlate::ensure_ixex_zones() +void PartPlate::ensure_imex_zones() { - std::string cache_key = build_ixex_cache_key(); - if (cache_key != m_ixex_zones_mode_cache) { - m_ixex_zones_mode_cache = cache_key; - calc_ixex_zones(); + std::string cache_key = build_imex_cache_key(); + if (cache_key != m_imex_zones_mode_cache) { + m_imex_zones_mode_cache = cache_key; + calc_imex_zones(); } } -bool PartPlate::has_ixex_placement_violations() +bool PartPlate::has_imex_placement_violations() { - ensure_ixex_zones(); - if (m_ixex_secondary_zone_boxes.empty() && m_ixex_collision_zones.empty()) + ensure_imex_zones(); + if (m_imex_secondary_zone_boxes.empty() && m_imex_collision_zones.empty()) return false; for (const auto& pr : obj_to_instance_set) { int obj_id = pr.first; @@ -855,11 +855,11 @@ bool PartPlate::has_ixex_placement_violations() Polygon hull = instance->convex_hull_2d(); if (hull.points.empty()) continue; - for (const auto& box : m_ixex_secondary_zone_boxes) { + for (const auto& box : m_imex_secondary_zone_boxes) { if (!intersection({box.polygon(true)}, {hull}).empty()) return true; } - for (const auto& strip : m_ixex_collision_zones) { + for (const auto& strip : m_imex_collision_zones) { if (!intersection({strip.polygon(true)}, {hull}).empty()) return true; } @@ -867,15 +867,15 @@ bool PartPlate::has_ixex_placement_violations() return false; } -void PartPlate::render_ixex_zones(bool force_default_color) +void PartPlate::render_imex_zones(bool force_default_color) { if (force_default_color) return; - ensure_ixex_zones(); + ensure_imex_zones(); // Read visualization theme from printer config. - struct IXexTheme { + struct IMEXTheme { ColorRGBA copy; // secondary copy zone fill ColorRGBA mirror; // secondary mirror zone fill ColorRGBA danger; // blocking collision strip @@ -883,7 +883,7 @@ void PartPlate::render_ixex_zones(bool force_default_color) }; // Standard (Okabe-Ito orange + sky blue) - static const IXexTheme k_standard = { + static const IMEXTheme k_standard = { { 0.337f, 0.706f, 0.914f, 0.45f }, // copy — sky blue #56B4E9 { 0.902f, 0.624f, 0.000f, 0.45f }, // mirror — orange #E69F00 { 0.850f, 0.100f, 0.100f, 0.55f }, // danger — red @@ -891,7 +891,7 @@ void PartPlate::render_ixex_zones(bool force_default_color) }; // Deuteranopia / Protanopia: avoids red-green confusion. // Danger strip uses strong blue-violet (red invisible to protanopes). - static const IXexTheme k_deuteranopia = { + static const IMEXTheme k_deuteranopia = { { 0.000f, 0.447f, 0.698f, 0.50f }, // copy — blue #0072B2 { 0.941f, 0.894f, 0.259f, 0.50f }, // mirror — yellow #F0E442 { 0.200f, 0.100f, 0.800f, 0.65f }, // danger — blue-violet (red not visible) @@ -899,24 +899,24 @@ void PartPlate::render_ixex_zones(bool force_default_color) }; // Tritanopia: avoids blue-yellow confusion. // Copy uses vermilion, mirror uses reddish-purple, margin uses teal. - static const IXexTheme k_tritanopia = { + static const IMEXTheme k_tritanopia = { { 0.835f, 0.369f, 0.000f, 0.50f }, // copy — vermilion #D55E00 { 0.800f, 0.475f, 0.655f, 0.50f }, // mirror — reddish purple #CC79A7 { 0.850f, 0.100f, 0.100f, 0.55f }, // danger — red (visible to tritanopes) { 0.000f, 0.700f, 0.600f, 0.45f }, // margin — teal }; // High contrast: saturated, higher alpha for low-vision users. - static const IXexTheme k_high_contrast = { + static const IMEXTheme k_high_contrast = { { 0.000f, 0.600f, 1.000f, 0.65f }, // copy — vivid blue { 1.000f, 0.600f, 0.000f, 0.65f }, // mirror — vivid orange { 1.000f, 0.000f, 0.000f, 0.75f }, // danger — full red { 0.000f, 1.000f, 0.200f, 0.60f }, // margin — bright green }; - const IXexTheme* theme = &k_standard; + const IMEXTheme* theme = &k_standard; if (wxGetApp().preset_bundle) { const DynamicPrintConfig& pcfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; - if (auto* t = pcfg.option("ixex_viz_theme")) { + if (auto* t = pcfg.option("imex_viz_theme")) { if (t->value == "deuteranopia") theme = &k_deuteranopia; else if (t->value == "tritanopia") theme = &k_tritanopia; else if (t->value == "high_contrast") theme = &k_high_contrast; @@ -926,27 +926,27 @@ void PartPlate::render_ixex_zones(bool force_default_color) // Both fills and border lines use the flat shader already active from render(). glsafe(::glDepthMask(GL_FALSE)); - if (!m_ixex_copy_zones.empty()) - for (GLModel& z : m_ixex_copy_zones) + if (!m_imex_copy_zones.empty()) + for (GLModel& z : m_imex_copy_zones) if (z.is_initialized()) { z.set_color(theme->copy); z.render(); } - if (!m_ixex_mirror_zones.empty()) - for (GLModel& z : m_ixex_mirror_zones) + if (!m_imex_mirror_zones.empty()) + for (GLModel& z : m_imex_mirror_zones) if (z.is_initialized()) { z.set_color(theme->mirror); z.render(); } glsafe(::glDepthMask(GL_TRUE)); - if (!m_ixex_collision_overlay.empty()) { + if (!m_imex_collision_overlay.empty()) { glsafe(::glDepthMask(GL_FALSE)); - for (GLModel& s : m_ixex_collision_overlay) + for (GLModel& s : m_imex_collision_overlay) if (s.is_initialized()) { s.set_color(theme->danger); s.render(); } glsafe(::glDepthMask(GL_TRUE)); } - if (!m_ixex_margin_overlay.empty()) { + if (!m_imex_margin_overlay.empty()) { glsafe(::glDepthMask(GL_FALSE)); - for (GLModel& b : m_ixex_margin_overlay) + for (GLModel& b : m_imex_margin_overlay) if (b.is_initialized()) { b.set_color(theme->margin); b.render(); } glsafe(::glDepthMask(GL_TRUE)); } @@ -1683,17 +1683,17 @@ void PartPlate::render_icons(bool bottom, bool only_name, int hover_id) } else render_icon_texture(m_move_front_icon.model, m_partplate_list->m_move_front_texture); - // iXex mode icon — only when is_ixex is active + // IDEX/IQEX mode icon — only when is_imex is active { PresetBundle* pb = wxGetApp().preset_bundle; - auto* is_ixex_opt = pb ? pb->printers.get_edited_preset().config.option("is_ixex") : nullptr; - if (is_ixex_opt && is_ixex_opt->value) { - if (hover_id == (int)PLATE_IXEX_MODE_ID) { - render_icon_texture(m_ixex_mode_icon.model, m_partplate_list->m_ixex_mode_hovered_texture); - std::string cur = get_ixex_mode(); - show_tooltip(_u8L("iXex mode: ") + cur + _u8L(" (left-click to cycle, right-click for menu)")); + auto* is_imex_opt = pb ? pb->printers.get_edited_preset().config.option("is_imex") : nullptr; + if (is_imex_opt && is_imex_opt->value) { + if (hover_id == (int)PLATE_IMEX_MODE_ID) { + render_icon_texture(m_imex_mode_icon.model, m_partplate_list->m_imex_mode_hovered_texture); + std::string cur = get_imex_mode(); + show_tooltip(_u8L("IDEX/IQEX mode: ") + cur + _u8L(" (left-click to cycle, right-click for menu)")); } else { - render_icon_texture(m_ixex_mode_icon.model, m_partplate_list->m_ixex_mode_texture); + render_icon_texture(m_imex_mode_icon.model, m_partplate_list->m_imex_mode_texture); } } } @@ -2007,11 +2007,11 @@ void PartPlate::register_raycasters_for_picking(GLCanvas3D &canvas) if (dual_bbl) register_model_for_picking(canvas, m_plate_filament_map_icon, picking_id_component(PLATE_FILAMENT_MAP_ID)); - // Register iXex mode icon only when iXex is active on the current printer preset. + // Register IDEX/IQEX mode icon only when IDEX/IQEX is active on the current printer preset. if (preset) { - auto* is_ixex_opt = preset->printers.get_edited_preset().config.option("is_ixex"); - if (is_ixex_opt && is_ixex_opt->value) - register_model_for_picking(canvas, m_ixex_mode_icon, picking_id_component(PLATE_IXEX_MODE_ID)); + auto* is_imex_opt = preset->printers.get_edited_preset().config.option("is_imex"); + if (is_imex_opt && is_imex_opt->value) + register_model_for_picking(canvas, m_imex_mode_icon, picking_id_component(PLATE_IMEX_MODE_ID)); } } @@ -3024,8 +3024,8 @@ bool PartPlate::contain_instance_totally(int obj_id, int instance_id) const //check whether instance is outside the plate or not bool PartPlate::check_outside(int obj_id, int instance_id, BoundingBoxf3* bounding_box) { - // Ensure iXex zone geometry is current before any placement check. - ensure_ixex_zones(); + // Ensure IDEX/IQEX zone geometry is current before any placement check. + ensure_imex_zones(); bool outside = true; @@ -3070,14 +3070,14 @@ bool PartPlate::check_outside(int obj_id, int instance_id, BoundingBoxf3* boundi outside = false; } - // iXex placement check (prepare-mode bounding-box test). + // IDEX/IQEX placement check (prepare-mode bounding-box test). // Block objects that overlap secondary (copy/mirror) zones or the carriage // danger strip at the primary zone boundary. Reuses the existing // outside=true → instance_outside_set → update_states() → blocks slicing path. - if (!outside && (!m_ixex_secondary_zone_boxes.empty() || !m_ixex_collision_zones.empty())) { + if (!outside && (!m_imex_secondary_zone_boxes.empty() || !m_imex_collision_zones.empty())) { Polygon obj_hull = instance->convex_hull_2d(); // scaled Clipper coords // 1. Object must not touch any secondary zone. - for (const auto& box : m_ixex_secondary_zone_boxes) { + for (const auto& box : m_imex_secondary_zone_boxes) { Polygon p = box.polygon(true); if (!intersection({ p }, { obj_hull }).empty()) { outside = true; @@ -3086,7 +3086,7 @@ bool PartPlate::check_outside(int obj_id, int instance_id, BoundingBoxf3* boundi } // 2. Object must not enter the carriage danger strip inside the primary zone. if (!outside) { - for (const auto& strip : m_ixex_collision_zones) { + for (const auto& strip : m_imex_collision_zones) { Polygon strip_poly = strip.polygon(true); if (!intersection({ strip_poly }, { obj_hull }).empty()) { outside = true; @@ -3684,8 +3684,8 @@ bool PartPlate::set_shape(const Pointfs& shape, const Pointfs& exclude_areas, co const BoundingBox& pp_bbox = poly.contour.bounding_box(); calc_gridlines(poly, pp_bbox); - m_ixex_zones_mode_cache = "\x01"; // force rebuild on next render - calc_ixex_zones(); + m_imex_zones_mode_cache = "\x01"; // force rebuild on next render + calc_imex_zones(); calc_vertex_for_icons(0, m_del_icon); calc_vertex_for_icons(1, m_orient_icon); @@ -3699,9 +3699,9 @@ bool PartPlate::set_shape(const Pointfs& shape, const Pointfs& exclude_areas, co calc_vertex_for_icons(dual_bbl ? 5 : 6, m_plate_filament_map_icon); calc_vertex_for_icons(dual_bbl ? 6 : 5, m_move_front_icon); { - auto* is_ixex_opt = preset->printers.get_edited_preset().config.option("is_ixex"); - if (is_ixex_opt && is_ixex_opt->value) - calc_vertex_for_icons(dual_bbl ? 7 : 6, m_ixex_mode_icon); + auto* is_imex_opt = preset->printers.get_edited_preset().config.option("is_imex"); + if (is_imex_opt && is_imex_opt->value) + calc_vertex_for_icons(dual_bbl ? 7 : 6, m_imex_mode_icon); } calc_vertex_for_number(0, false, m_plate_idx_icon); @@ -3795,7 +3795,7 @@ void PartPlate::render(const Transform3d& view_matrix, const Transform3d& projec render_background(force_background_color); render_exclude_area(force_background_color); - render_ixex_zones(force_background_color); + render_imex_zones(force_background_color); if(m_selected && wxGetApp().plater()->get_enable_wrapping_detection()){ if(!m_wrapping_detection_triangles.is_initialized()){ auto points = get_plate_wrapping_detection_area(); @@ -4572,17 +4572,17 @@ void PartPlateList::generate_icon_textures() } } - // iXex mode icon textures (fall back gracefully if SVG not present yet) + // IDEX/IQEX mode icon textures (fall back gracefully if SVG not present yet) { - file_name = path + (m_is_dark ? "plate_ixex_mode_dark.svg" : "plate_ixex_mode.svg"); - if (!m_ixex_mode_texture.load_from_svg_file(file_name, true, false, false, icon_size)) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":load file %1% failed (iXex mode icon)") % file_name; + file_name = path + (m_is_dark ? "plate_imex_mode_dark.svg" : "plate_imex_mode.svg"); + if (!m_imex_mode_texture.load_from_svg_file(file_name, true, false, false, icon_size)) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":load file %1% failed (IDEX/IQEX mode icon)") % file_name; } } { - file_name = path + (m_is_dark ? "plate_ixex_mode_hover_dark.svg" : "plate_ixex_mode_hover.svg"); - if (!m_ixex_mode_hovered_texture.load_from_svg_file(file_name, true, false, false, icon_size)) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":load file %1% failed (iXex mode hover icon)") % file_name; + file_name = path + (m_is_dark ? "plate_imex_mode_hover_dark.svg" : "plate_imex_mode_hover.svg"); + if (!m_imex_mode_hovered_texture.load_from_svg_file(file_name, true, false, false, icon_size)) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":load file %1% failed (IDEX/IQEX mode hover icon)") % file_name; } } @@ -4629,8 +4629,8 @@ void PartPlateList::release_icon_textures() m_plate_set_filament_map_hovered_texture.reset(); m_plate_name_edit_texture.reset(); m_plate_name_edit_hovered_texture.reset(); - m_ixex_mode_texture.reset(); - m_ixex_mode_hovered_texture.reset(); + m_imex_mode_texture.reset(); + m_imex_mode_hovered_texture.reset(); for (int i = 0;i < MAX_PLATE_COUNT; i++) { m_idx_textures[i].reset(); } diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index 773524c77c..423de28585 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -136,14 +136,14 @@ private: GLModel m_logo_triangles; GLModel m_gridlines; GLModel m_gridlines_bolder; - std::vector m_ixex_copy_zones; // blue tint (Wong #56B4E9) - std::vector m_ixex_mirror_zones; // orange tint (Wong #E69F00) - std::vector m_ixex_secondary_zone_boxes; // secondary (copy/mirror) zone rects — objects blocked here - std::vector m_ixex_collision_zones; // carriage danger strips inside primary zone (mm) - std::vector m_ixex_collision_overlay; // red-orange rendered fill for danger strips - std::vector m_ixex_margin_overlay; // amber advisory bands just inside collision strips - std::optional m_ixex_primary_zone_box; // primary zone extents in mm; empty when iXex is off/primary-only - std::string m_ixex_zones_mode_cache{ "\x01" }; // cached mode+topology key; sentinel = unbuilt + std::vector m_imex_copy_zones; // blue tint (Wong #56B4E9) + std::vector m_imex_mirror_zones; // orange tint (Wong #E69F00) + std::vector m_imex_secondary_zone_boxes; // secondary (copy/mirror) zone rects — objects blocked here + std::vector m_imex_collision_zones; // carriage danger strips inside primary zone (mm) + std::vector m_imex_collision_overlay; // red-orange rendered fill for danger strips + std::vector m_imex_margin_overlay; // amber advisory bands just inside collision strips + std::optional m_imex_primary_zone_box; // primary zone extents in mm; empty when IDEX/IQEX is off/primary-only + std::string m_imex_zones_mode_cache{ "\x01" }; // cached mode+topology key; sentinel = unbuilt GLModel m_height_limit_common; GLModel m_height_limit_bottom; GLModel m_height_limit_top; @@ -155,7 +155,7 @@ private: PickingModel m_plate_filament_map_icon; PickingModel m_plate_name_edit_icon; PickingModel m_move_front_icon; - PickingModel m_ixex_mode_icon; + PickingModel m_imex_mode_icon; GLModel m_plate_idx_icon; GLTexture m_texture; @@ -187,10 +187,10 @@ private: void calc_triangles_from_polygon(const ExPolygon &poly, GLModel& render_model); void calc_gridlines(const ExPolygon& poly, const BoundingBox& pp_bbox); void calc_height_limit(); - void calc_ixex_zones(); - void ensure_ixex_zones(); - std::string build_ixex_cache_key() const; - void render_ixex_zones(bool force_default_color); + void calc_imex_zones(); + void ensure_imex_zones(); + std::string build_imex_cache_key() const; + void render_imex_zones(bool force_default_color); void calc_vertex_for_number(int index, bool one_number, GLModel &buffer); void calc_vertex_for_plate_name_edit_icon(GLTexture *texture, int index, PickingModel &model); void calc_vertex_for_icons(int index, PickingModel &model); @@ -222,7 +222,7 @@ private: public: static constexpr unsigned int PLATE_NAME_HOVER_ID = 6; static constexpr unsigned int PLATE_FILAMENT_MAP_ID = 8; - static constexpr unsigned int PLATE_IXEX_MODE_ID = 9; + static constexpr unsigned int PLATE_IMEX_MODE_ID = 9; static constexpr unsigned int GRABBER_COUNT = 10; static ColorRGBA SELECT_COLOR; @@ -283,9 +283,9 @@ public: bool get_spiral_vase_mode() const; void set_spiral_vase_mode(bool spiral_mode, bool as_global); - std::string get_ixex_mode() const; - void set_ixex_mode(const std::string& mode); - void reset_ixex_mode(); + std::string get_imex_mode() const; + void set_imex_mode(const std::string& mode); + void reset_imex_mode(); std::vector get_plate_wrapping_detection_area() const; @@ -451,14 +451,14 @@ public: { return m_ready_for_slice && !m_apply_invalid; } - // Returns true if any instance on this plate overlaps an iXex secondary or collision zone. - bool has_ixex_placement_violations(); - // Returns the primary-zone bounding box in mm when an iXex parallel mode is active. - // Empty (nullopt) when iXex is off or the mode is "primary" (full-bed). - std::optional ixex_primary_zone() { ensure_ixex_zones(); return m_ixex_primary_zone_box; } - // Returns carriage collision strips (mm) for the current iXex mode. - // Always call ixex_primary_zone() first to ensure the cache is warm. - const std::vector& ixex_collision_zones() const { return m_ixex_collision_zones; } + // Returns true if any instance on this plate overlaps an IDEX/IQEX secondary or collision zone. + bool has_imex_placement_violations(); + // Returns the primary-zone bounding box in mm when an IDEX/IQEX parallel mode is active. + // Empty (nullopt) when IDEX/IQEX is off or the mode is "primary" (full-bed). + std::optional imex_primary_zone() { ensure_imex_zones(); return m_imex_primary_zone_box; } + // Returns carriage collision strips (mm) for the current IDEX/IQEX mode. + // Always call imex_primary_zone() first to ensure the cache is warm. + const std::vector& imex_collision_zones() const { return m_imex_collision_zones; } void update_slice_ready_status(bool ready_slice) { m_ready_for_slice = ready_slice; @@ -634,8 +634,8 @@ class PartPlateList : public ObjectBase GLTexture m_plate_set_filament_map_hovered_texture; GLTexture m_plate_name_edit_texture; GLTexture m_plate_name_edit_hovered_texture; - GLTexture m_ixex_mode_texture; - GLTexture m_ixex_mode_hovered_texture; + GLTexture m_imex_mode_texture; + GLTexture m_imex_mode_hovered_texture; GLTexture m_idx_textures[MAX_PLATE_COUNT]; // set render option bool render_bedtype_logo = true; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 5c85f75f45..6e5753c546 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -7722,13 +7722,13 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool q->post_process_string_object_exception(err); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": validate err=%1%, warning=%2%")%err.string%warning.string; - // iXex placement check: if objects overlap secondary zones, treat it as a + // IDEX/IQEX placement check: if objects overlap secondary zones, treat it as a // validation error so the standard pathway handles button state, notifications, // and auto-slice blocking consistently. if (err.string.empty()) { - PartPlate* ixex_plate = partplate_list.get_curr_plate(); - if (ixex_plate && ixex_plate->has_ixex_placement_violations()) - err.string = _u8L("Cannot slice: objects are in secondary zones reserved for iXex parallel printing."); + PartPlate* imex_plate = partplate_list.get_curr_plate(); + if (imex_plate && imex_plate->has_imex_placement_violations()) + err.string = _u8L("Cannot slice: objects are in secondary zones reserved for IDEX/IQEX parallel printing."); } if (err.string.empty()) { @@ -17640,7 +17640,7 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi update(); p->partplate_list.select_plate(0); } - else if (action == (int)PartPlate::PLATE_IXEX_MODE_ID) + else if (action == (int)PartPlate::PLATE_IMEX_MODE_ID) { ret = select_plate(plate_index); if (!ret) { @@ -17649,7 +17649,7 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi std::vector modes; modes.push_back("primary"); const DynamicPrintConfig& printer_cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; - auto* names_opt = printer_cfg.option("ixex_mode_names"); + auto* names_opt = printer_cfg.option("imex_mode_names"); if (names_opt) { for (const auto& n : names_opt->values) if (!n.empty()) modes.push_back(n); @@ -17658,7 +17658,7 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi if (right_click) { // Show a popup menu with all modes. wxMenu menu; - std::string current = curr_plate->get_ixex_mode(); + std::string current = curr_plate->get_imex_mode(); for (size_t i = 0; i < modes.size(); ++i) { wxMenuItem* item = menu.AppendRadioItem(wxID_HIGHEST + (int)i, from_u8(modes[i])); if (modes[i] == current) @@ -17667,8 +17667,8 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi menu.Bind(wxEVT_MENU, [this, curr_plate, &modes](wxCommandEvent& e) { int idx = e.GetId() - wxID_HIGHEST; if (idx >= 0 && idx < (int)modes.size()) { - take_snapshot("set ixex mode"); - curr_plate->set_ixex_mode(modes[idx]); + take_snapshot("set imex mode"); + curr_plate->set_imex_mode(modes[idx]); update_project_dirty_from_presets(); set_plater_dirty(true); update(); @@ -17678,13 +17678,13 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi ret = 1; // signal to caller: popup was shown, suppress plate context menu } else { // Left-click: cycle to next mode. - std::string current = curr_plate->get_ixex_mode(); + std::string current = curr_plate->get_imex_mode(); auto it = std::find(modes.begin(), modes.end(), current); size_t next_idx = (it == modes.end()) ? 0 : ((it - modes.begin() + 1) % modes.size()); std::string next_mode = modes[next_idx]; if (next_mode != current) { - take_snapshot("set ixex mode"); - curr_plate->set_ixex_mode(next_mode); + take_snapshot("set imex mode"); + curr_plate->set_imex_mode(next_mode); update_project_dirty_from_presets(); set_plater_dirty(true); update(); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index a048597a4d..cbc95a328b 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4338,7 +4338,7 @@ void TabPrinter::build() } // --------------------------------------------------------------------------- -// IXexModesCtrl — visual parallel-mode editor for the iXex printer tab +// IMEXModesCtrl — visual parallel-mode editor for the IDEX/IQEX printer tab // // Tool button states: // 0 = Inactive (grey) — not participating in this mode @@ -4348,7 +4348,7 @@ void TabPrinter::build() // // Active-tools string format: "idx:P,idx:C,idx:M" (backwards-compat: plain "idx" = Primary) // --------------------------------------------------------------------------- -class IXexModesCtrl : public wxPanel { +class IMEXModesCtrl : public wxPanel { public: std::function on_change; @@ -4360,7 +4360,7 @@ public: return 0; // "front-left" default } - IXexModesCtrl(wxWindow* parent, int n_cols, int n_rows, int layout = 0) + IMEXModesCtrl(wxWindow* parent, int n_cols, int n_rows, int layout = 0) : wxPanel(parent, wxID_ANY), m_n_cols(std::max(1, n_cols)), m_n_rows(std::max(1, n_rows)), m_layout(layout) { m_outer = new wxBoxSizer(wxVERTICAL); @@ -4437,9 +4437,9 @@ public: void load_from_config(const DynamicPrintConfig& cfg) { clear_rows(); - auto* names = cfg.option("ixex_mode_names"); - auto* tools = cfg.option("ixex_mode_active_tools"); - auto* gcodes = cfg.option("ixex_mode_gcodes"); + auto* names = cfg.option("imex_mode_names"); + auto* tools = cfg.option("imex_mode_active_tools"); + auto* gcodes = cfg.option("imex_mode_gcodes"); size_t n = names ? names->values.size() : 0; for (size_t i = 0; i < n; ++i) add_row(names->values[i], @@ -4931,14 +4931,14 @@ void TabPrinter::build_fff() option.opt.height = gcode_field_height;//150; optgroup->append_single_option_line(option, "printer_machine_gcode#template-custom-g-code"); - page = add_options_page(L("iXex"), "custom-gcode_multi_material"); // ORCA: icon only visible on placeholders + page = add_options_page(L("IDEX/IQEX"), "custom-gcode_multi_material"); // ORCA: icon only visible on placeholders optgroup = page->new_optgroup(L("Carriage Configuration"), L"param_advanced"); - optgroup->append_single_option_line("is_ixex"); - optgroup->append_single_option_line("ixex_gantry_count"); - optgroup->append_single_option_line("ixex_tools_per_gantry"); + optgroup->append_single_option_line("is_imex"); + optgroup->append_single_option_line("imex_gantry_count"); + optgroup->append_single_option_line("imex_tools_per_gantry"); { // Tool 0 corner selector - auto opt = optgroup->get_option("ixex_tool_layout"); + auto opt = optgroup->get_option("imex_tool_layout"); auto line = Line{ opt.opt.label, opt.opt.tooltip }; line.append_option(opt); line.widget = [this](wxWindow* parent) -> wxSizer* { @@ -4947,37 +4947,37 @@ void TabPrinter::build_fff() _L("Rear-left"), _L("Rear-right") }; static const char* values[] = { "front-left", "front-right", "rear-left", "rear-right" }; - m_ixex_layout_combo = new ComboBox(parent, wxID_ANY, wxEmptyString, + m_imex_layout_combo = new ComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(12 * wxGetApp().em_unit(), -1), 4, choices, wxCB_READONLY); - m_ixex_layout_combo->GetDropDown().SetUseContentWidth(true); + m_imex_layout_combo->GetDropDown().SetUseContentWidth(true); // Set current selection std::string cur = "front-left"; - if (auto* o = m_config->option("ixex_tool_layout")) cur = o->value; + if (auto* o = m_config->option("imex_tool_layout")) cur = o->value; for (int i = 0; i < 4; ++i) - if (cur == values[i]) { m_ixex_layout_combo->SetSelection(i); break; } - m_ixex_layout_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { + if (cur == values[i]) { m_imex_layout_combo->SetSelection(i); break; } + m_imex_layout_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { static const char* layout_vals[] = { "front-left", "front-right", "rear-left", "rear-right" }; - int sel = m_ixex_layout_combo->GetSelection(); + int sel = m_imex_layout_combo->GetSelection(); std::string val = (sel >= 0 && sel < 4) ? layout_vals[sel] : "front-left"; - m_config->set_key_value("ixex_tool_layout", new ConfigOptionString(val)); - on_value_change("ixex_tool_layout", val); - if (m_ixex_modes_ctrl) - m_ixex_modes_ctrl->set_grid_size( - m_config->opt_int("ixex_tools_per_gantry"), - m_config->opt_int("ixex_gantry_count"), - IXexModesCtrl::parse_layout(val)); + m_config->set_key_value("imex_tool_layout", new ConfigOptionString(val)); + on_value_change("imex_tool_layout", val); + if (m_imex_modes_ctrl) + m_imex_modes_ctrl->set_grid_size( + m_config->opt_int("imex_tools_per_gantry"), + m_config->opt_int("imex_gantry_count"), + IMEXModesCtrl::parse_layout(val)); }); auto* s = new wxBoxSizer(wxHORIZONTAL); - s->Add(m_ixex_layout_combo, 0, wxALIGN_CENTER_VERTICAL); + s->Add(m_imex_layout_combo, 0, wxALIGN_CENTER_VERTICAL); return s; }; optgroup->append_line(line); } - optgroup->append_single_option_line("ixex_nozzle_clearance_x"); - optgroup->append_single_option_line("ixex_nozzle_clearance_y"); - optgroup->append_single_option_line("ixex_carriage_margin"); + optgroup->append_single_option_line("imex_nozzle_clearance_x"); + optgroup->append_single_option_line("imex_nozzle_clearance_y"); + optgroup->append_single_option_line("imex_carriage_margin"); { static const wxString theme_choices[] = { @@ -4986,27 +4986,27 @@ void TabPrinter::build_fff() _L("Tritanopia (blue-yellow)"), _L("High Contrast") }; static const char* theme_vals[] = { "standard", "deuteranopia", "tritanopia", "high_contrast" }; - auto opt = optgroup->get_option("ixex_viz_theme"); + auto opt = optgroup->get_option("imex_viz_theme"); auto line = Line{ opt.opt.label, opt.opt.tooltip }; line.append_option(opt); line.widget = [this](wxWindow* parent) -> wxSizer* { - m_ixex_theme_combo = new ComboBox(parent, wxID_ANY, wxEmptyString, + m_imex_theme_combo = new ComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(22 * wxGetApp().em_unit(), -1), 4, theme_choices, wxCB_READONLY); - m_ixex_theme_combo->GetDropDown().SetUseContentWidth(true); + m_imex_theme_combo->GetDropDown().SetUseContentWidth(true); std::string cur = "standard"; - if (auto* o = m_config->option("ixex_viz_theme")) cur = o->value; + if (auto* o = m_config->option("imex_viz_theme")) cur = o->value; for (int i = 0; i < 4; ++i) - if (cur == theme_vals[i]) { m_ixex_theme_combo->SetSelection(i); break; } - m_ixex_theme_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { - int sel = m_ixex_theme_combo->GetSelection(); + if (cur == theme_vals[i]) { m_imex_theme_combo->SetSelection(i); break; } + m_imex_theme_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { + int sel = m_imex_theme_combo->GetSelection(); std::string val = (sel >= 0 && sel < 4) ? theme_vals[sel] : "standard"; - m_config->set_key_value("ixex_viz_theme", new ConfigOptionString(val)); - on_value_change("ixex_viz_theme", val); + m_config->set_key_value("imex_viz_theme", new ConfigOptionString(val)); + on_value_change("imex_viz_theme", val); }); auto* s = new wxBoxSizer(wxHORIZONTAL); - s->Add(m_ixex_theme_combo, 0, wxALIGN_CENTER_VERTICAL); + s->Add(m_imex_theme_combo, 0, wxALIGN_CENTER_VERTICAL); return s; }; optgroup->append_line(line); @@ -5018,23 +5018,23 @@ void TabPrinter::build_fff() auto line = Line{ L("Modes"), L("") }; line.full_width = 1; line.widget = [this](wxWindow* parent) -> wxSizer* { - int n_cols = m_config->opt_int("ixex_tools_per_gantry"); - int n_rows = m_config->opt_int("ixex_gantry_count"); + int n_cols = m_config->opt_int("imex_tools_per_gantry"); + int n_rows = m_config->opt_int("imex_gantry_count"); std::string layout_str = "front-left"; - if (auto* o = m_config->option("ixex_tool_layout")) layout_str = o->value; - int layout = IXexModesCtrl::parse_layout(layout_str); - m_ixex_modes_ctrl = new IXexModesCtrl(parent, n_cols, n_rows, layout); - m_ixex_modes_ctrl->load_from_config(*m_config); - m_ixex_modes_ctrl->on_change = [this]() { - auto [names, tools, gcodes] = m_ixex_modes_ctrl->get_mode_data(); - m_config->set_key_value("ixex_mode_names", new ConfigOptionStrings(names)); - m_config->set_key_value("ixex_mode_active_tools", new ConfigOptionStrings(tools)); - m_config->set_key_value("ixex_mode_gcodes", new ConfigOptionStrings(gcodes)); + if (auto* o = m_config->option("imex_tool_layout")) layout_str = o->value; + int layout = IMEXModesCtrl::parse_layout(layout_str); + m_imex_modes_ctrl = new IMEXModesCtrl(parent, n_cols, n_rows, layout); + m_imex_modes_ctrl->load_from_config(*m_config); + m_imex_modes_ctrl->on_change = [this]() { + auto [names, tools, gcodes] = m_imex_modes_ctrl->get_mode_data(); + m_config->set_key_value("imex_mode_names", new ConfigOptionStrings(names)); + m_config->set_key_value("imex_mode_active_tools", new ConfigOptionStrings(tools)); + m_config->set_key_value("imex_mode_gcodes", new ConfigOptionStrings(gcodes)); update_dirty(); - on_value_change("ixex_mode_names", std::string("")); + on_value_change("imex_mode_names", std::string("")); }; auto* sizer = new wxBoxSizer(wxHORIZONTAL); - sizer->Add(m_ixex_modes_ctrl, 1, wxEXPAND); + sizer->Add(m_imex_modes_ctrl, 1, wxEXPAND); return sizer; }; modes_og->append_line(line); @@ -5622,28 +5622,28 @@ void TabPrinter::reload_config() if (m_config) { // Sync layout combo - if (m_ixex_layout_combo) { + if (m_imex_layout_combo) { static const char* layout_vals[] = { "front-left", "front-right", "rear-left", "rear-right" }; std::string cur = "front-left"; - if (auto* o = m_config->option("ixex_tool_layout")) cur = o->value; + if (auto* o = m_config->option("imex_tool_layout")) cur = o->value; for (int i = 0; i < 4; ++i) - if (cur == layout_vals[i]) { m_ixex_layout_combo->SetSelection(i); break; } + if (cur == layout_vals[i]) { m_imex_layout_combo->SetSelection(i); break; } } // Sync theme combo - if (m_ixex_theme_combo) { + if (m_imex_theme_combo) { static const char* theme_vals[] = { "standard", "deuteranopia", "tritanopia", "high_contrast" }; std::string cur = "standard"; - if (auto* o = m_config->option("ixex_viz_theme")) cur = o->value; + if (auto* o = m_config->option("imex_viz_theme")) cur = o->value; for (int i = 0; i < 4; ++i) - if (cur == theme_vals[i]) { m_ixex_theme_combo->SetSelection(i); break; } + if (cur == theme_vals[i]) { m_imex_theme_combo->SetSelection(i); break; } } - if (m_ixex_modes_ctrl) { - int n_cols = m_config->opt_int("ixex_tools_per_gantry"); - int n_rows = m_config->opt_int("ixex_gantry_count"); + if (m_imex_modes_ctrl) { + int n_cols = m_config->opt_int("imex_tools_per_gantry"); + int n_rows = m_config->opt_int("imex_gantry_count"); std::string layout_str = "front-left"; - if (auto* o = m_config->option("ixex_tool_layout")) layout_str = o->value; - m_ixex_modes_ctrl->set_grid_size(n_cols, n_rows, IXexModesCtrl::parse_layout(layout_str)); - m_ixex_modes_ctrl->load_from_config(*m_config); + if (auto* o = m_config->option("imex_tool_layout")) layout_str = o->value; + m_imex_modes_ctrl->set_grid_size(n_cols, n_rows, IMEXModesCtrl::parse_layout(layout_str)); + m_imex_modes_ctrl->load_from_config(*m_config); } } } @@ -5662,9 +5662,9 @@ void TabPrinter::clear_pages() { Tab::clear_pages(); m_reset_to_filament_color = nullptr; - m_ixex_modes_ctrl = nullptr; - m_ixex_layout_combo = nullptr; - m_ixex_theme_combo = nullptr; + m_imex_modes_ctrl = nullptr; + m_imex_layout_combo = nullptr; + m_imex_theme_combo = nullptr; } void TabPrinter::toggle_options() @@ -5874,13 +5874,13 @@ void TabPrinter::update_fff() m_use_silent_mode = m_config->opt_bool("silent_mode"); } - // Sync iXex tool grid whenever carriage configuration changes. + // Sync IDEX/IQEX tool grid whenever carriage configuration changes. // set_grid_size() is a no-op when dimensions and layout are unchanged. - if (m_ixex_modes_ctrl) { - int n_cols = m_config->opt_int("ixex_tools_per_gantry"); - int n_rows = m_config->opt_int("ixex_gantry_count"); - std::string layout_str = m_config->opt_string("ixex_tool_layout"); - m_ixex_modes_ctrl->set_grid_size(n_cols, n_rows, IXexModesCtrl::parse_layout(layout_str)); + if (m_imex_modes_ctrl) { + int n_cols = m_config->opt_int("imex_tools_per_gantry"); + int n_rows = m_config->opt_int("imex_gantry_count"); + std::string layout_str = m_config->opt_string("imex_tool_layout"); + m_imex_modes_ctrl->set_grid_size(n_cols, n_rows, IMEXModesCtrl::parse_layout(layout_str)); } toggle_options(); diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index 7d86278db7..c76f6b5f0d 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -589,7 +589,7 @@ public: void set_custom_gcode(const t_config_option_key& opt_key, const std::string& value) override; }; -class IXexModesCtrl; +class IMEXModesCtrl; class TabPrinter : public Tab { @@ -605,9 +605,9 @@ private: std::vector m_pages_sla; wxBoxSizer* m_presets_sizer {nullptr}; - IXexModesCtrl* m_ixex_modes_ctrl {nullptr}; - ComboBox* m_ixex_layout_combo {nullptr}; - ComboBox* m_ixex_theme_combo {nullptr}; + IMEXModesCtrl* m_imex_modes_ctrl {nullptr}; + ComboBox* m_imex_layout_combo {nullptr}; + ComboBox* m_imex_theme_combo {nullptr}; public: ScalableButton* m_reset_to_filament_color = nullptr; From 0d73cd13ba4e2aeee751c1faeb8cf8a571964ac3 Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Thu, 9 Apr 2026 14:36:54 -0400 Subject: [PATCH 028/434] refactor: move IDEX/IQEX settings from standalone tab into Multimaterial section - Remove standalone IDEX/IQEX printer tab - Add IDEX/IQEX Configuration and Parallel Modes sections to the Multimaterial page, gated behind is_imex - Carriage config options hidden via toggle_options() when is_imex is off - Custom widgets (layout combo, theme combo, modes ctrl) show/hide accordingly Co-Authored-By: Claude Sonnet 4.6 --- src/slic3r/GUI/Tab.cpp | 224 +++++++++++++++++++++-------------------- 1 file changed, 115 insertions(+), 109 deletions(-) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index cbc95a328b..001bd5f305 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4931,115 +4931,6 @@ void TabPrinter::build_fff() option.opt.height = gcode_field_height;//150; optgroup->append_single_option_line(option, "printer_machine_gcode#template-custom-g-code"); - page = add_options_page(L("IDEX/IQEX"), "custom-gcode_multi_material"); // ORCA: icon only visible on placeholders - optgroup = page->new_optgroup(L("Carriage Configuration"), L"param_advanced"); - optgroup->append_single_option_line("is_imex"); - optgroup->append_single_option_line("imex_gantry_count"); - optgroup->append_single_option_line("imex_tools_per_gantry"); - { - // Tool 0 corner selector - auto opt = optgroup->get_option("imex_tool_layout"); - auto line = Line{ opt.opt.label, opt.opt.tooltip }; - line.append_option(opt); - line.widget = [this](wxWindow* parent) -> wxSizer* { - static const wxString choices[] = { - _L("Front-left"), _L("Front-right"), - _L("Rear-left"), _L("Rear-right") - }; - static const char* values[] = { "front-left", "front-right", "rear-left", "rear-right" }; - m_imex_layout_combo = new ComboBox(parent, wxID_ANY, wxEmptyString, - wxDefaultPosition, - wxSize(12 * wxGetApp().em_unit(), -1), - 4, choices, wxCB_READONLY); - m_imex_layout_combo->GetDropDown().SetUseContentWidth(true); - // Set current selection - std::string cur = "front-left"; - if (auto* o = m_config->option("imex_tool_layout")) cur = o->value; - for (int i = 0; i < 4; ++i) - if (cur == values[i]) { m_imex_layout_combo->SetSelection(i); break; } - m_imex_layout_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { - static const char* layout_vals[] = { "front-left", "front-right", "rear-left", "rear-right" }; - int sel = m_imex_layout_combo->GetSelection(); - std::string val = (sel >= 0 && sel < 4) ? layout_vals[sel] : "front-left"; - m_config->set_key_value("imex_tool_layout", new ConfigOptionString(val)); - on_value_change("imex_tool_layout", val); - if (m_imex_modes_ctrl) - m_imex_modes_ctrl->set_grid_size( - m_config->opt_int("imex_tools_per_gantry"), - m_config->opt_int("imex_gantry_count"), - IMEXModesCtrl::parse_layout(val)); - }); - auto* s = new wxBoxSizer(wxHORIZONTAL); - s->Add(m_imex_layout_combo, 0, wxALIGN_CENTER_VERTICAL); - return s; - }; - optgroup->append_line(line); - } - optgroup->append_single_option_line("imex_nozzle_clearance_x"); - optgroup->append_single_option_line("imex_nozzle_clearance_y"); - optgroup->append_single_option_line("imex_carriage_margin"); - - { - static const wxString theme_choices[] = { - _L("Standard"), - _L("Deuteranopia / Protanopia (red-green)"), - _L("Tritanopia (blue-yellow)"), - _L("High Contrast") }; - static const char* theme_vals[] = { "standard", "deuteranopia", "tritanopia", "high_contrast" }; - auto opt = optgroup->get_option("imex_viz_theme"); - auto line = Line{ opt.opt.label, opt.opt.tooltip }; - line.append_option(opt); - line.widget = [this](wxWindow* parent) -> wxSizer* { - m_imex_theme_combo = new ComboBox(parent, wxID_ANY, wxEmptyString, - wxDefaultPosition, - wxSize(22 * wxGetApp().em_unit(), -1), - 4, theme_choices, wxCB_READONLY); - m_imex_theme_combo->GetDropDown().SetUseContentWidth(true); - std::string cur = "standard"; - if (auto* o = m_config->option("imex_viz_theme")) cur = o->value; - for (int i = 0; i < 4; ++i) - if (cur == theme_vals[i]) { m_imex_theme_combo->SetSelection(i); break; } - m_imex_theme_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { - int sel = m_imex_theme_combo->GetSelection(); - std::string val = (sel >= 0 && sel < 4) ? theme_vals[sel] : "standard"; - m_config->set_key_value("imex_viz_theme", new ConfigOptionString(val)); - on_value_change("imex_viz_theme", val); - }); - auto* s = new wxBoxSizer(wxHORIZONTAL); - s->Add(m_imex_theme_combo, 0, wxALIGN_CENTER_VERTICAL); - return s; - }; - optgroup->append_line(line); - } - - { - // Parallel modes panel: visual toggle-grid list with + button - auto modes_og = page->new_optgroup(L("Parallel Modes"), L"param_advanced"); - auto line = Line{ L("Modes"), L("") }; - line.full_width = 1; - line.widget = [this](wxWindow* parent) -> wxSizer* { - int n_cols = m_config->opt_int("imex_tools_per_gantry"); - int n_rows = m_config->opt_int("imex_gantry_count"); - std::string layout_str = "front-left"; - if (auto* o = m_config->option("imex_tool_layout")) layout_str = o->value; - int layout = IMEXModesCtrl::parse_layout(layout_str); - m_imex_modes_ctrl = new IMEXModesCtrl(parent, n_cols, n_rows, layout); - m_imex_modes_ctrl->load_from_config(*m_config); - m_imex_modes_ctrl->on_change = [this]() { - auto [names, tools, gcodes] = m_imex_modes_ctrl->get_mode_data(); - m_config->set_key_value("imex_mode_names", new ConfigOptionStrings(names)); - m_config->set_key_value("imex_mode_active_tools", new ConfigOptionStrings(tools)); - m_config->set_key_value("imex_mode_gcodes", new ConfigOptionStrings(gcodes)); - update_dirty(); - on_value_change("imex_mode_names", std::string("")); - }; - auto* sizer = new wxBoxSizer(wxHORIZONTAL); - sizer->Add(m_imex_modes_ctrl, 1, wxEXPAND); - return sizer; - }; - modes_og->append_line(line); - } - page = add_options_page(L("Notes"), "custom-gcode_note"); // ORCA: icon only visible on placeholders optgroup = page->new_optgroup(L("Notes"), "note", 0); option = optgroup->get_option("printer_notes"); @@ -5378,6 +5269,111 @@ if (is_marlin_flavor) optgroup->append_single_option_line("machine_load_filament_time", "printer_multimaterial_advanced#filament-load-time"); optgroup->append_single_option_line("machine_unload_filament_time", "printer_multimaterial_advanced#filament-unload-time"); optgroup->append_single_option_line("machine_tool_change_time", "printer_multimaterial_advanced#tool-change-time"); + + // IDEX/IQEX (IMEX) parallel printing configuration + optgroup = page->new_optgroup(L("IDEX/IQEX Configuration"), L"param_advanced"); + optgroup->append_single_option_line("is_imex"); + optgroup->append_single_option_line("imex_gantry_count"); + optgroup->append_single_option_line("imex_tools_per_gantry"); + { + auto opt = optgroup->get_option("imex_tool_layout"); + auto line = Line{ opt.opt.label, opt.opt.tooltip }; + line.append_option(opt); + line.widget = [this](wxWindow* parent) -> wxSizer* { + static const wxString choices[] = { + _L("Front-left"), _L("Front-right"), + _L("Rear-left"), _L("Rear-right") + }; + static const char* values[] = { "front-left", "front-right", "rear-left", "rear-right" }; + m_imex_layout_combo = new ComboBox(parent, wxID_ANY, wxEmptyString, + wxDefaultPosition, + wxSize(12 * wxGetApp().em_unit(), -1), + 4, choices, wxCB_READONLY); + m_imex_layout_combo->GetDropDown().SetUseContentWidth(true); + std::string cur = "front-left"; + if (auto* o = m_config->option("imex_tool_layout")) cur = o->value; + for (int i = 0; i < 4; ++i) + if (cur == values[i]) { m_imex_layout_combo->SetSelection(i); break; } + m_imex_layout_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { + static const char* layout_vals[] = { "front-left", "front-right", "rear-left", "rear-right" }; + int sel = m_imex_layout_combo->GetSelection(); + std::string val = (sel >= 0 && sel < 4) ? layout_vals[sel] : "front-left"; + m_config->set_key_value("imex_tool_layout", new ConfigOptionString(val)); + on_value_change("imex_tool_layout", val); + if (m_imex_modes_ctrl) + m_imex_modes_ctrl->set_grid_size( + m_config->opt_int("imex_tools_per_gantry"), + m_config->opt_int("imex_gantry_count"), + IMEXModesCtrl::parse_layout(val)); + }); + auto* s = new wxBoxSizer(wxHORIZONTAL); + s->Add(m_imex_layout_combo, 0, wxALIGN_CENTER_VERTICAL); + return s; + }; + optgroup->append_line(line); + } + optgroup->append_single_option_line("imex_nozzle_clearance_x"); + optgroup->append_single_option_line("imex_nozzle_clearance_y"); + optgroup->append_single_option_line("imex_carriage_margin"); + { + static const wxString theme_choices[] = { + _L("Standard"), + _L("Deuteranopia / Protanopia (red-green)"), + _L("Tritanopia (blue-yellow)"), + _L("High Contrast") }; + static const char* theme_vals[] = { "standard", "deuteranopia", "tritanopia", "high_contrast" }; + auto opt = optgroup->get_option("imex_viz_theme"); + auto line = Line{ opt.opt.label, opt.opt.tooltip }; + line.append_option(opt); + line.widget = [this](wxWindow* parent) -> wxSizer* { + m_imex_theme_combo = new ComboBox(parent, wxID_ANY, wxEmptyString, + wxDefaultPosition, + wxSize(22 * wxGetApp().em_unit(), -1), + 4, theme_choices, wxCB_READONLY); + m_imex_theme_combo->GetDropDown().SetUseContentWidth(true); + std::string cur = "standard"; + if (auto* o = m_config->option("imex_viz_theme")) cur = o->value; + for (int i = 0; i < 4; ++i) + if (cur == theme_vals[i]) { m_imex_theme_combo->SetSelection(i); break; } + m_imex_theme_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { + int sel = m_imex_theme_combo->GetSelection(); + std::string val = (sel >= 0 && sel < 4) ? theme_vals[sel] : "standard"; + m_config->set_key_value("imex_viz_theme", new ConfigOptionString(val)); + on_value_change("imex_viz_theme", val); + }); + auto* s = new wxBoxSizer(wxHORIZONTAL); + s->Add(m_imex_theme_combo, 0, wxALIGN_CENTER_VERTICAL); + return s; + }; + optgroup->append_line(line); + } + { + auto modes_og = page->new_optgroup(L("IDEX/IQEX Parallel Modes"), L"param_advanced"); + auto line = Line{ L("Modes"), L("") }; + line.full_width = 1; + line.widget = [this](wxWindow* parent) -> wxSizer* { + int n_cols = m_config->opt_int("imex_tools_per_gantry"); + int n_rows = m_config->opt_int("imex_gantry_count"); + std::string layout_str = "front-left"; + if (auto* o = m_config->option("imex_tool_layout")) layout_str = o->value; + int layout = IMEXModesCtrl::parse_layout(layout_str); + m_imex_modes_ctrl = new IMEXModesCtrl(parent, n_cols, n_rows, layout); + m_imex_modes_ctrl->load_from_config(*m_config); + m_imex_modes_ctrl->on_change = [this]() { + auto [names, tools, gcodes] = m_imex_modes_ctrl->get_mode_data(); + m_config->set_key_value("imex_mode_names", new ConfigOptionStrings(names)); + m_config->set_key_value("imex_mode_active_tools", new ConfigOptionStrings(tools)); + m_config->set_key_value("imex_mode_gcodes", new ConfigOptionStrings(gcodes)); + update_dirty(); + on_value_change("imex_mode_names", std::string("")); + }; + auto* sizer = new wxBoxSizer(wxHORIZONTAL); + sizer->Add(m_imex_modes_ctrl, 1, wxEXPAND); + return sizer; + }; + modes_og->append_line(line); + } + m_pages.insert(m_pages.end() - n_after_single_extruder_MM, page); } @@ -5735,6 +5731,16 @@ void TabPrinter::toggle_options() toggle_option("extruders_count", !bSEMM); toggle_option("manual_filament_change", bSEMM); toggle_option("purge_in_prime_tower", bSEMM && supports_wipe_tower_2); + + // IDEX/IQEX: show carriage config options only when is_imex is enabled + bool is_imex = m_config->opt_bool("is_imex"); + for (auto el : {"imex_gantry_count", "imex_tools_per_gantry", "imex_tool_layout", + "imex_nozzle_clearance_x", "imex_nozzle_clearance_y", + "imex_carriage_margin", "imex_viz_theme"}) + toggle_option(el, is_imex); + if (m_imex_layout_combo) m_imex_layout_combo->Show(is_imex); + if (m_imex_theme_combo) m_imex_theme_combo->Show(is_imex); + if (m_imex_modes_ctrl) m_imex_modes_ctrl->Show(is_imex); } wxString extruder_number; long val = 1; From 9a87363044e10704e62c35d98edb16a81fb1afa7 Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Thu, 9 Apr 2026 17:09:24 -0400 Subject: [PATCH 029/434] feat: 4-tool support, primary row, zone sizing, and stability fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IMEXModesCtrl: - Primary mode is now a non-deletable first row stored as sentinel "primary" in imex_mode_names; older configs load cleanly - New rows default T0 → Primary when no tool assignment is stored - Filter "primary" from plater popup/cycle list to prevent double entry - imex_tools_per_gantry cap raised 2 → 4 Zone sizing: - Zone width/height now based on active tool count only; inactive tools donate their bed share to active neighbors (fixes 4-tool layout) - Active col/row maps (col_to_zone/row_to_zone) applied consistently across zone fills, collision strips, and primary zone box GCodeViewer animation: - Mirror position formula fixed: left-of-copy reflects across copy zone's left edge; right-of-copy reflects across right edge (T3 was rendering on top of T1) - strip_width/row_strip_height use active counts, matching PartPlate Stability: - Early return in calc_imex_zones() when tool_states is empty; prevents OOB crash on new printer with stale process-preset mode name - is_imex toggle in on_config_change calls refresh_imex_icons() so the plate mode icon appears without requiring a new project GCode: - Remove "primary" guard so Primary mode gcode field is emitted at start of print Co-Authored-By: Claude Sonnet 4.6 --- resources/images/plate_ixex_mode.svg | 10 -- resources/images/plate_ixex_mode_dark.svg | 10 -- resources/images/plate_ixex_mode_hover.svg | 10 -- .../images/plate_ixex_mode_hover_dark.svg | 10 -- src/libslic3r/GCode.cpp | 2 +- src/libslic3r/PrintConfig.cpp | 2 +- src/slic3r/GUI/GCodeViewer.cpp | 101 +++++++++------- src/slic3r/GUI/PartPlate.cpp | 110 ++++++++++++++---- src/slic3r/GUI/PartPlate.hpp | 3 + src/slic3r/GUI/Plater.cpp | 8 +- src/slic3r/GUI/Tab.cpp | 88 ++++++++++---- 11 files changed, 220 insertions(+), 134 deletions(-) delete mode 100644 resources/images/plate_ixex_mode.svg delete mode 100644 resources/images/plate_ixex_mode_dark.svg delete mode 100644 resources/images/plate_ixex_mode_hover.svg delete mode 100644 resources/images/plate_ixex_mode_hover_dark.svg diff --git a/resources/images/plate_ixex_mode.svg b/resources/images/plate_ixex_mode.svg deleted file mode 100644 index 30a67cdf35..0000000000 --- a/resources/images/plate_ixex_mode.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/resources/images/plate_ixex_mode_dark.svg b/resources/images/plate_ixex_mode_dark.svg deleted file mode 100644 index 981fd88447..0000000000 --- a/resources/images/plate_ixex_mode_dark.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/resources/images/plate_ixex_mode_hover.svg b/resources/images/plate_ixex_mode_hover.svg deleted file mode 100644 index 9463e767df..0000000000 --- a/resources/images/plate_ixex_mode_hover.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/resources/images/plate_ixex_mode_hover_dark.svg b/resources/images/plate_ixex_mode_hover_dark.svg deleted file mode 100644 index 301fa995ab..0000000000 --- a/resources/images/plate_ixex_mode_hover_dark.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index ec249a6ae5..31f1f421a7 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -3058,7 +3058,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // macro is active before any motion, homing, or probing in start G-code. if (print.config().is_imex.value && !print.objects().empty()) { const std::string& imex_mode = print.objects().front()->config().imex_parallel_mode.value; - if (!imex_mode.empty() && imex_mode != "primary") { + if (!imex_mode.empty()) { const auto& mode_names = print.config().imex_mode_names.values; const auto& mode_gcodes = print.config().imex_mode_gcodes.values; for (size_t i = 0; i < mode_names.size() && i < mode_gcodes.size(); ++i) { diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index b4978b1263..5b05933e93 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -5506,7 +5506,7 @@ void PrintConfigDef::init_fff_params() def->label = L("Tools per Gantry"); def->tooltip = L("Number of independent toolheads per gantry along X. 2 for IDEX-style."); def->min = 1; - def->max = 2; + def->max = 4; def->mode = comAdvanced; def->set_default_value(new ConfigOptionInt(2)); diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index f64d9f5e4b..91ad35cadc 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1652,78 +1652,99 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) return flip_y ? (gantry_count - 1 - raw) : raw; }; - const Vec3f prim_pos = libvgcode::convert(curr_vertex.position); - const float strip_width = (bed_x_max - bed_x_min) / (float)tools_per_gantry; - const float row_strip_height = (bed_y_max - bed_y_min) / (float)gantry_count; - const int pri_phys_col = (pri_tool >= 0) ? phys_col_of(pri_tool) : 0; - const int pri_phys_row = (pri_tool >= 0) ? phys_row_of(pri_tool) : 0; + // Build active col/row sets — mirrors PartPlate::calc_imex_zones() so + // zone sizes and positions stay in sync with the bed visualization. + const int pri_phys_col = (pri_tool >= 0) ? phys_col_of(pri_tool) : 0; + const int pri_phys_row = (pri_tool >= 0) ? phys_row_of(pri_tool) : 0; + std::map phys_col_to_zone_gv, phys_row_to_zone_gv; + int n_active_cols_gv = 1, n_active_rows_gv = 1; + { + std::set ac, ar; + ac.insert(pri_phys_col); ar.insert(pri_phys_row); + for (int tid : sec_tool_ids) { + ac.insert(phys_col_of(tid)); + ar.insert(phys_row_of(tid)); + } + // col_to_zone / row_to_zone: physical index → zone index (sorted) + int k = 0; + for (int c : ac) phys_col_to_zone_gv[c] = k++; + k = 0; + for (int r : ar) phys_row_to_zone_gv[r] = k++; + n_active_cols_gv = (int)ac.size(); + n_active_rows_gv = (int)ar.size(); + } + auto zone_col = [&](int pc) { auto it = phys_col_to_zone_gv.find(pc); return it != phys_col_to_zone_gv.end() ? it->second : 0; }; + auto zone_row = [&](int pr) { auto it = phys_row_to_zone_gv.find(pr); return it != phys_row_to_zone_gv.end() ? it->second : 0; }; - // Pre-pass: for each physical row, find the Copy reference column. + const Vec3f prim_pos = libvgcode::convert(curr_vertex.position); + const float strip_width = (bed_x_max - bed_x_min) / (float)n_active_cols_gv; + const float row_strip_height = (bed_y_max - bed_y_min) / (float)n_active_rows_gv; + const int pri_zone_col = zone_col(pri_phys_col); + const int pri_zone_row = zone_row(pri_phys_row); + + // Pre-pass: for each physical row, find the Copy reference physical column. // The primary row's reference is the primary itself. - // Non-primary rows need a Copy tool; Mirror tools on that row - // reflect that Copy's X position. - std::map row_copy_col; + std::map row_copy_col; // phys_row → phys_col of copy reference row_copy_col[pri_phys_row] = pri_phys_col; for (int i = 0; i < sec_count; ++i) { - if (sec_tool_states[sec_tool_ids[i]] == 2) { - row_copy_col[phys_row_of(sec_tool_ids[i])] = - phys_col_of(sec_tool_ids[i]); - } + if (sec_tool_states[sec_tool_ids[i]] == 2) + row_copy_col[phys_row_of(sec_tool_ids[i])] = phys_col_of(sec_tool_ids[i]); } - // Primary carriage box: nozzle sits at the collision-side edge. - // Default to centered; override once we know which side the secondaries are on. - float pri_box_offset_x = (pri_phys_col == 0) ? 0.0f : -imex_box_wx; // nozzle at inner edge (facing center of bed) - float pri_box_offset_y = -imex_box_wy; // nozzle at high-Y edge by default (gantry always behind nozzle); override if secondary is behind + // Primary carriage box + float pri_box_offset_x = (pri_zone_col == 0) ? 0.0f : -imex_box_wx; + float pri_box_offset_y = -imex_box_wy; for (int i = 0; i < sec_count; ++i) { int sc = phys_col_of(sec_tool_ids[i]); int sr = phys_row_of(sec_tool_ids[i]); - if (sc > pri_phys_col) { pri_box_offset_x = 0.0f; } - else if (sc < pri_phys_col) { pri_box_offset_x = -imex_box_wx; } - if (sr > pri_phys_row) { pri_box_offset_y = 0.0f; } - else if (sr < pri_phys_row) { pri_box_offset_y = -imex_box_wy; } + if (sc > pri_phys_col) { pri_box_offset_x = 0.0f; } + else if (sc < pri_phys_col) { pri_box_offset_x = -imex_box_wx; } + if (sr > pri_phys_row) { pri_box_offset_y = 0.0f; } + else if (sr < pri_phys_row) { pri_box_offset_y = -imex_box_wy; } } carriage_box_draws.push_back({ prim_pos, s_carriage_colors[0], pri_box_offset_x, pri_box_offset_y }); - const float pri_zone_x = bed_x_min + (float)pri_phys_col * strip_width; - const float pri_zone_y = bed_y_min + (float)pri_phys_row * row_strip_height; + const float pri_zone_x = bed_x_min + (float)pri_zone_col * strip_width; + const float pri_zone_y = bed_y_min + (float)pri_zone_row * row_strip_height; const float rel_x = prim_pos.x() - pri_zone_x; const float rel_y = prim_pos.y() - pri_zone_y; for (int i = 0; i < sec_count; ++i) { int sec_phys_col = phys_col_of(sec_tool_ids[i]); int sec_phys_row = phys_row_of(sec_tool_ids[i]); + int sec_zc = zone_col(sec_phys_col); + int sec_zr = zone_row(sec_phys_row); const int sec_state = sec_tool_states[sec_tool_ids[i]]; - // Y: all tools in a row share a Y rail, so Y is always - // zone-relative copy from the primary. - float sec_zone_y = bed_y_min + (float)sec_phys_row * row_strip_height; + // Y: all tools on a row share a Y rail — always zone-relative copy. + float sec_zone_y = bed_y_min + (float)sec_zr * row_strip_height; float sec_y = sec_zone_y + rel_y; - // X: Copy places the tool at the same relative position within - // its own zone. Mirror reflects the Copy reference on this row. + // X: Copy → same zone-relative position. + // Mirror → reflect copy reference across the boundary it shares with + // this mirror zone (left or right edge of copy zone depending on side). float sec_x; if (sec_state == 2) { - float sec_zone_x = bed_x_min + (float)sec_phys_col * strip_width; - sec_x = sec_zone_x + rel_x; + sec_x = bed_x_min + (float)sec_zc * strip_width + rel_x; } else { - // Mirror: find the Copy on the same row and reflect its X. auto ref_it = row_copy_col.find(sec_phys_row); - if (ref_it != row_copy_col.end()) { - float ref_zone_x = bed_x_min + (float)ref_it->second * strip_width; - float ref_x = ref_zone_x + rel_x; - sec_x = bed_x_min + bed_x_max - ref_x; + int ref_phys_col = (ref_it != row_copy_col.end()) ? ref_it->second : pri_phys_col; + int ref_zc = zone_col(ref_phys_col); + float ref_zone_x = bed_x_min + (float)ref_zc * strip_width; + float ref_abs = ref_zone_x + rel_x; + if (sec_phys_col < ref_phys_col) { + // Mirror left of copy — reflects across copy zone's left edge + sec_x = 2.0f * ref_zone_x - ref_abs; } else { - sec_x = bed_x_min + bed_x_max - prim_pos.x(); + // Mirror right of copy — reflects across copy zone's right edge + float ref_zone_right = bed_x_min + (float)(ref_zc + 1) * strip_width; + sec_x = 2.0f * ref_zone_right - ref_abs; } } Vec3f sec_pos{ sec_x, sec_y, prim_pos.z() }; m_sequential_view.m_imex_secondary_markers[i].set_world_position(sec_pos); m_sequential_view.m_imex_secondary_markers[i].set_z_offset(m_z_offset + 0.5f); - // X: copy tools move in sync with primary so they share the same - // X-facing orientation. Mirror tools approach from the opposite side - // so the nozzle sits at the collision-side edge. float sec_box_offset_x; if (sec_state == 2) { sec_box_offset_x = pri_box_offset_x; @@ -1732,8 +1753,6 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) else if (sec_phys_col < pri_phys_col) sec_box_offset_x = 0.0f; else sec_box_offset_x = pri_box_offset_x; } - // Y: always row-based — gantry is always behind nozzle regardless of - // copy/mirror (a back-row copy still has its gantry at the back). float sec_box_offset_y; if (sec_phys_row > pri_phys_row) sec_box_offset_y = -imex_box_wy; else if (sec_phys_row < pri_phys_row) sec_box_offset_y = 0.0f; diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 17447a6707..a5c2865574 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -579,6 +579,11 @@ void PartPlate::calc_imex_zones() } } + // If the mode name was found but has no tools (e.g. stale process-preset mode on a new + // printer that has no modes defined yet), there is nothing to compute. + if (tool_states.empty()) + return; + // Identify the Primary tool from the mode definition for (auto& [idx, state] : tool_states) { if (state == 1) { auto [c, r] = tool_to_phys(idx); pri_col = c; pri_row = r; break; } @@ -601,8 +606,33 @@ void PartPlate::calc_imex_zones() auto bed_ext = get_extents(m_shape); double x_min = bed_ext.min(0), x_max = bed_ext.max(0); double y_min = bed_ext.min(1), y_max = bed_ext.max(1); - double zone_w = (x_max - x_min) / n_cols; - double zone_h = (y_max - y_min) / n_rows; + + // Zone sizing is based on ACTIVE tool count per axis, not total grid dimensions. + // Inactive tools (absent from tool_states) donate their bed share to active neighbors. + // Sorted active col/row lists map physical index k → zone index k. + std::vector active_cols_v, active_rows_v; + { + std::set ac_set, ar_set; + for (auto& [idx, state] : tool_states) { + auto [c, r] = tool_to_phys(idx); + ac_set.insert(c); ar_set.insert(r); + } + active_cols_v.assign(ac_set.begin(), ac_set.end()); // sorted ascending + active_rows_v.assign(ar_set.begin(), ar_set.end()); + } + int n_active_cols = std::max(1, (int)active_cols_v.size()); + int n_active_rows = std::max(1, (int)active_rows_v.size()); + + std::map col_to_zone, row_to_zone; + for (int k = 0; k < n_active_cols; ++k) col_to_zone[active_cols_v[k]] = k; + for (int k = 0; k < n_active_rows; ++k) row_to_zone[active_rows_v[k]] = k; + + double zone_w = (x_max - x_min) / n_active_cols; + double zone_h = (y_max - y_min) / n_active_rows; + + // Zone index of the primary tool + int pri_col_k = col_to_zone.count(pri_col) ? col_to_zone[pri_col] : 0; + int pri_row_k = row_to_zone.count(pri_row) ? row_to_zone[pri_row] : 0; // Separation axes: row-sep = secondaries on a different gantry, col-sep = different column bool has_row_sep = false, has_col_sep = false; @@ -647,18 +677,26 @@ void PartPlate::calc_imex_zones() auto make_boxes = [&](const std::set>& cells) -> std::vector { std::vector boxes; if (cells.empty()) return boxes; + auto ck = [&](int c) { return col_to_zone.count(c) ? col_to_zone.at(c) : 0; }; + auto rk = [&](int r) { return row_to_zone.count(r) ? row_to_zone.at(r) : 0; }; if (has_row_sep && !has_col_sep) { std::set rows; for (auto& [c,r] : cells) rows.insert(r); - for (int sr : rows) - boxes.push_back({x_min, x_max, y_min + sr*zone_h, y_min + (sr+1)*zone_h}); + for (int sr : rows) { + int k = rk(sr); + boxes.push_back({x_min, x_max, y_min + k*zone_h, y_min + (k+1)*zone_h}); + } } else if (has_col_sep && !has_row_sep) { std::set cols; for (auto& [c,r] : cells) cols.insert(c); - for (int sc : cols) - boxes.push_back({x_min + sc*zone_w, x_min + (sc+1)*zone_w, y_min, y_max}); + for (int sc : cols) { + int k = ck(sc); + boxes.push_back({x_min + k*zone_w, x_min + (k+1)*zone_w, y_min, y_max}); + } } else { - for (auto& [sc,sr] : cells) - boxes.push_back({x_min + sc*zone_w, x_min + (sc+1)*zone_w, - y_min + sr*zone_h, y_min + (sr+1)*zone_h}); + for (auto& [sc,sr] : cells) { + int ck_ = ck(sc), rk_ = rk(sr); + boxes.push_back({x_min + ck_*zone_w, x_min + (ck_+1)*zone_w, + y_min + rk_*zone_h, y_min + (rk_+1)*zone_h}); + } } return boxes; }; @@ -718,11 +756,11 @@ void PartPlate::calc_imex_zones() } }; - // Primary zone extent — mirrors in_primary_zone / make_boxes expansion logic - double pz_x0 = has_col_sep ? x_min + pri_col * zone_w : x_min; - double pz_x1 = has_col_sep ? x_min + (pri_col + 1) * zone_w : x_max; - double pz_y0 = has_row_sep ? y_min + pri_row * zone_h : y_min; - double pz_y1 = has_row_sep ? y_min + (pri_row + 1) * zone_h : y_max; + // Primary zone extent — uses zone indices so inactive tools don't shrink the zone + double pz_x0 = has_col_sep ? x_min + pri_col_k * zone_w : x_min; + double pz_x1 = has_col_sep ? x_min + (pri_col_k + 1) * zone_w : x_max; + double pz_y0 = has_row_sep ? y_min + pri_row_k * zone_h : y_min; + double pz_y1 = has_row_sep ? y_min + (pri_row_k + 1) * zone_h : y_max; m_imex_primary_zone_box = BoundingBoxf(Vec2d(pz_x0, pz_y0), Vec2d(pz_x1, pz_y1)); // Helper: build a BoundingBoxf3 strip, clip to bed, add to members @@ -753,26 +791,27 @@ void PartPlate::calc_imex_zones() for (auto& [idx, state] : tool_states) { if (state != 3) continue; // only mirror tools (state==3) require collision strips auto [c, r] = tool_to_phys(idx); - // X-boundary strips: any mirror in an adjacent column can cause an X collision. - if (c == pri_col + 1) has_right_sec = true; - if (c == pri_col - 1) has_left_sec = true; - // Y-boundary strips: only a mirror DIRECTLY above/below (same column) causes a Y collision. - // A mirror in a different column that happens to be in an adjacent row is only an X risk. - if (r == pri_row + 1 && c == pri_col) has_top_sec = true; - if (r == pri_row - 1 && c == pri_col) has_bottom_sec = true; + int zc = col_to_zone.count(c) ? col_to_zone.at(c) : -1; + int zr = row_to_zone.count(r) ? row_to_zone.at(r) : -1; + // X-boundary strips: mirror in the zone immediately adjacent to the primary zone. + if (zc == pri_col_k + 1) has_right_sec = true; + if (zc == pri_col_k - 1) has_left_sec = true; + // Y-boundary strips: mirror zone-adjacent in Y, same physical column as primary. + if (zr == pri_row_k + 1 && c == pri_col) has_top_sec = true; + if (zr == pri_row_k - 1 && c == pri_col) has_bottom_sec = true; } // X-axis boundaries (vertical strips, width = nozzle_clearance_x on primary side) if (carriage_w > 0.0) { if (has_right_sec) { - double bnd_x = x_min + (pri_col + 1) * zone_w; + double bnd_x = x_min + (pri_col_k + 1) * zone_w; double strip_inner = bnd_x - carriage_w; add_strip(strip_inner, bnd_x, pz_y0, pz_y1); if (margin > 0.0) add_margin_fill(strip_inner - margin, strip_inner, pz_y0, pz_y1); } if (has_left_sec) { - double bnd_x = x_min + pri_col * zone_w; + double bnd_x = x_min + pri_col_k * zone_w; double strip_inner = bnd_x + carriage_w; add_strip(bnd_x, strip_inner, pz_y0, pz_y1); if (margin > 0.0) @@ -783,14 +822,14 @@ void PartPlate::calc_imex_zones() // Y-axis boundaries (horizontal strips, width = nozzle_clearance_y on primary side) if (carriage_h > 0.0) { if (has_top_sec) { - double bnd_y = y_min + (pri_row + 1) * zone_h; + double bnd_y = y_min + (pri_row_k + 1) * zone_h; double strip_inner = bnd_y - carriage_h; add_strip(pz_x0, pz_x1, strip_inner, bnd_y); if (margin > 0.0) add_margin_fill(pz_x0, pz_x1, strip_inner - margin, strip_inner); } if (has_bottom_sec) { - double bnd_y = y_min + pri_row * zone_h; + double bnd_y = y_min + pri_row_k * zone_h; double strip_inner = bnd_y + carriage_h; add_strip(pz_x0, pz_x1, bnd_y, strip_inner); if (margin > 0.0) @@ -830,6 +869,20 @@ std::string PartPlate::build_imex_cache_key() const + "|mg" + std::to_string(mgn_opt ? (int)(mgn_opt->value * 10) : 0); } +// Reposition the IMEX mode icon without requiring a full set_shape() rebuild. +// Called when is_imex is toggled on a printer whose bed shape matches the current plate, +// which would otherwise cause set_shape() to short-circuit before reaching icon calc. +void PartPlate::refresh_imex_icon() +{ + if (!m_plater) return; + PresetBundle* preset = wxGetApp().preset_bundle; + if (!preset) return; + bool dual_bbl = preset->is_bbl_vendor() && preset->get_printer_extruder_count() == 2; + auto* is_imex_opt = preset->printers.get_edited_preset().config.option("is_imex"); + if (is_imex_opt && is_imex_opt->value) + calc_vertex_for_icons(dual_bbl ? 7 : 6, m_imex_mode_icon); +} + // Ensure zone geometry (secondary boxes, collision strips, visual meshes) is up to date. // Called before any collision check AND before rendering so both paths share the same data. void PartPlate::ensure_imex_zones() @@ -7190,6 +7243,13 @@ void PartPlateList::load_cali_textures() PartPlateList::is_load_cali_texture = true; } +void PartPlateList::refresh_imex_icons() +{ + const std::lock_guard local_lock(m_plates_mutex); + for (PartPlate* plate : m_plate_list) + plate->refresh_imex_icon(); +} + void PartPlateList::on_extruder_count_changed(int extruder_count) { for (unsigned int i = 0; i < (unsigned int) m_plate_list.size(); ++i) { diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index 423de28585..6ff42c3ef6 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -191,6 +191,7 @@ private: void ensure_imex_zones(); std::string build_imex_cache_key() const; void render_imex_zones(bool force_default_color); + void refresh_imex_icon(); void calc_vertex_for_number(int index, bool one_number, GLModel &buffer); void calc_vertex_for_plate_name_edit_icon(GLTexture *texture, int index, PickingModel &model); void calc_vertex_for_icons(int index, PickingModel &model); @@ -952,6 +953,8 @@ public: void load_cali_textures(); void on_extruder_count_changed(int extruder_count); + // Re-position the IMEX mode icon on all plates without a full shape rebuild. + void refresh_imex_icons(); void set_filament_count(int filament_count); void on_filament_deleted(int filament_count, int filament_id); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 6e5753c546..af49d6341b 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -16268,6 +16268,12 @@ void Plater::on_config_change(const DynamicPrintConfig &config) opt_key == "sparse_infill_filament" || opt_key == "solid_infill_filament") { update_scheduled = true; } + // IDEX/IQEX: when is_imex toggles the mode icon needs to be repositioned on every plate. + // set_shape() short-circuits when the bed geometry is unchanged, so we refresh explicitly. + else if (opt_key == "is_imex") { + p->partplate_list.refresh_imex_icons(); + update_scheduled = true; + } } if (bed_shape_changed) @@ -17652,7 +17658,7 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi auto* names_opt = printer_cfg.option("imex_mode_names"); if (names_opt) { for (const auto& n : names_opt->values) - if (!n.empty()) modes.push_back(n); + if (!n.empty() && n != "primary") modes.push_back(n); } if (right_click) { diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 001bd5f305..7b7bdc8974 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4431,7 +4431,7 @@ public: m_n_rows = n_rows; m_layout = layout; for (size_t i = 0; i < names.size(); ++i) - add_row(names[i], tools[i], gcodes[i]); + add_row(names[i], tools[i], gcodes[i], /*is_primary=*/(names[i] == "primary")); Layout(); } @@ -4440,11 +4440,31 @@ public: auto* names = cfg.option("imex_mode_names"); auto* tools = cfg.option("imex_mode_active_tools"); auto* gcodes = cfg.option("imex_mode_gcodes"); + + // Primary row is always first and non-deletable. Look for an existing + // "primary" entry in the config (present in configs saved after #8 was + // implemented); fall back to empty tool/gcode for older configs. + std::string primary_tools, primary_gcode; + size_t primary_cfg_idx = std::string::npos; + if (names) { + for (size_t i = 0; i < names->values.size(); ++i) { + if (names->values[i] == "primary") { + primary_cfg_idx = i; + if (tools && i < tools->values.size()) primary_tools = tools->values[i]; + if (gcodes && i < gcodes->values.size()) primary_gcode = gcodes->values[i]; + break; + } + } + } + add_row("primary", primary_tools, primary_gcode, /*is_primary=*/true); + size_t n = names ? names->values.size() : 0; - for (size_t i = 0; i < n; ++i) + for (size_t i = 0; i < n; ++i) { + if (i == primary_cfg_idx) continue; // already added above add_row(names->values[i], (tools && i < tools->values.size()) ? tools->values[i] : "", (gcodes && i < gcodes->values.size()) ? gcodes->values[i] : ""); + } Layout(); } @@ -4452,7 +4472,7 @@ public: get_mode_data() const { std::vector names, tools, gcodes; for (auto& r : m_rows) { - std::string nm = r.name->GetValue().ToStdString(); + std::string nm = r.is_primary ? "primary" : r.name->GetValue().ToStdString(); if (nm.empty()) continue; names.push_back(nm); tools.push_back(active_tools_string(r)); @@ -4513,32 +4533,44 @@ private: struct Row { wxPanel* panel; - wxTextCtrl* name; + wxTextCtrl* name; // nullptr for primary row (name is fixed) wxTextCtrl* gcode; std::vector btns; - std::vector btn_states; // parallel to btns - // tool_idx for btns[i]: computed during add_row, stored here for quick lookup + std::vector btn_states; std::vector btn_tool_idx; - // Full assignment map including tools not currently visible due to grid size. - // Preserved across grid resizes so assignments restore when grid expands again. std::map all_tool_states; + bool is_primary {false}; }; void add_row(const std::string& name = "", const std::string& active_tools = "", - const std::string& gcode = "") + const std::string& gcode = "", + bool is_primary = false) { Row r; + r.is_primary = is_primary; r.panel = new wxPanel(this, wxID_ANY); auto* sizer = new wxBoxSizer(wxHORIZONTAL); - r.name = new wxTextCtrl(r.panel, wxID_ANY, name, wxDefaultPosition, wxSize(130, -1)); - r.name->Bind(wxEVT_TEXT, [this](wxCommandEvent&) { notify(); }); + if (is_primary) { + r.name = nullptr; + auto* lbl = new wxStaticText(r.panel, wxID_ANY, _L("Primary"), + wxDefaultPosition, wxSize(130, -1)); + wxFont f = lbl->GetFont(); + f.SetWeight(wxFONTWEIGHT_BOLD); + lbl->SetFont(f); + sizer->Add(lbl, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6); + } else { + r.name = new wxTextCtrl(r.panel, wxID_ANY, name, wxDefaultPosition, wxSize(130, -1)); + r.name->Bind(wxEVT_TEXT, [this](wxCommandEvent&) { notify(); }); + } auto* grid_panel = new wxPanel(r.panel, wxID_ANY); auto* grid_sizer = new wxGridSizer(m_n_rows, m_n_cols, 2, 2); auto tool_states = parse_tool_states(active_tools); + if (tool_states.empty()) + tool_states[0] = 1; // default T0 → Primary when no assignment is stored r.all_tool_states = tool_states; // preserve all assignments, including off-screen tools wxPanel* this_panel = r.panel; @@ -4617,17 +4649,21 @@ private: wxDefaultPosition, wxSize(220, 54), wxTE_MULTILINE); r.gcode->Bind(wxEVT_TEXT, [this](wxCommandEvent&) { notify(); }); - auto* rm = new wxButton(r.panel, wxID_ANY, wxT("\u00d7"), - wxDefaultPosition, wxSize(24, 24), wxBU_EXACTFIT); - rm->Bind(wxEVT_BUTTON, [this, this_panel](wxCommandEvent&) { - remove_row(this_panel); - notify(); - }); - - sizer->Add(r.name, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6); + if (!is_primary) { + sizer->Add(r.name, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6); + } sizer->Add(grid_panel, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6); sizer->Add(r.gcode, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6); - sizer->Add(rm, 0, wxALIGN_CENTER_VERTICAL); + + if (!is_primary) { + auto* rm = new wxButton(r.panel, wxID_ANY, wxT("\u00d7"), + wxDefaultPosition, wxSize(24, 24), wxBU_EXACTFIT); + rm->Bind(wxEVT_BUTTON, [this, this_panel](wxCommandEvent&) { + remove_row(this_panel); + notify(); + }); + sizer->Add(rm, 0, wxALIGN_CENTER_VERTICAL); + } r.panel->SetSizer(sizer); m_rows_sizer->Add(r.panel, 0, wxEXPAND | wxBOTTOM, 4); @@ -4638,6 +4674,7 @@ private: void remove_row(wxPanel* panel) { for (size_t i = 0; i < m_rows.size(); ++i) { if (m_rows[i].panel == panel) { + if (m_rows[i].is_primary) return; // primary row is non-deletable m_rows_sizer->Detach(panel); m_rows.erase(m_rows.begin() + i); Layout(); @@ -5734,13 +5771,14 @@ void TabPrinter::toggle_options() // IDEX/IQEX: show carriage config options only when is_imex is enabled bool is_imex = m_config->opt_bool("is_imex"); - for (auto el : {"imex_gantry_count", "imex_tools_per_gantry", "imex_tool_layout", + for (auto el : {"imex_gantry_count", "imex_tools_per_gantry", "imex_nozzle_clearance_x", "imex_nozzle_clearance_y", - "imex_carriage_margin", "imex_viz_theme"}) + "imex_carriage_margin"}) toggle_option(el, is_imex); - if (m_imex_layout_combo) m_imex_layout_combo->Show(is_imex); - if (m_imex_theme_combo) m_imex_theme_combo->Show(is_imex); - if (m_imex_modes_ctrl) m_imex_modes_ctrl->Show(is_imex); + // These lines have custom combo widgets — toggle_line hides the full row + toggle_line("imex_tool_layout", is_imex); + toggle_line("imex_viz_theme", is_imex); + if (m_imex_modes_ctrl) m_imex_modes_ctrl->Show(is_imex); } wxString extruder_number; long val = 1; From 1d68f917b0d35f8afa6fc334d08488d4e47209d5 Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Thu, 9 Apr 2026 18:04:35 -0400 Subject: [PATCH 030/434] feat: add imex_mode/imex_mode_index/imex_mode_gcode placeholders with global var flow Register imex_mode (string), imex_mode_index (int), and imex_mode_gcode (string) in OtherSlicingStatesConfigDef so they appear in the placeholder search UI under Slicing State. Set all three via placeholder_parser().set() before any script processing in _do_export(). Process imex_mode_gcode first so {global} declarations defined there flow forward into machine_start_gcode. Co-Authored-By: Claude Sonnet 4.6 --- src/libslic3r/GCode.cpp | 54 ++++++++++++++++++++++++----------- src/libslic3r/PrintConfig.cpp | 12 ++++++++ 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 31f1f421a7..87e4f2d25f 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -3036,6 +3036,40 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato this->placeholder_parser().set("print_time_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Sec_Placeholder))); this->placeholder_parser().set("used_filament_length", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder))); + // IDEX/IQEX: set {imex_mode}, {imex_mode_index}, {imex_mode_gcode} placeholders before + // any G-code script is processed so they are available in machine_start_gcode, + // filament_start_gcode, filament_end_gcode, change_filament_gcode, etc. + // {imex_mode_names} and {imex_mode_gcodes} array placeholders are already accessible + // via the printer config (e.g. {imex_mode_names[0]}, {imex_mode_gcodes[1]}). + std::string imex_active_mode; + int imex_active_mode_index = 0; + std::string imex_active_mode_gcode; + if (print.config().is_imex.value && !print.objects().empty()) { + const std::string& raw = print.objects().front()->config().imex_parallel_mode.value; + imex_active_mode = raw.empty() ? "primary" : raw; + const auto& mode_names = print.config().imex_mode_names.values; + const auto& mode_gcodes = print.config().imex_mode_gcodes.values; + for (size_t i = 0; i < mode_names.size(); ++i) { + if (mode_names[i] == imex_active_mode) { + imex_active_mode_index = (int)i; + if (i < mode_gcodes.size()) + imex_active_mode_gcode = mode_gcodes[i]; + break; + } + } + } + this->placeholder_parser().set("imex_mode", imex_active_mode); + this->placeholder_parser().set("imex_mode_index", imex_active_mode_index); + this->placeholder_parser().set("imex_mode_gcode", imex_active_mode_gcode); + + // IDEX/IQEX: process mode G-code BEFORE machine_start_gcode so that any + // {global abc = 1} declarations in the mode script are visible to machine_start_gcode. + // The processed result is buffered here and written to the file after temp setup below. + std::string imex_processed_gcode; + if (!imex_active_mode_gcode.empty()) + imex_processed_gcode = this->placeholder_parser_process( + "imex_mode_gcode", imex_active_mode_gcode, initial_extruder_id); + std::string machine_start_gcode = this->placeholder_parser_process("machine_start_gcode", print.config().machine_start_gcode.value, initial_extruder_id); if (print.config().gcode_flavor != gcfKlipper) { // Set bed temperature if the start G-code does not contain any bed temp control G-codes. @@ -3054,23 +3088,9 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.write(m_writer.set_chamber_temperature(max_chamber_temp, true)); // set chamber_temperature } - // IDEX/IQEX: inject mode G-code before machine_start_gcode so the carriage mode - // macro is active before any motion, homing, or probing in start G-code. - if (print.config().is_imex.value && !print.objects().empty()) { - const std::string& imex_mode = print.objects().front()->config().imex_parallel_mode.value; - if (!imex_mode.empty()) { - const auto& mode_names = print.config().imex_mode_names.values; - const auto& mode_gcodes = print.config().imex_mode_gcodes.values; - for (size_t i = 0; i < mode_names.size() && i < mode_gcodes.size(); ++i) { - if (mode_names[i] == imex_mode && !mode_gcodes[i].empty()) { - std::string imex_gcode = this->placeholder_parser_process( - "imex_mode_gcode", mode_gcodes[i], initial_extruder_id); - file.writeln(imex_gcode); - break; - } - } - } - } + // Write IMEX mode G-code first (processed above), then machine_start_gcode. + if (!imex_processed_gcode.empty()) + file.writeln(imex_processed_gcode); // Write the custom start G-code file.writeln(machine_start_gcode); diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 5b05933e93..310d799e19 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -10509,6 +10509,18 @@ OtherSlicingStatesConfigDef::OtherSlicingStatesConfigDef() new_def("initial_no_support_extruder", coInt, "Initial no support extruder", "Zero-based index of the first extruder used for printing without support. Same as initial_no_support_tool."); new_def("in_head_wrap_detect_zone", coBool, "In head wrap detect zone", "Indicates if the first layer overlaps with the head wrap zone."); + + def = this->add("imex_mode", coString); + def->label = L("iMEX active mode"); + def->tooltip = L("Name of the active iMEX parallel print mode for this plate (e.g. 'primary', 'mirror', 'copy'). Empty string if iMEX is not enabled."); + + def = this->add("imex_mode_index", coInt); + def->label = L("iMEX active mode index"); + def->tooltip = L("Zero-based index of the active iMEX parallel print mode within imex_mode_names."); + + def = this->add("imex_mode_gcode", coString); + def->label = L("iMEX active mode G-code"); + def->tooltip = L("The raw mode G-code template for the active iMEX parallel print mode, after placeholder evaluation. Globals defined here flow into machine_start_gcode."); } PrintStatisticsConfigDef::PrintStatisticsConfigDef() From a7441c7f482392acac81b8d4c8534bf174a7f0c5 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 11 Mar 2026 00:27:14 -0500 Subject: [PATCH 031/434] 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 032/434] 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 033/434] 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 034/434] 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 035/434] 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 036/434] 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 037/434] 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 038/434] 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 039/434] 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 040/434] 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 041/434] 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 zcmV(|K+(U6P)Px#1ZP1_K>z@;j|==^1poj7(@;!QMMrQ9vvdt+}$iKJSZnH zA0H|;Fg+1etCmDI7LcNVk$F7PEA~Vdy73i zOgS+=h=-FG79oX*ie6rCV`zDdiHKK=o@{S_(9q1XsG{cG(V?THU}Jl!v9#UV)br7~ zL^UuWASP*JVIUqM5D*ncHZo{oU>6u292y-J78`42WD5)u5fd3EA|)6ZARHSbgo=(r zGB51y?1G4kB_t|VMnh;{UM(swEG8%y6BdSykniyDQ$s*)XlNW486p!S>FVizf`&0L zH!&?RP(VC~j+E!<=Zcb-8VwwQg@`F9F9-(=q^+>>^z%PHLVSRPWnEk~H9O32OGHCP zWLjF|<>X;kR?VAnkeQx2IzL-aPE1Wvp{lNomYh5;EK*ZeM@UOvQ&U=8VRUzUZ*X&y zouX`zrjSXGu(r9Xw7E<@J4-q?D;Fv^Gc|pMi@wFjSW8JrI5cu|c6WMy_W1aVkdj_t zWy{jkd3}Kx4-lB5rd3y6MreJJQ$K zacpdgLx+%4kF2w`(v4u7Oq!9EmV!Keq_4DqM}Ei4%(1(_W@v1kQ=Y$*acVkca+j)7 zEKQrApixIir(von8Wy0Xrkkg%wz|5yzreU~OJOQkz01+4glC^xp{u#TJt!ewZi;6y zU(l0fq_Vj&ARBE*ZHZ=3QcO+6c~h`!w9KP?ezCw=V|<%!RGW8SQZ-O@Q$c@RNVIra zyl6mpov&U&GPs6jmuH-GskzL8S!hc-gkXs|4=;C9dQUPaTR>d={{5|cwwraNzIMg8 zguz%HONYJ5!-C7ck;-Ftjzzr5{94aK0000zbW%=J009F1{QUkt^WW2~=54zyAEQvu zX$4zPjfOlFF6sw-+yk5Z84|(%4Z|b(`3&{xql-TAS1ZvTG7vpjC~|ZO&X2ML>I2o-MHS6m2$}!ODfN z%SF*AiYLb`*&ylqTFAM#y~!4xm4M|-$1K~WYHYq!IX$;-c8#(d=a-`IvXrsatGoXn z5X+^5d;#ysn8TDg7ez5&F5ovC@3cJpWY+G^&Xyh(eO$T}#o`7_wRYFKkrDZa8|;wF zKZ94Vb1RSYz-~Fis+w!Tg{!7TJ_@qLXgTeTJCja%7C~Gh(~G`G13cnrXN>p`-b`+=-S8iAE`$E z6R$>VWI@iCujsCxht>M=OEdb0ik;0@I9Ata1*zP*W=y;0w$bz?MmpBCx-o-Z-%)jS z6*MC2A0#s;Am?LRbpsv-^4Yo7atkXIML=ACw-#+I!R;O&d*HAs7ZiT~n%iiz8A)n+ z=)stw?ZYP!7^@={yuM;)w5R2>a4WiybpqnNC~laWG&P4<5}khhs}+ZF%0g`)@gdJn zfOO&trE=0pdHZx@_w@tSWuev00^YNXMDLeTbjVs7a6p_d7#n+Qtbj;x4O*|eZjM}; zi7*70+up@6=Rx)xKV?=bG0HLAr&G5;ik`g`Pgt_XM2*M> z)O`+PU7gLnln60s_AS0(ac=J<9zUK4re6mrg<{f1Qq-;HR)^`8=quj5+ac?a{}<2k zhC5mY8{-ZAwUpFysV8Cznl^)e-)${<&A5AIK7u}e^yty!i5Nj7;{ObgN~LL>s?!cx51A?Y@&&TFhA5UJFleS}YX4ws7q^&b8Y7Kwas*KU{Agk#CJ>$w zpUS@Q?~+(1@4IY(*(>YU415^W$d@{IM{xZ3iG~o;BwLxoX@!lH;@eQIWWTs^KoJFsPxBdftxycBfYz;8^u&Zj45;J+=D8F@ zcsxwvaLG?i(lpIly>IMCs3&EOo}7xa(M^J5LLn}=`rR_FP?#aUm$kZfP5-Ahj{Gzs zTCJ}{@gI|o-L>Ijh^#fti4sgIoE3N?XrnhhoMc*#@d%N1*QNubl}Z}h-S;odil?Qh zGevL&!Ev6D@ULet{ElghMQL<9hP3MTam~-mH;ktGLeY0_8m^JL1(8K5_8Ec8+E)e6 z^Tc9)oj?IBAqYZ@V;CZF^knlL#?rJYdIr*@_pS(r<9SXv+Iznkk1wvL4+2yji;+&p zTAxm5(CB|cG$QLAWJ|t$##kGghGjrYf}(jeIde$xyx%hY;#qigF@^Gs2b?SL41h;> zmNrTHm9z<1H=i6Pn2;y2p3c6{uIJz(dALY++iL~KPXpC!eXWKz_UCBZA@Q35H8itoOOy?)tm^*~`NeFwIEv<_XVL4u=t3%$Oxl@4j_q_ZI)U zp3R{=2M6YKh)bonSB#bg6K5&~3fUSPHs%COu<{Rdtn1eu9-ue@105`cZMGyL-4f)J5P zzxKOEOVux>Qe0$|AKJ{!Ks5omHg=i!-6)(sgA4J37#PR`HRT{GdA zagcZDh^-K8O6tFhpq;=J)T%9{>}Fktu)z;^-T@Uoh_Q#xi)dgc7!jD*-uu1xutX6aA(#B)GHx;{mCCy1 z{d7uz!opQ@j3*?iqeu45deyC-J1TaAvJH*3b$^`G$nTcXjwvU~6FGZ0L3n~T(ukW) z7VO=(*~)eZEKi^W!3f^y`Dwm@Z$F=hk+rzGx0i!C&BNphF+wi9wQN&@zH#dN7wHIJ z@w9|kif<1Qi9dPUG-@*8(8z`drSjeTYb(e9kl+EGTX_JjW6z%E`HC0$P{bTW%=`iX5vx{Ix27wJOV%flgYt{h?R|2#) zU%`qzmL(Gj2~{}C15q$YStSpNQYjBn_hI|chsuA)-rwM4le`jj9*~S=F=y_%=bU?g zBW9L7cj5+ugHr1kSbw6?2AbvyFeW|x)|k&p#a6?eSzUFS&9zvzf-5M;P=@EFY^@M5 zS#kIlqlsn_q`(JB1|pdR=_QwwnSfL(1L+m;m^E^3>JP7Uui}<~B9e=5n{9Q5&yt3? z&8b_8BJFtJXG5p7q(N_02NR8yxcu_fYdcru0hp_pbkiAdEHiBjuViJP1|b7}GmO*< z??xzqY$s1(C4=A$(Kwl^&pbFKK zdr=X>w@OI`HasmH*RZt%vn2^0jRw-m&m5>b$$Eu>>Ddt+$v$$*?j6I|%!du5=(#pP zQElF(aobY7)B$kC$MYdfZ?n@0 z{B}3nb$QZoNKj6kGn0RfEE<>Lnin?0SuA8Sy|IEr4s}5ejf#tE(>$KDW=Ai7OvH+J z2%~t9Jdip9${>SNN4~B6tAluifBjgkR38*-tx)}^ z{c;iKNdTzH?k8~!^?2wS&>KPr1Hf=g`!uq8<8VTWk21kG{o|;&b6E4<2P^Ky7YlE_V$c(s#TwZZ_*98504%dbhBK<{;tRI^`wq}dS{^v1-Wru_mJx%lPu*Ec8MS-bV($q+%_(B$bB z6LJOh?6z)h?mo#5D#rf*#Kw9CNvzR}SI(X5*Agbf0)miH%YswZs1A*Qnf%yo8sw|x^@_ZI8_yMmjPHNA*lkyckH-^wlNFDGk^z_-m zPflsQ1mwwsjeJW3dW3oE;K7tbGHCE=vn||gfluRNI`HNsCuPHXL9ZR;8?li+PsC0N zg=z@?m3I_ZlI3t*Nqo}%9=Er17Rc(CiNZ=2q|stml3YOcz)3mn40EdOT!@d!sl#cU zFU-x&t$K;K3r2i8Jl1YE9Jf`+YZF&JR&%edg_8j#H{NZD@%hPh zkx-lbdGJ`6abQH>4M3$0a@v7dJ9(W#DIX)%=oGkR+{l&y@B`#MIVE^=s(V`f?BT25 z<_BT0J$cg*+GO+!i1#AQh$=1$*5kt}ER!1&NIeDWoyj;e<{DHQ@~0155?-Cin?aT! z!01q9yIyS8*1~YDB}*oD`cij@0q?9pAT_}zQboLHl}B``>Ws6XoF-1ZGW@kjJ_75k z=Yi2o-2#jTO2<)0qj8tdOic9kjm$3Kt)u$Qd06L!zh~z1=*6i|cCzFJj29oK4E75w!3v`Fm)pO- z`1JF48~fzgfAZ|`^njN0l1e3te4lSu&lrDayK) zt2FlnJ7sYuF<8KyjH5tzfO~sJ1XDT%Vb8P9>6e%F7HGGMivcf@Do>ssZL?}MNzCsw zic-1O=0p6Boht$02TJxVuCC(rxdD!&xPYKm-+c+AaQ_FlBftKE*?oCry#FgNOBnRf z(>wW;hWB;0e+*DKRB1QEd7MMd`EnJ)36r~TC!vpk-K!KOxK9dPjv}Eez<3T~l~tbx zlc@-!x@ZN^+(tvHv{%aIqo;fGdXg|2m-TGCP%7q*H6j_Low~W;!lzg9dr8AumV`$V z_pI%i?5}VC$A1337d1}aVtMm^$7J8&l>vcckyV9+9UG{3Ct2YD;*c$iHp}58maV7l zL3s(r9D5Mj#U+L&c1OtWDhvg_xDb)D+l^KWY!OEB>gu{^TsE%9%15!-^Ndv=&1T)Z zeo}8mmh?`zL|S%fk51<`od?I7>Sq@%!DujzEDyGrWn#vY6F5XcouN&^}K0k_;lYXlRVG@GDo_mn@NM$2nyT*a@26TY)42BR>7%0> zy)9a|p0wpDC&w3439px>#FE*$v||~9d;eU!DBS=4$AA6!_xs<7$ol)kU!R@tyy1X> z&0wjTo*ljR-rDs>hwmfrD1&Xo~uLetZ0n$4yKZo7)Ne|Uq$ny* zd(3IedAY#qZys5qfAhci%Xj~z^uK{-T^KtVJ#LI*?KT)p93=h^NO*!-F*z|Z4m~Y; zDK$dr9U8|;xu-+EhQVmdIYMDi9SD{%a;uq4#$($UyQ^+;H+{8l#xs@~=_xE7F9X;K%oGT8m^zuoKmm); z8Ze=-aG?}Jpdq0qB!(2SF>SP(bTc-=*yN_(sELj7#ZBMZKVi?*AFuAUo3!%^UO>+0 zJm);;`+d$cBRAr+uMqHo?msH`{wI$gLKXtBenBJv`@?VC`F@J@S!GXOGAP+wqhho7 z6Wh@Gf)QJ=Zy)`7i#ddgNT5tl+a#5%Yj?_W8<;IN>J>zqD6TYOwMO>gk}OL^IHSwa zOezuO$FB^>YIRvMvJtDUL~vs^KiuA{N5IncU@R-?R#No8IOFr5pEERZ>d3k7t}`6Q zbPyz$nsUs!F!qPjzgeud-fkc)Oi2{$Qi*n(%$w2jZdJnn9q2T#$JHTnoR+= zQYvyc6}d{O4CpJXm72G)E)PPzp;xBV~d)Lx=eL!~DFb|JL)Tb1uU( z2s;=;M5n@{r96K>Q*+yY${E3=$vKTCTQzv*!tQ~MJ_KBeElUO=UNsdfv0}7^fD0y* zkMuPfQ8_hkk}Hil+>k2)H3N~E>Y%#LqLCiK?vCl8DKBxYCSgGC{~ds-nc}k40SiuR zB$c`v*Mw}kFvJdh|D~1qsp$TfQ&*n9gH*@h0I4=0_4CA(S#aAu0>LWxc2=G66}4OI zpa83UiAIH&v+?yieWB{IT&dSuEx}l@r71jHMEhEfHKH+Dz$K@PpnaH8qoNF*C)$s- zeT`Lf!Kyc(g@F6$i6LO75Dcr; zwnnrTL%BQJ@D7sMIkncSpLvv$Afq~retr0$;m5jP7>3f64;1_WO@|BQrsj+QsvT&vVR0Q-z=tJU}byfTMl=>jeWqB9n^9vR5a!~4JarklF! zXocfy(WM3NxBB|}?ubNgDunxPUS3HwW&(8e-c6#KOmc%4ciwx)_H`TIY`kE6tLrSu zB02qd2pLw-3H!-=JaF6`s!#=39kop8~wpLclcy9G<3(S-Bqj2rOxQ&cc8% z|0uusNKyCYg!dm^2T8aToR;8#27uMA2Y2rDiRyG-!i67sF9pU|g5c(`3ns6{)tx&7 zx3zuEyxVxi_}=$Y_~cI>{o>G6+2g*iA;ON_<%lVQQo(n71RUjgd73Lcc>BH@ZlEQt z0GFC15scMi+fRds17T~E(2C)RV=C|UF4h)2W=~=cK;#58E>m-MRf%xlC4vfZq>u`5 zQtIJEQMfWJ64#S)^3smL?fXu4&c57uIV-ICw9I3(eLkbB>B|lMiACyJT5i%;1@%ZWKG^x)&cHqF?5mBJMrqxp7Cd(IV^e0v zVqX}zC%}R%Pw^<0V{28vDaO~8s7f%*YDz^OscQsWEBb*j7Kx;) zTBYnl0?t2YTF$5942g@*dAHqC`V6c~i6pX^gdvxuTP1i|B1e*(_FQ}WzMX-MuQuKp z{k>YF)2z2FW)>|Ta|RzMIGQ6@gAqvWL6_b(uvoM`T?({n4LQ(4G3DzhGfUhOBptE> zT(4E4c>~N^b7Ny`q|~wi6;`yMTYRI|k_-l>6jQ#UCnkp&!J4L#R{|lCaIL@3 ziomEQj4(ctQ;N>^azaGQcmN+A?YAqI-80k33nUHV!)t(xgM%lOx0~8_0(L!o@%jzm zFpbG=@hqBnz1?j!q8#1-?|w(K1D^kW54v5HdHsrAF3R;l0A((|&bKb#8j@Qfn#O{y z3cvltrXW$UrUe=;W-S0zuMc2`aP;w{01zfy${=57_x6W`&|JWRf}>Q;z01?c@(qvE z6or8^pBsqVzc`X-FJAvjKs7dFu`D22X|{VLhMOKrj^rFd$L=fAAIr3V#s;XYZdoqM z+kCC-$SuV}G!R<+2jD;>Wu#NNbb3;P1_Q*jYJ!3}&*tW)K9LD?qobpwFv;6Dk*zc9 zk%OP9EPAJ=@(zlWBpNbmKpDjIUB`RQesV}{Uj(M`3A4w-n-rrIB|s>c8o6!C0dT2j zPu^>4Yd?LC8dP;~6A^E#(LlRacg@;rp^-M_K>Y)7(U1Ga(qmSg1pIzTK@4bcf}6-Z zE^luBROVCC;OMBva3hmYS{LS{2sWReayVUsT9Sd;5QgIeS|q>EbR9i;;dgKIQI>W+ z{pIzq0ODS^z3kOh%pP~n2%EwFk!CQg!p@xCoBpk~eWwo}IzV**24nq`j{mW4cD-q% zSsV{ZNjj5ESDn60+nKG>rrUP5?P$B2-OfeUeXQCJJQ#sn+a$399)d~;3~~|~FblyT zHM0VDA!A$+q6mpZid02}Do~!Yvo@^Z&g1oTF@= zZ0+~^EC&O9C8Q{bH`HUORHUS*6C%unp%f*UVih2NC1VY_?RxiazI?PO!Ic#vp}lMl za4Si7#LGGyY(`$5-#N;r*|Agh!2)(T z;wvwplBN!eN5UJ4-hQ&8bq%y`U^s7OWYkz7yd z9VyS+Bj%$ijj|#RIC{G`U`Y2t?vOAE1Y8Cn}N$t3%mCNO}rj*jG zV9WF>6%iOtqaKAo2s@oVnq`4tw^Lj=6G70GuJXFPw7ojs^rqpdfzq+*v-bxJXsG>1 zz#NCuFkd{{Y08!L_0=vanNRKq@l+2*AN8S( zT(~OaRW)yGJQ&RRXaD3AWUbphsU@qDqz4WT_O$Y`)}b)DC%2#>cX$#=RWjnw6PZ#y z6$Kb!x6`cDDO42Y=qU7hXK8i5-0j3jb3AEo=nRA&9ldfFS}_X8ZKNpa$(OF}M*>tm zo8K0bDQoc3P*-db@HED9BuN5}4Ni{S{>WZba&oTHGb4})5=JS8Gx{ICo}K;O>^Gv2 z)3#1dQY$#j13V_zwklcWIBLr#JuQgLk4=zjkuXePcfr$uOgPHW6op!jbf=K(p+-wN zD6hMg+qs6sjh!+#bf(UJh7@jLWVq=?CeQN%7S#=rjCs_B=WeHn4KZZBpQ%mJ-*2=4 z7lh2|iIEFu95K@fbLgX#0IVBo9V2kSPJj8wZ)BFtY5ND#ih~u4lARK~$JzMWab~3& z@Q0zZ@uE@S)gpyb36iy{)Cv_NOKaLygE8E@mm6g8y!LS z`gKxNOGP6=Vphjj=dMpqo4_^0#&8_WhT$Q{%SW5L2skqU?(ju}OvOx-24+ph0_@h? zAI;99Ium6XTA%sIUJ z=f61<1Qo3oB@r(_7qd&7Ks-=FT6HCp41}S(gO^Hn9M=7wJjjcHB54|9V>8vaKmPpl z06GY1g22$@^dhCZ-J>@7MFd>#0)iq5-)@|s7Y=QD zEmW(n=*d6}y6b$EW%NM6>4^&U2nt#b#7EGW?~W<734LqE#{dGysi4$xR*=v~O>Jjs ze%%8I?+GgAd%fn~oA%4)G$o3JgU}mx>&5n#DqrwK z$&9n$ZxEOiMO_w6g5WREgU0y|f~M>|j8@fvvaj@%W?L70sCwVetv#yrL!kp1ko7_a z^-g(f@VsT%0+V(B8VNQxT8?vr+3v(?km%(qNkeUme-L*_lukH$VsSqpyOgB;#3(2h7I>x4fSPrNmM6Dw&>a>=eUIQ9UZyET8mv- zju?KFn9`7KIG;LoB6txc;t7UGYJQYjqfZd<;-g|FhBs=O8MF|vk@Th@E=F0W#|KcN z1uP8Sdi&m#qd|;tkYE7)3-zrqi_sKs2eEnzkV&L$-yPqSw?fVRmXL5G;yMADvzDRq zesPC}CBMOt@wG$JoP$~{x6fetwiZg3j6hQMqu$u*jd&yynfvI-XUtggpcf=qzExa& zIJc27++;=@0Q+O4R|Pnek+Yn~4%ZVRw4R_?JJ4;xHd2xy!-4`E&F%(lJzO}X?(vufVfhQPV-~Kl%*VdCr zwuMEcy-+XzYfnwpyw3D~ zxsMU!SJuR+-7#l|0*pbolNP=K#LBjr{;xvXx7YmVAC0Ht zPE`~+MSpp+5?1TIpT<~y)agmF)aS{`EMz5oT=lHi%Hzpdr*jv)I7>&s&f8Y(10(qO z8Urr-KPAlkFTcij9L(vYWU3vt0dLBwpx{t8oHEB#Ie)RJeZ)g)yC13%7MvvL$YxDG zV;Zq6i+6zFPm&NV@A(g!n=A)fT@_Fg80HE{@4h5ii4(#1f0JlK$tiCTbm-;eDbeg? z0NLT|O1NfS+uX_QqRTr6nphPJZho}+Wj7Mi{($%?GUt9(jgxN{6mz`14S@3)u)1M1 zW~;JC-V8BhBrm2ltf-)@x@Hegsv+bs(i9;@&O=$j;SEIdNKnh$XMp`$eZ?H{LPR?g zxPkUE7{(U#@n6OWy*u^JbIe$Rlas3qx`E$ir>>vq`rw+{3UeP_TQeauk>pe^LHX`O zj_&LjMnbK&yjm{@=jhT3bDA~tTEo8%b+`#QDOGG9j~Hc*G}t7p)oew7?WCx!0n-?? zLO9G8yb16_i|GVTS`1=+S_U6`;GI*gktQr9448|smy)tDyHBq5`DiNYm2**w&ITp} zcGT>F!Z$9M_}#^sL!0wG#5HM&cysV%x)=9H!?$BpYS(7l&K%|GIkV9`7cABr{kFP9P9)6GL~J9q_tAWIT2)PWd_9P3kn#c}~nF4QwC2 z;tOQiC~ruX*UOoMvX)FF@?54-OE=1;Gr%@EcbuiDNUl~Z_;yZ%h#@FJ|Ga1Am`j8I zI?e#b)bS*w3RIA*28AcS;xp#LhoBig%9fMpWYv? zANdbbuf=j4Yv~a9;XF^%1QYh;)rJVa3>G45XYP)UKBsYb<2}ujZM@sFc(^~A6}lk2 z3F%Q#h%~e|V1Fh?(TN-Y7Tql0eZh9&Cfq)sNiXGXVL2J1X#4VXV2QW(w%Q~+btAo& z?AF%II)@{A9g!g?ltYDXKN-^20FNAIN-E`QN9DCWf8(G3nl49S4SPcNe2_Mxl8?!Q z#5!)QFw-IOx8cFt)$+ENK zr$>gY1R6@Phbqou9YQ-?kIz+#@{XOL$f)X(So=!xm@KNo>N54}b=C_1e0})lt)Z)T zDawuqUhrnh2XMbLv%7vZ?k1^lqg{fx8=6F6S*hAWxCnP%h~8(gc1`+Vh4VN!LZro% zU7nm=Pz*Sp&pO$k^&|b7YwF57ZrinWB}BVbuj=Zs!P%3hx z4h$!%ZpO}J^ISMAnRW_Anc~9a@)G5K?PhOvv<6^)>>j*7#0uCjv`*EF+lPzuTZ%by zh8I%SUcDeNDLlEjiA)zd1jmK zG3(zZT9I)?lfIjOdh%0Kw45nxVl^%iP-PsNn6%a2Z(y??}qL%)U;|`C9SYEaM z`r2;At(olumOD3xdI?D1D1t#lc<+TNb0wp-89pdS9l;y`&fy|_0VmuE(L6>_!Bi|9 z3r88+Vh;tTr(GudDhNtB&yO?7V8GDmIbZWQc(;$L~0g>zzmbnkQD= zq?m|8I(~Ps071z`jnx+u6BCj(xym>ajVc-VOzr$)cz(YATaBh8z;h2`X+5Fj#qdGY zL59DG%8Z8Sl1&uVT!g;BWkkTwcMp74a58BRcN{U%D6|k*SW*}(>i_s?= z&ChJ@b8Azxvx*t1In|k<(ETsD1K!+#6{Xi}&X^o!sA@fyin<9llG-me0gq3BiHL1_ zwX`q$GE4raHES>w;Q~mVoL9_kkk+1W zcEa$$Yhy%LL@e6mO+_%GpO+Q_s|qp^eS;6b^l*3Tu1S=#HG_3D&e5br;Y6=3Lh*kV zv>>Q^tjO8$Y)F(rNF2RbJJQnO*u?mF$=n1S(U&~wbS_A(uG$e#Ty2eXUGEs~C3>Dd z#?xQT9~&7Q9rFI1!>v-Hp7(TjYuIzIk z=ci^?n)MJoDf++a037#$)j>nIZ{M{NrCf0@b954ZmqM%zcqe!Q0gCMD|2Vtar>4>? zP6)Svf`|_2zO=4Oce~Cy9Y1tBwKKhWvbpz$+}xO4@&Y063CR`mGI@dUQWA*c3Ivb< z8zh4;0!!KHYPP6DQIrp8%c4>;PX) zetrpBH*%|U`qhHGox!>#Z_dnp&^vLPcl_ZX1{LwZ= z3y`*4RM9vN-5KXhd&=|C9met>GkKV1{>7}GJ$9hv|1a*7LY!_(52EW=A>LIq^+!jf zdQE&T%SssL)*^;8Ho4YVDIE<2!cl-1Zm-9EEz*mm{G|q@I+kkuevi&%bfjz2Bd@1_ z-pkczWjTjbWccyMMl;O>J){H>XrQ8ESwK*fL?WRWmMh#_dZL0lzOK_JZahrbofU=q z@?R16dl0avJ<@jT+$uEhtyP$WUGaF#F`CC@xl9ZxBOL2I(-sEAbXU};rKJUj_wTeX zHkWsxK;p4&d$ghEHbsQ2F6hx1mqEcpet=xGoJs{K_R*tHaVd;nbl8Vfg+_-v+P!Af zpkq0+nZNnpb^Fxpzmj%FN-+nw!xLJ#h@qjJ2A+=I3#;euUQmP4URQ6gQAOg#*&6sA zmfz9TG{?ZQS8H*SczS;-Z>LX|vU!~ietD~6(PkTrod$CphJ5u)aGfZ!89bsWOp1K4N`C{-emPa`v$^lDGP;0 z*=}djIi1($Z|FlCXlj;7NeK1Pg5;YNJTIr@!@b4jCr+NMP)Vf(Gu>II7??qA`dai| zTpKX63<2~jXV3oU^3@wjdtx?WFG5prURmxTPB(nFezndOi**E9SbhLN(n(PZ)s{9rOW}JPepLWyRO6FQ;cq;~y8Y7=6$Lm> z0du#uKj0S6%%s7J#R8GoEM(ZRG5yq8_;vNp?8;yXQF29}E|;Hf4z|cMR-5!ECx-ujmmoFjJ zA^#kpo3ln)kmBCZWkFB;sJIYkq>Oi9$PuZn6@{z1Pkm;Am?2^q#%lyMc7OKjowL6? zJNc|<%04x@q9=uwlArJU@F1t_931~*$l*A1;Yv(GwbwBmn`-PDk@8r-gte=(%E)K_2cl`sP` zpElAq4De|)U3&Db0Yf)E#+YFir`WJ>nkm|!A)lfxVf-bV{L8fJ_>qS)8y&9dQ6%s( z9fHltdG~M;%VL-_A~VhiP|ylQBDYc`aEJRX-Q8VPVdcz#mt-XR`Q+@1{{GhU=VSAm zBUPzXIukzu#pI0zA9`vM@1}yhT*V6^|0Ex}VON$zeU9 z%5y>;eLJnAKi3fDMB6c^Aem4lq^NVmd(BX8b@ zEP#;R5VTlnQ3G=1-rtrtbQ=Ab)V`ju_awNH#}M$hjlZb`eNfp|SNF+n<7jNB2x|wd z?K&K2-P$Y`vq0^n!X_N;l~6*f@FN*w5lS9dnD(W&gu(jIZxWh&jjrkzK3BYR5>B`C z5nMh$I#cba4M6|>3}F_C1>2AzU|5H)sYsx?w{b1im|Fg=j)3LElSB`r`_p%g5byHM z%8(81fBo8TX!wdEymbTG=H6ZvNs{J3z)XMqwtK^nSma_xuh8P_$Jr8IZbo+FuNnN? zm0`mYNwmmZMn_ayP_oloel?o{d`a}80>-XDTq_7yb)7oZT@?_dxf!~(Km_YEiSXuA z2)8knx^|B-+vi{0Nv`*3Hok)fPCd|7%4+;y*2yl_*!~$4g1I`eMQoj;G#0hSqFhMN z!gF)q4lwSCyMmH_G~LhCAj;*Ha+;wA8|3K48g$tJOgLPMh!o%H22T3lWF6ka;TY)= zIUF-0C9){wE5uXhyDj};CF15svkg`^n3bW4gco1p&5b z*l+yuJ~YLW+k%u?p$J!XTAh-S#~YO}ilfp`f05}J{Z4@Q0;e%%0%4Jp%yilbT?_;C zH>e*i>w_Ncgq5MuSluGoo+!S?%xI#I2NDBa%^6$)W@v$m_dyQ*0kmV$&tAM5u z0tE;Anjl933yiA<1VU5+N~Oe*C_xtll!U1H8}2=5Pf0Ivf3lGfW;`>W`F!Th&a1=J z^Ru(_e20D+fQu!^|M12!`qvxWsO#fsSDBM6%F!@E2i-)g)`}i+JZ$OtH%Yca)?u%r zj(+fePw=DDi(&`H&-BlOA#RN0oDE+z022S`lp1GywC~Fon3vjIy)cUK8p{p zkHu4>Z6YOy?|uJu(+_JI{4*HQXv>eKr=7<_HHvccr7IN3+#6pC)86`^60lXpB_~h) z<--Fn-HejcV63s*?=X1MQZ534);nXWn>`lmqz?WIwF5leZm-m}li;maF?#P9y`;&38xV{X9~&Dt{e zroU?%Z^2{eF;b)PdWGeB6&W;E`n-MBP6`NHj~{nJr1fC{T`R#A7N0qB{G(qgVQzz2 zsVhxm27@8j0AJpLw+jIVBl@~g2if#tj%?7Oh|YgCqW7*YflvjZnY*kp`bW$PuL*LC zE?<2a^sat?kV&ENN&r@(j*zSK?%MeLg(@FlY#S>nS&k&e0*R$8GtS9c|mat)knZ^dBg2oX$J=tEmA{Kss6>BT=}__34-sQst3lgOXR` zonCK{)~v$Nh} z$&1x>Ua!5nf#kZn=JX+U`|L+c@eeV{b_@dkDjDEexRn~~}VNxnH!`JLKz|XfR z)$(Et!&#QdZqRs4i0#jf;-Cz5aTHT5^QF2((&%1T@%su14Fp_RL?=?9)JoO4Er8?`v@UpZZZ0>G| ztPdl&r@Pxi(7oz2@xc z*Xu2W(JVPxYt_PUTkGx_AV7QN-ue3_#s?ln# z{akaiUt9Zyns8Q!Og^24qqs7&Vq}?wE?y1eUC_li!(OX1YBU<7PIH?Dpzufz^gVY5 za9&@KOuNqGsNMR`8#EP$R?4DYhsAn0NOAVMI$fh0Lw^}Et-SY&95^^cx9WQP{L3Ub z>%jlUs2qCPJfj#jm~a31KvGrPf{ht+DsIRr@K0CJtakD}VpN9zgu= z7=$V{+16LnU20wygpC&wAW`Desd&l;81#B$L1cwZXm~(oY>PXA8;J=Ryz$YQ!A;pV zoR}J$gLt(tI5d&T%s+YX`0?7jfCD9ctKSNR(ya=McA0nBE4}6Z8WhoHSk2+*+`T{k zh=)JLh(=>~t3;|PtgTPe+fEiZPUfgLr;3>tuTLVo(qtot6`y)d?LK($7^|B6(PEi2 zBGz%6fUE;zGy~AV{eidNKgwc&Zu1*Mu-qzWViRU4<{%b#9&5ex>$>IWZu&@fvUD`tJSX^~k>vkid1hooHw$-4B zhC)d~$^*GLyxO!Rkj@bw#kFDtI>ca9Y@e~FEgviXI0tOKj}@4pi#ZQ1EgP`n6X$+y zGU{A27y-{UFv=ADS9^_LRJgB7R6}K5ou(O$qCs;DVe-;Jz2D!4Q?x!!NH5l5hT_Pv z&|qqDcPt(VBxO08OpFfB%!s0#jL*y@H)JsqSeY7242v`H8(5I#Y(x%k&CV`uWk{U! zHUeT2En&BRrFGZ(1t^phCUffJ}jSo zFW0$1A&_7eQyoq+oi4z+GsjVTG%xR!UWck#ixw-~ggp8)f&473U54%CuLly-zoQVaMKmi-8iMvAegf)E86RHaB=2$U9Sr9cFh{;x__GXs4VU zd_I%|__EFn3=9=y21G^~ff26) z5oyJ1kwU^om@d-FX(X&tm{T`sY?czLG;NKhA<=riu>MF|ld5f6HSXVV&ye2Rwv(9Q zg^7N6p6By?KJ$FP-`PD7;-%uk`r#4K)7<*{^70%fl!}EyG%ees)jUyIdA0q_VznZT zKQh$+T}yq^eaoy*fn_dc4g*D?WrfYI=n z6bM-OI(ftPg>&!x$Ej22>y=}UUdQA%r!N2iAOJ~3K~#@AC+O&h1RlJ3>*h@xGSq25 z>6_4xO!@-;NYLauS}qAleE_|uP!<4Yw0c~cehUFDEg#l^a&klq(-~p$-5qi{TZ)r3 zC#Xlam$sikg75*A3jDK5B)qjl*md&SG*EVCcfj9n?;|uSMK{7pioem`uYP%D!%B-0 z4`M*r%g6VAcM++1dncv@o__DJ%LRX^COwc82!-ZB?$Yv65L>&zD~+LYLc*>&3W#i1_#ip|H-E2t!88i$h3yAna}*3DQ8*0GOZrm1S;5y7_^2^TpvF zwKnNwPJduEL=7=(3560orX~16h;k?tnD3E9o@Fr%1J&VZx}(3^+tTz4fpvRH0w=vG zk8JM)UP{>8Q-V-DDMItf>_LDR_xE$V&}ZdYQK1NtJ&qPZKSB}D&CX`C8B@Ai&1TCS z(!@Do^wqPajUf|sQHZX`ALxh;4K{}iL_bLPN4l9$fjcl|pE=wdlD9f(Faq^N+-`Tu zMdCHUI6tXkZDSxiD9PSA+^;4loHjM~f#n}xL7ifCQo!#|NE!mi_`!Mj43~^t!b}ft zw>f5=&(1C`PQN+XjdT0iDTwSs zS;#}JE>`m-L^fNCtbEHr8ydAf*;?9&RjfEJ`V~fxM}8g@XzsW~n`UmyuN8OzLT6Y_ z-Y_dH3TFgq5aFEwxD#HAq>=LZl?@Zgw>CmlaM4B+5%X|RjYk0R2iLoRZ8~G18H?ZN z2i8J3j#!&CXlV|Fz+)r!KP@eH&2GA@%gZ2)u-bmr=_4MaVr@;;W3nABR|Q&tj4niT z`8b!)%`P0C94j0<}VG@6EkSRBzLPIy1VJFRWx$)uBP$2AtgdXo z*m<$EwPFI^2(_v_p4#|Dp1`O)EET*gTmPByww6Z}-eXU81UlU=LcG=435pIpprN6I zH9fue(nLy;kj6kHop!G>Xtwus(If4Ivi|(Xtfl?zf(sF>G#~OAeTl^UpvWQ?xY(c` z1|R-lW}s_tPfar^y*>$ilA z!u4reOD&_@#%*d>dwVG0HhW1dG9i+r7F{2T#bVcGf4D**e=Dd=QRfV6iLkvLQ+uza zPLpT8{>TN>-+uP_S$QLzhUF!x!=Uji)(|z|XuH!-An*fw0v`LJVNjmxs#w$NURtX> zI0R({9w?uis+M+-PL4oH<+HQJ;%qju<&MA0Ev&Ir-1eGur?H?wp{M{Z|K0)8{^Z$HS>VetGU6f2zmi%V3aGT6yhJkgy^|yN;$g z(jT(K8e$!xd0GQ7X+d`N(K!$rncOURg7xVIkRRpj6wq=8EN*soQ(!L%`KjXiLOP$% zaHh)E&VOIrd-k$oC2`5Gs#R?()NpM=4&SA5QlhQ@yzTfOXV>@BMs|f8IUx^`HV;*m zttuYUm#x(O7c!m+GyXBY1RUD~cqZ5wOuX1eHpt)r4#p4>0|7z|iDSTv0XrgIXG_cv zXS<1AD~lQzQJPwbNLa-I3JWItFZ5go^=Vs$&I3q(nosB4?|k36XYOrsls>4ua@#Fa zOd4#AL?)w2q=5B4*b8t6Z}u_I-#*@E7@KyLW|2g#mdGa`wmB(!$e zclNhu3Qw_uM1)f!qxr^0Af}ig0i}x@uQ&Aw1psFDFpUAnw*o>a7+y<6=I1womH{q^ zM(;{6Iu)W3$%D26CkRg_WPI}S?{D9M1!VL9*IL*p7HhkPK}(E|-nwwH_cFn^Ow=|M zPT8-VXry$8J90VcbSXKCMufWk;>}*jXbu1B@yjPHYnAp|-WGMah9V8EazW7snU#@@Yp!IOl#18jf_??|XDcnII&@Oav!P=wnR zveC+kvad^*YMX9<#WNjsdP?`#RO;J@ooY%(DOgSWU~eC@jm;rx*4kNmvcH8UoQBdi z@D80Gmk1qur~Jf6oAj&m=e|@RL?dVY!#f<>-HZ#YSX&}ix%xDS_U`5a-klsEdH+7M z25|*x1$8$ZTm-)mj3@ml5T9BMjBUSs#RoF6sTF_dAd1^tus#UQg{sT9@CJDy!n4(d zL+u@-))H%pUQRu2YAOO**-+bXhu2sp8Q$X?xS#U*?mNaCX^tnzB!5~17uR!JH$;E&$_TuI1*U!hi0g#c#_dyURUWR0CC8`2X zRkeb0RyEs43)dDnX03LsouJGUMYXv9UU|i*VOrW+C}-;aq~2M5Ej2Tf`fAk0@)DA5 zZyf}gsTKF}`19rZ`u=`H3r*egIOXX0=O1FY=TDyjrOuHZ!`kb5ic(OyL~N1M*)-S9 z6k*lV!d_y<8&G3`oB*;4&u<2dft4`SAwNXeYxM7A1x~@gfqMJw)%KsDKqMwK1mQ%87}k@};t}h7peWlbd0_E3%m6CKGKN z{n|G%;k!}Zf$!B|2sCY7CL7Bw_~s(e(G;btvmnv2uNud2|8VY%Qb~$PSM`WxwnQnp zDi~uuu>gsWbzsrno?y)E#});DE(BqXtj$H|6NxRb^eYER0PF{6w7b8*>E#g?7|))) zz07;JHm9QLSS%7ZUj4Y4i0e^v)lF-u!{cz8sK|Gt^q!8U7<^oa+FiIuvZ4bF$gsmELdU$J=4g|MKuAP5 zEp7E$yR{if_A+1v;sU|%e2KWN{Qw6F3nWD-;uiwRscM5gOrSYPk&&~qlLaoq9LhZl60ndBIjBwSt9r9(JA8GaRPj_ zNkk}+h;dar%DbeHWvlYQZlWg|4I$&shpeD<>jh)(r|60x#O7mwIv$+^ym;|qUxcNV zUzmcQ(rde0S-~{+=Ivje^UGO5*vff@om_xx#ha&Kj7o+K5Z!uhYgNtQrAxhd9MQ(| zBe$Dsm&)%pHQla|F+(=Wmh!o{e{6Iw*_+3g2!bJmt&ErO?p&1@09&ovQjf=Dn6=xX zG&SFIb5xESs?{eIpZ(_36Z&YA2q8JmUc3wy7Y~1GXIpVQ1NUTP*$-*RVS;0YY$Cd1 zG{zup*D`|Ok78wY1Q_p^kR)&!$gE&A8rjPjCE!Cyy$0#@q=w-Mav6-Ty~F~Qx6W;~ zW9(Mo=aTDc#1PDCSy|nwQmvfvo3V!N48C?uptTU$Btu;Kw&{~;OCiKyzLyYj333*8HVHUHxR zBz8W(v9Zzp!xw69j4_XY{b*q3+GrzF^oExR;Ejy$ zV`(5WIcv5{t>RU^SuEA6nt_^CrEQ+NVLW?@l_kI}v5~ZdB05&9#NYTHfUIn2D$;Q+ zW|?ka#wq&(2l!)7!ymxiy|A#cvG8+e3pu8~F)-k%C{-hg2o<0HFVpS62>18J5M7bk z-DYr$!2yE8umhWDc>9Mf{PKd)yBJGCf!#fv4})Yx$eRpC6Y06wA_lw&uAcK|1IDp; z;D_G6B9YM>Slmpcf!C;MsoknlVM0*BI?;~SVQTJe2b#hS4wY2fs-$QM?HV;SS4r_X zpWii={cDLMp%S@g?n;ypV2Ci}bROq)98LzcQu34wn7iSaEAaDQG5 zR3&e8xBZ*3^Z!XBPvdxRwU;!PYc9ET_mWHY?zh_?F8d!a%p^ju2RQ&vpVLWygxwBg*A-L74{h&9z3?Z0r(XTWVQ*?yTs zLev;vzMtp$JkK-FGfn_5-)Ip7bYz{z4^IyZ*4)cIKnhxQ3#TVBE((RdA8LJ+JxAx^ zeF(njKWNm^s2aNesBt%Ul< zXsPegM~3F#{pkY6RMFV1%Q7y6@WGFgfp-aI8eQQuAM+PZU&bKbT0K^4ArXy(sNoz~ zc`gC<&=%@2!+8U#M!{(O{@IWB|NG#fHF=nV!x?}RejPpxDJ1WxV!NRkVwJfcVilU2 zhFq(u-l3)?aJn|vH21eN*yHBz0sirGCaFVXTb{g&Gp{Sa*SEL9rXhB1@16mFrjC1&@uEULJ9aP!tsoi(wm7zFXE{ zD3J>?P~)RIkXnrKpC@c^fx-5P z?Py#L#-jjyp(#q}_c}4K>}ae~d`A3k-HAbIs;BNpQXH;ogRaKp1-3?cVdwP6m;*}rrg^w1(x_`35&8Ls}4*J2X zqf{)6M&;P#0mLXB32`}u>Oj9W83j3yimdM8lfVD8VLaIPmtv`{a15ebqqxJ{Boc`P z^EKgpRNPZR=v4TzS zZl6iKGfVVNj`kU9^zGDzFFtqxBXuu*`g_Q#z<*rbZT(Q(dx2S0>qPf=#4?AD-8)Ie zxqOOSjHXMuS4S~8kJ)gVWoRgVJO#|^2f4vrAyCJ;EEi4fd8Rl2^M zYdxbyM=_8hoy!t~2g@y!cZE~tF`D`Ct_e~XaLMXw)11qNW_(cut#j56t00(Mg#fA{ za;K?iG=G#`jL*kjo*b=2xhUr^rIV~DT-Yz}v+PzVjY7*3O)~JrI~$uCX|%5AB*D+)JNc0QaeGneBG9L0ym9X+pGC z1I$Cf?7gZr79poxIa+~4I(2fC0$MH_o!^>|6^r?N7MvR0%nlkUmNByEkm43&kcUW( zQYmJ_u)ivBI=JsAdO=YcYHKtbuDZQLs14X5jK~}kg0kV)YLt3#UXsCjhJ9f6JqnzW z;Ep-hGO3XM?bl=S&bN3$;@zFHP4jWO-PVw~E9^5*Y2UvOSjPSAB9!+O-CE;nFgW?{ zA-{rA_Ft!W$S1&CmsEb27-daPjT$>Ve) z=9gfKo2p@rLV=qUWDp}C8-)Gc11@Dhiikp#(TIlY*VPn+sNN~Pa{LNL(YRIwxBv{u zT8yTQrcPYfHM!J?fBXNyXEqH^yz?z_b1Bd)ZkXtjzIPt&9Ix(02CJep3qwsqUbhpC zaLdZmBIWUX(79uQVT*axQ;){Ms=36fKNt*#kGa@e)6kxpMQ1{W5{aL zvc=M0TMo@)*&B_~4u(){a5B1F5#2@Lvf#a7z*d?KCKK5#S2_;)fi9c_YWe2VC(k!G zwJMX*liOP@ZLNB6C&IFOYk@(~<3?>EevGz`A0n1Tgq;jc5{+|uy>hulPEb+}64%E| zj5LNomRJja)y=wsw&*Zv*#^!*(k(80lAzsA>G2z~~;*#V_fqYW1zJ zzftz{4l9${-A#g?o?&lCWdRadwXBtp9~9EueEj%0g<7!(r+ZK!t)y0>(Zp&vnTYKt zN$JDyfBIp1+P_!ai|s=unMG3@5)(iB&X2##>MDD8b+U*7wX(#$I3*!lW|WqRwq6M# zp#fL*hPSLSTneHyFt~ItletmTg~{bi=ge!Kc@2dvc-h?^kuY~N6ZSUqE#mw;75?Ph zr9XU0lGV)2#5Yji3oCF)4a6)Ma@Upj0mZUb%64#)T7(D<=MC^O5n0fhNTm|TrPAu^ zaU#ma3exFE&z@{f7Y_FlDKxQ?f^YUSl67I37j3S{_&+D4T8Py)hUF+#Ahg2FH6Dn~iGx3owJiATa{IWt+(i8X`5y zM#*{R&!3ymz66KsQR~YayFA$Rg_!`N1nbWGybnR!dg6 zY^6{(1!V*4IZme&iLIbc{^HsFM;rKlp_I<2R#K_OIQX{YChx6fhpp^(psuW7xc0FL ziUc@$&3f~kiIUK`NdWTlv}eS8uvV!QggOwoJXJGr`(7ZhG&*{x;ffmlXMt<4Aw=&W z!`?wpvhKD|ndeBHc?U4?USz7O6{6uGH`?ud16=;~P#e+PaBP3~{F=QA^?+jDU8#rrOZqunM~@Juat*|GNn?PPdm|rx`JYy|8 z1eVtGeA3TAE7IsD6xFLbe8q%@k1)>6l*Y#X$+t~SMIu)UgM~r?k~`C%^)aoq!IS;@ zXCDzN^?M`MD1|Su(i7^w+6D}~EV0_3SPpTMU=%>*pp3v{zzdE*6@Ie6zqyZgSK*kpvgdFeE^ZBL&4IkJ7e)qBQ}ts0 zh*iP{79WHL8*JiveIG`EY!1t~UsNoKJB%@rKTnU1jg@EkWZO{T-XL7)%>rZPj;xPp zC7L~!&eB*?-Nbj&zCPwu;4>F4Vp=9Uqqo_nxaE*O+8aZ2qh3@#tCvi1PEf?+CxGNlE;8-sMd zcMoC=Uj<0+nTWpp*DK3~zn8fDs1PI!*1pq#(UZ@qv!-RMS}Y)ecCT&Hoj}ZSnOPdk zy*}3UK6kJ<@Zi?xUqAilqhnwVln0vD0I9M5s0h5d+@8atd->+BiX=^SAS`-!Hv+pP z#n?xxQzfD{728o=LvN3UBnfTWZH2*0F>s4?6URmQWLzhz`~ZwQn>BXAGLjx0DwQ$? zz(vf1ONCLD>aL~@?!#C)KgA`TOm`@K8t|E#bKrZU9eJ<*xns#lduE)_HjRlQ4 zhHSJf)uhtg-aa12DV9z2)v2hOz^#He2 z*3c6sZmt}-J}+JeUG-=fbR#q)5!Th(YR7S$WEk9!(gMD~PEIIdEnOn4sAa_B42|B_ z?xa|T@+1=p?y7-iD3!BL@kMQ}{x~5=93mgD!^m`p5E{+;4HQiinqcyFX&ME(ka6OD zkr8_`Qz%bPa!m#%$XibXzEJZg14~VEV!M!Cpu`-ghs;3ZIk2H&h*rh@7J447YYyDZ z9n9x$+!#PUVHoX;oO7h;lzk`O)d-%I!aYW{QTl5(Bp_qxplrL`+!3&`-WC|4D3WfD z3Vb3u!id={)n!RNOJnw=kd`7GpD<%&cZlPxf-o8l`Y^l2qUo8M$&@mKW7$@-6F1M4 z3mwWfAIdvmVV)WbKgp4@+;v#VHk0ZgA!d3+(w0v$NM5kQBdQKW749+Dx^b_qd{2CpWp zlXQkol#|D9;V9tJeF$Tifl3Q*J*U@md`LhKX;Ik;%9rgZOixD^!V4UFFRo3mzeIR= zcj7eQn%{rN)_UR(!-@_0h_<*DtV*pkswX`pb@%@B%a`vwv};|ixh?SeHwIQ_!37o% zi}Qf{;q{CCw=Y;nfFb|@5?e_`K~!5T7Lqju@-{T_cx0eO4+0$+RAFob-Beq?+dwch z8EoSdP2J#mDGid%N5pd3I!2Yr0at^wDh+XUJH)vGnA39!U{3qAG~H?+S?!;m2C=!6 z=~qXrS(04k_57(t>mpWbXj4KE!n&CB_zYQ7)=N4o?_)?D@jl*vu|qnM-JLBK*M}jS z4-aP@sniFw!|KJmyC53Ox_AuSq*^}CFF(jY^wHklh6x3#`rb|`&4dT7RwJQ-Y8Aqi zdat4yR$WzBqU@f4y9GuV^q9eq(N{Cuw`T-k=By~ULp(6!8r(vTK%7BnMz1wlN62PQ zZ|yiW!ryDp_P7J$3GF(aB0dgr2I*|9mu0VQm(hyxef)Iu?K?VL`8eyKT`t-+TRHqV zXP0pIUx}r;*A^I$i^usvSSag{(^{N(t@pNfHGqF7DXMeQ%5&G8G(n;TG{qN#6;-L< z=q!ju7!(Xed77duk87ZyjSH%ns|lX()@rrq zzT~}do~`wJd!@CUu9iA+Z4gyhq;ugq_7gVa zv>v5AzX{ST&>+QGsXjR!W;1T!j25{BE8i5vKvK${SJsa*UsYutwJ4nqIyOmQeW1tM z{3HTK@BUp{8Y~RSxedT(u0t4I?Q!BH1e$2(raUSZ`}X2jyfuHsYPF5xHQwb#{~>zJ zL|g&Py#WaDF2U^F0p7oDZa#*z-rRh*lS;W9!^7YKchE-PJFo5%47*@N5>#$G;`(C4 zgbu}7En5)y*@rR- z*B(J70lyqo$BvFmh_pC|etmlQ{=?_jTdJzQKKuCl;mhCN3^Ci(%y6H6@1!@e`Cw?! zj=nv^J7UA25{Rlx{1743=F8`6Jsg+SZlsgk=BR+P@NYi_hB1{6;6`)q}a-Y@(tFjBcVFcD;=e~{oIl$}Lbi3WD zWmy5Zpc1siYzWxA5iG~V zv@Om?ctJC?ul42u4i65_=Cb+xnukCTf=X}#(U2%}xa@lL)+*3d>1zcSa8O3r68zly zxTJ!40kAL~dx5*=pFZVSpFMl}2myOIU}xaL$g&yWw%q%>>G~T70POcxPY^MCb0JIj!P@qmv%f-^%@uw~3N#Bpf_ZRYr8AZl9V7{jV7|8zqE4=q)pc0(% zZKgt1Pv*6Tm1azr^+M3r6ob_sW|Z_VPe86(CZI0r|J>5j!E8kEEKdCJ(@#G`s(z#y zdS{7vM$n^Ug_U}NF-vFr|6J<;F73~GbKaVbtKb(vC%#kEvr@5G#9ad%Xa)Vd$OF1` ze6mITll$wB1J4mNHcE0c0|UmsJ(p)`T1d^yK*k(UWoi}HWxb5rTCKvqenlbSgF>N@ni3?6Hq$V4w{~@iA|x_0 zy7c1@Kkux9Q=>~y1Y#58lG)?wYX0J$lD~*D_4|_c2wFLK39(J#9u1ZSGZ*2b#49rQ z@0V{?@@{}P)+YxC(vJfV0CSGF?KvsOCU)i%pzm5 zAb6%Xe);#x(CC2=N|B|p+w`>w!zM<<-iS5ncL3&d6C>a~Mfx;{A$*8bW^gzaFBB$*7x;kW;Jgexa(*3T4Zf3vKXj6}8g;Ior5UV$vshNB8*Tx$<855xxW%6ide=-~7 z;motw8W}o4z!<{CVuITl{Ts}D{7F)#hbMKwe9#3H^z6(eVvdgNFJ!EuLh&Nd=}b>S zz)`>6Pp=80nvtuO7-=N7+9^ZxVoL|?&6kwD9@Ir)^^j) z#*=O{b3cb0i9~`K^liY5zMV^wt@ky+mD5wrvCDvW?ntG;4RL*+4p`sehkX)ke5O4L z8BN8afo+>AQ`rZM%XO{^44kTZv;^j}F`0}%j>X*DOduLt`ocm!&CY-A?rv`Gt~1?i z+OE!Yr-PdP5rm6~Baw^?!kPQ_8j+4F6+LHFG{@#1U7X8Gws%3Rc6+SdnVjEIAZ8}1 zP$;77A--2aX4>VL927l8jU# z!^vbZh50jK09;Na5(dB!!kR~usWi?qxqxo)9!FnaP4vT`6kZre9BdH~k$-X?#R+pHw zmYI_-$W^!*X%Tp2d{GvtR!{%PSIb^py=EO`(%5#?xjaVH<#++|bS!0)<)lH{SGB%=iDY&8WtkADyiqrH zsX4LwL*K)B`Kjm@CNf8OFLL1oGi)f!0qY0inJ4Mu+?mQyLND;Crd~e1z^4QTcn2Po zP1qUOR3&w#5XWbS;;~Ia9d_-8TkqJN>m(>ez_y|Cg4jui1c7$6-PD`^Z^YegqFB$U z=(9$G=b;=FmA8k93#XhY1G%1asiMYCyr`=neg|h?Wvn=-hj&pnLekLgh-o0fqeN)7ydo57FshI-Zis<2-0Xe*NlDK>z zo=a~eM^$0*J_%o2s5RV!v*SdXjs~X2)bYXyzN0Sgj>n_hM9*870OK4IFe?;+@g*uC zJG^VEt22G$1M$+{*wjNA#|$OOi}fk9M%2&c#8t>20dUU9LURz%eK?z#i`es&(p2m~ zC2RtaVpkDhyVn&C*lHNL;u*`wWJL;N2WNK_L|{i}`?vMx3qyDfjZGau(I{n}wJeaY zY}O`iy*K)^c1}OOB@UyWM3=~ENvwBrPbTw)Xh?JD!0z&L+6|69;!zxVd#wiZgl$J2 zsi?k9cQl%w0_*nNzVpp;^YxEW-o4X#+oEFzt{+KWZ{ou@!~=k}KYFJmf_;+7-r;nJ zkSF)o71iT*yPb<5dnA17(HWA(tldC1fQPso0GBSsvHfaR724E#>$|}5WKq(yayo+-nQoN57%_QueYwDu>~-*`g%ZoQoaPaayDf1hm-lp z#Rdr;@0v{h+uilNG?GN&#BK=^`~gi=2tn}VDgQx=^pX@+sj8tnbZEjrn>0;FFf?9v z5R4>qN?_S235tgympRVK>s;nEmu2zd;x5c#=inc*uc|ZZ4i2-IbrS0!9{qT)-uu4q zJ*zJuqiN)ZLC3V);+Gcbx?yNjdoV2AO#+Lc@p%_6Z|@b+OZoKw--^C}pHrI#l9;vg z+dn`23-Z;?Id8<4LGvrfRR`nAy9+`)E~K&9@jb6a#=<&dv^Jd1@`r^QD-n8dCfxS1 zrk9wpzVM`%_nY^5EwyDVLT>BlzkT}s0p_dgm#k6!>BuP-rL3wxtk?*WIRF-YDXcca z6A_-4&50!@Caw*nS{M$7j_kbAYu63Cazv0@&i<%u{t9y@HVKzFD^Z2{uP?cM_0i;# zaD!m4)qtK=fBj;6);X~~HzKTOiBn#B?FJVFg`V3XxFLtVgTWDp+T6I5m)9X?tco(p z#7vxNoE2tG(_eo4e0}w|#C-L64-}<@+#eKZTiYv!VdQUb zAkLrmuv3pc138@m%$15$=tD}Ikq8}vJOMNd1QVeX^?(4RbQ0+rk2`NNF{?-Ac5)}RzG>vJNh%e}%KfTRR&}K`;O7upRf+Ce?y-p>uAmCT zwh-!NiIR<|+T9Uy&roN4<}UBI{t-mxw!tvf`68E7sVS+P2D&+flH~balXe2&(xH@w zoorQot{Y)I4r07+TW)kvdn|nSWS7A2>3PWJN9y+4 zdOD}xoG&_eE=jWvit=FOUQ&*Ffv_w$I6(xPSgyQ#$5f|SPDGM(1$P=|`8Tg_t?>Ar z{l-v*xnA#=DGJ4CpR(e(ZCR&>a>Xh-@%`|^o?05og~VJQ73UqG+c^f1HPq;<6_FY? z$AN6crde#YIHeRilkWnL)OhT!Clhmf7GhZyOAJ=7GzQJ;TZ|poa=m>5$E4KmiXb?< z^CRaO?mZ>RrnZ-)WuPrqj+zd~hjPXDGP1cHb-m!7xLAZ%v;?haUbFcTy1lcpscaIB zBgW0n$nu2iwL{?t{@B(Se>y+%$s7ZS^QMuP-;m`c^FYm5{2=frkq$B;y*mHd{HT1E z`zD-mZlX@l3%%2E1R{_tcl}wxeMrML(z0yp1X?lDpz-2(aVqgFPcoBH?Rg0@8^<46 zP-}6SWa8N#MM;2#8Bn+LODpSZiRYNjkG1F3_2rFha>k Date: Fri, 10 Apr 2026 00:10:03 -0400 Subject: [PATCH 042/434] feat: iMEX multi-material warning badge, conflict dialog, and mode row UI - Add warning badge (obj_warning.svg overlay) to the iMEX plate icon when a parallel mode is active alongside multi-material objects on the same plate - Add has_imex_multimaterial_conflict() using get_extruders(true) so only filaments actually used on the plate are checked - Move multi-material caution dialog from reslice() into on_action_slice_plate / on_action_slice_all so it fires exactly once per user action and does not disrupt GL thumbnail generation during Slice All - Fix is_imex missing from p->config init key list so on_config_change() diff detection correctly triggers refresh_imex_icons() - Defer imex_changed handling until after set_bed_shape() so m_shape is current - Replace plain remove button with ScalableButton (imex_remove.svg) in mode rows - Add EditGCodeDialog launch button per mode row for placeholder browsing Co-Authored-By: Claude Sonnet 4.6 --- resources/images/imex_remove.svg | 1 + src/slic3r/GUI/PartPlate.cpp | 78 +++++++++++++++++++++++++++++--- src/slic3r/GUI/PartPlate.hpp | 6 +++ src/slic3r/GUI/Plater.cpp | 61 ++++++++++++++++++++++++- src/slic3r/GUI/Tab.cpp | 25 ++++++++-- 5 files changed, 159 insertions(+), 12 deletions(-) create mode 100644 resources/images/imex_remove.svg diff --git a/resources/images/imex_remove.svg b/resources/images/imex_remove.svg new file mode 100644 index 0000000000..6c18a21d66 --- /dev/null +++ b/resources/images/imex_remove.svg @@ -0,0 +1 @@ + diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index a5c2865574..e599f69171 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -879,8 +879,11 @@ void PartPlate::refresh_imex_icon() if (!preset) return; bool dual_bbl = preset->is_bbl_vendor() && preset->get_printer_extruder_count() == 2; auto* is_imex_opt = preset->printers.get_edited_preset().config.option("is_imex"); - if (is_imex_opt && is_imex_opt->value) - calc_vertex_for_icons(dual_bbl ? 7 : 6, m_imex_mode_icon); + if (is_imex_opt && is_imex_opt->value) { + int imex_slot = dual_bbl ? 7 : 6; + calc_vertex_for_icons(imex_slot, m_imex_mode_icon); + calc_vertex_for_imex_warn_badge(imex_slot, m_imex_warn_icon); + } } // Ensure zone geometry (secondary boxes, collision strips, visual meshes) is up to date. @@ -920,6 +923,20 @@ bool PartPlate::has_imex_placement_violations() return false; } +bool PartPlate::has_imex_multimaterial_conflict() const +{ + // Condition 1: iMEX is active and the plate mode is non-primary + auto* pb = wxGetApp().preset_bundle; + if (!pb) return false; + auto* is_imex_opt = pb->printers.get_edited_preset().config.option("is_imex"); + if (!is_imex_opt || !is_imex_opt->value) return false; + if (get_imex_mode() == "primary") return false; + + // Condition 2: objects on this plate actually use more than one unique filament/extruder + std::vector used = get_extruders(true); + return used.size() > 1; +} + void PartPlate::render_imex_zones(bool force_default_color) { if (force_default_color) @@ -1237,6 +1254,37 @@ void PartPlate::calc_vertex_for_icons(int index, PickingModel &model) init_raycaster_from_model(model); } +// Positions the iMEX multi-material warning badge as a small overlay at the bottom-right +// corner of the iMEX mode icon. The icon slot index matches the one used in calc_vertex_for_icons. +void PartPlate::calc_vertex_for_imex_warn_badge(int imex_icon_index, GLModel &model) +{ + model.reset(); + + auto bed_ext = get_extents(m_shape); + Vec2d p = bed_ext[2]; + auto factor = bed_ext.size()(1) / 200.0; + float size = PARTPLATE_ICON_SIZE * factor; + float gap_left = PARTPLATE_ICON_GAP_LEFT * factor; + float gap_y = PARTPLATE_ICON_GAP_Y * factor; + float gap_top = PARTPLATE_ICON_GAP_TOP * factor; + + // Centre of the iMEX mode icon (top-left corner = p after offset) + p += Vec2d(gap_left, -1 * (imex_icon_index * (size + gap_y) + gap_top)); + + // Badge is half the icon size, anchored to the bottom-right corner of the icon slot + float badge = size * 0.55f; + Vec2d bp(p(0) + size - badge, p(1) - size); + + ExPolygon poly; + poly.contour.append({ scale_(bp(0)) , scale_(bp(1)) }); + poly.contour.append({ scale_(bp(0) + badge), scale_(bp(1)) }); + poly.contour.append({ scale_(bp(0) + badge), scale_(bp(1) + badge)}); + poly.contour.append({ scale_(bp(0)) , scale_(bp(1) + badge)}); + + if (!init_model_from_poly(model, poly, GROUND_Z + 0.01f)) + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "Unable to generate geometry for iMEX warn badge\n"; +} + /* void PartPlate::calc_vertex_for_icons_background(int icon_count, GLModel &buffer) { @@ -1748,6 +1796,12 @@ void PartPlate::render_icons(bool bottom, bool only_name, int hover_id) } else { render_icon_texture(m_imex_mode_icon.model, m_partplate_list->m_imex_mode_texture); } + // Warning badge: iMEX parallel mode active alongside multi-material objects + if (has_imex_multimaterial_conflict()) { + render_icon_texture(m_imex_warn_icon, m_partplate_list->m_imex_warn_texture); + if (hover_id == (int)PLATE_IMEX_MODE_ID) + show_tooltip(_u8L("Warning: this plate uses a parallel iMEX mode with multi-material objects. Proceed with caution — verify your G-code handles this combination correctly.")); + } } } @@ -2060,10 +2114,10 @@ void PartPlate::register_raycasters_for_picking(GLCanvas3D &canvas) if (dual_bbl) register_model_for_picking(canvas, m_plate_filament_map_icon, picking_id_component(PLATE_FILAMENT_MAP_ID)); - // Register IDEX/IQEX mode icon only when IDEX/IQEX is active on the current printer preset. + // Register IDEX/IQEX mode icon only when IDEX/IQEX is active and geometry is initialized. if (preset) { auto* is_imex_opt = preset->printers.get_edited_preset().config.option("is_imex"); - if (is_imex_opt && is_imex_opt->value) + if (is_imex_opt && is_imex_opt->value && m_imex_mode_icon.mesh_raycaster) register_model_for_picking(canvas, m_imex_mode_icon, picking_id_component(PLATE_IMEX_MODE_ID)); } } @@ -3753,8 +3807,11 @@ bool PartPlate::set_shape(const Pointfs& shape, const Pointfs& exclude_areas, co calc_vertex_for_icons(dual_bbl ? 6 : 5, m_move_front_icon); { auto* is_imex_opt = preset->printers.get_edited_preset().config.option("is_imex"); - if (is_imex_opt && is_imex_opt->value) - calc_vertex_for_icons(dual_bbl ? 7 : 6, m_imex_mode_icon); + if (is_imex_opt && is_imex_opt->value) { + int imex_slot = dual_bbl ? 7 : 6; + calc_vertex_for_icons(imex_slot, m_imex_mode_icon); + calc_vertex_for_imex_warn_badge(imex_slot, m_imex_warn_icon); + } } calc_vertex_for_number(0, false, m_plate_idx_icon); @@ -4625,6 +4682,14 @@ void PartPlateList::generate_icon_textures() } } + // iMEX multi-material conflict warning badge + { + file_name = path + "obj_warning.svg"; + if (!m_imex_warn_texture.load_from_svg_file(file_name, true, false, false, icon_size)) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":load file %1% failed (iMEX warn badge)") % file_name; + } + } + // IDEX/IQEX mode icon textures (fall back gracefully if SVG not present yet) { file_name = path + (m_is_dark ? "plate_imex_mode_dark.svg" : "plate_imex_mode.svg"); @@ -4684,6 +4749,7 @@ void PartPlateList::release_icon_textures() m_plate_name_edit_hovered_texture.reset(); m_imex_mode_texture.reset(); m_imex_mode_hovered_texture.reset(); + m_imex_warn_texture.reset(); for (int i = 0;i < MAX_PLATE_COUNT; i++) { m_idx_textures[i].reset(); } diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index 6ff42c3ef6..858e50fccd 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -156,6 +156,7 @@ private: PickingModel m_plate_name_edit_icon; PickingModel m_move_front_icon; PickingModel m_imex_mode_icon; + GLModel m_imex_warn_icon; // warning badge: iMEX parallel mode + multi-material conflict GLModel m_plate_idx_icon; GLTexture m_texture; @@ -195,6 +196,7 @@ private: void calc_vertex_for_number(int index, bool one_number, GLModel &buffer); void calc_vertex_for_plate_name_edit_icon(GLTexture *texture, int index, PickingModel &model); void calc_vertex_for_icons(int index, PickingModel &model); + void calc_vertex_for_imex_warn_badge(int imex_icon_index, GLModel &model); // void calc_vertex_for_icons_background(int icon_count, GLModel &buffer); void render_background(bool force_default_color = false); void render_logo(bool bottom, bool render_cali = true); @@ -454,6 +456,9 @@ public: } // Returns true if any instance on this plate overlaps an IDEX/IQEX secondary or collision zone. bool has_imex_placement_violations(); + // Returns true when iMEX parallel mode is active (non-primary) AND the plate + // has objects assigned to more than one filament — a combination that warrants caution. + bool has_imex_multimaterial_conflict() const; // Returns the primary-zone bounding box in mm when an IDEX/IQEX parallel mode is active. // Empty (nullopt) when IDEX/IQEX is off or the mode is "primary" (full-bed). std::optional imex_primary_zone() { ensure_imex_zones(); return m_imex_primary_zone_box; } @@ -637,6 +642,7 @@ class PartPlateList : public ObjectBase GLTexture m_plate_name_edit_hovered_texture; GLTexture m_imex_mode_texture; GLTexture m_imex_mode_hovered_texture; + GLTexture m_imex_warn_texture; // warning badge for iMEX + multi-material conflict GLTexture m_idx_textures[MAX_PLATE_COUNT]; // set render option bool render_bedtype_logo = true; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index af49d6341b..cd4b04712c 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -4821,7 +4821,8 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) "wipe_tower_rotation_angle", "wipe_tower_cone_angle", "wipe_tower_extra_spacing", "wipe_tower_extra_flow", "wipe_tower_max_purge_speed", "wipe_tower_wall_type", "wipe_tower_extra_rib_length","wipe_tower_rib_width","wipe_tower_fillet_wall", "wipe_tower_filament", - "best_object_pos", "master_extruder_id" + "best_object_pos", "master_extruder_id", + "is_imex" })) , sidebar(new Sidebar(q)) , notification_manager(std::make_unique(q)) @@ -9781,6 +9782,24 @@ void Plater::priv::on_action_slice_plate(SimpleEvent&) { if (q != nullptr) { BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received slice plate event\n" ; + + // iMEX parallel mode + multi-material caution check (current plate only) + { + PartPlate* plate = partplate_list.get_curr_plate(); + if (plate && plate->has_imex_multimaterial_conflict()) { + int plate_num = partplate_list.get_curr_plate_index() + 1; + wxString msg = wxString::Format( + _L("Plate %d has an IDEX/IQEX parallel mode active alongside multi-material objects.\n\n" + "Secondary tool heads operate at the firmware level and may not behave as expected with multi-material prints. " + "Proceed with caution and verify your G-code handles this combination correctly.\n\n" + "Continue slicing?"), + plate_num); + MessageDialog dlg(q, msg, _L("IDEX/IQEX Multi-Material Caution"), wxICON_WARNING | wxYES | wxNO); + if (dlg.ShowModal() != wxID_YES) + return; + } + } + //BBS update extruder params and speed table before slicing const Slic3r::DynamicPrintConfig& config = wxGetApp().preset_bundle->full_config(); auto& print = q->get_partplate_list().get_current_fff_print(); @@ -9800,6 +9819,32 @@ void Plater::priv::on_action_slice_all(SimpleEvent&) { if (q != nullptr) { BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received slice project event\n" ; + + // iMEX parallel mode + multi-material caution check (all plates) + { + std::vector conflict_plates; + int plate_count = partplate_list.get_plate_count(); + for (int i = 0; i < plate_count; ++i) { + PartPlate* plate = partplate_list.get_plate(i); + if (plate && plate->has_imex_multimaterial_conflict()) + conflict_plates.push_back(i + 1); // 1-based for display + } + if (!conflict_plates.empty()) { + wxString plate_list; + for (int n : conflict_plates) + plate_list += wxString::Format(" %d", n); + wxString msg = wxString::Format( + _L("Plate(s)%s have an IDEX/IQEX parallel mode active alongside multi-material objects.\n\n" + "Secondary tool heads operate at the firmware level and may not behave as expected with multi-material prints. " + "Proceed with caution and verify your G-code handles this combination correctly.\n\n" + "Continue slicing?"), + plate_list); + MessageDialog dlg(q, msg, _L("IDEX/IQEX Multi-Material Caution"), wxICON_WARNING | wxYES | wxNO); + if (dlg.ShowModal() != wxID_YES) + return; + } + } + //BBS update extruder params and speed table before slicing const Slic3r::DynamicPrintConfig& config = wxGetApp().preset_bundle->full_config(); auto& print = q->get_partplate_list().get_current_fff_print(); @@ -16180,6 +16225,7 @@ void Plater::on_config_change(const DynamicPrintConfig &config) { bool update_scheduled = false; bool bed_shape_changed = false; + bool imex_changed = false; //bool print_sequence_changed = false; t_config_option_keys diff_keys = p->config->diff(config); @@ -16270,8 +16316,9 @@ void Plater::on_config_change(const DynamicPrintConfig &config) } // IDEX/IQEX: when is_imex toggles the mode icon needs to be repositioned on every plate. // set_shape() short-circuits when the bed geometry is unchanged, so we refresh explicitly. + // Defer until after the loop so bed_shape_changed / set_bed_shape() run first if needed. else if (opt_key == "is_imex") { - p->partplate_list.refresh_imex_icons(); + imex_changed = true; update_scheduled = true; } } @@ -16279,6 +16326,16 @@ void Plater::on_config_change(const DynamicPrintConfig &config) if (bed_shape_changed) set_bed_shape(); + // After any bed-shape or is_imex change, ensure the iMEX mode icon geometry and + // raycaster are correct. set_shape() short-circuits when the bed is unchanged, + // so we call refresh_imex_icons() explicitly whenever is_imex is active. + // Always done AFTER set_bed_shape() so m_shape is current. + if (bed_shape_changed || imex_changed) { + auto* is_imex_opt = wxGetApp().preset_bundle->printers.get_edited_preset().config.option("is_imex"); + if (is_imex_opt && is_imex_opt->value) + p->partplate_list.refresh_imex_icons(); + } + config_change_notification(config, std::string("print_sequence")); if (update_scheduled) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 7b7bdc8974..90afb0928d 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4653,17 +4653,34 @@ private: sizer->Add(r.name, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6); } sizer->Add(grid_panel, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6); - sizer->Add(r.gcode, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6); + sizer->Add(r.gcode, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, 4); + + // Placeholder search + (for non-primary) delete — stacked vertically + auto* btn_col = new wxBoxSizer(wxVERTICAL); + wxTextCtrl* gcode_ctrl = r.gcode; + auto* ph_btn = new ScalableButton(r.panel, wxID_ANY, "edit", wxEmptyString, + wxDefaultSize, wxDefaultPosition, + wxBU_EXACTFIT | wxNO_BORDER, 16); + ph_btn->SetToolTip(_L("Edit G-code / browse placeholders")); + ph_btn->Bind(wxEVT_BUTTON, [this, gcode_ctrl](wxCommandEvent&) { + EditGCodeDialog dlg(this, "imex_mode_gcode", gcode_ctrl->GetValue().ToStdString()); + if (dlg.ShowModal() == wxID_OK) + gcode_ctrl->SetValue(dlg.get_edited_gcode()); + }); + btn_col->Add(ph_btn, 0, wxBOTTOM, 2); if (!is_primary) { - auto* rm = new wxButton(r.panel, wxID_ANY, wxT("\u00d7"), - wxDefaultPosition, wxSize(24, 24), wxBU_EXACTFIT); + auto* rm = new ScalableButton(r.panel, wxID_ANY, "imex_remove", wxEmptyString, + wxDefaultSize, wxDefaultPosition, + wxBU_EXACTFIT | wxNO_BORDER, 16); + rm->SetToolTip(_L("Remove mode")); rm->Bind(wxEVT_BUTTON, [this, this_panel](wxCommandEvent&) { remove_row(this_panel); notify(); }); - sizer->Add(rm, 0, wxALIGN_CENTER_VERTICAL); + btn_col->Add(rm, 0); } + sizer->Add(btn_col, 0, wxALIGN_CENTER_VERTICAL); r.panel->SetSizer(sizer); m_rows_sizer->Add(r.panel, 0, wxEXPAND | wxBOTTOM, 4); From 0703728e568d565476f43e4d9b00688a5dd5fdd1 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Thu, 9 Apr 2026 23:48:43 -0500 Subject: [PATCH 043/434] 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 5aa624b025975221a8fec353f51514c2bbf72606 Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Sat, 11 Apr 2026 02:00:02 -0400 Subject: [PATCH 044/434] fix: Mark secondary carriages as used in iMEX parallel modes In iMEX (IDEX/IQEX) parallel printing modes (copy/mirror), only the primary extruder generates toolpaths. The firmware duplicates the primary's movements for secondary carriages, so they don't appear in tool_ordering.all_extruders(). This caused is_extruder_used[N] to return false for secondary tools even though they're physically active and moving. The fix adds logic to parse the active mode's tool assignments from imex_mode_active_tools config and marks all assigned tools as used. Changes: - Added null checks and bounds validation for config options - Skip empty tool strings to avoid unnecessary parsing - Reordered bounds checks for defensive programming - Added clarifying comments for exception handling This ensures is_extruder_used[N] is true for all tools in a parallel mode, allowing printer profiles to correctly enable heaters and emit cleanup G-code for all active carriages. Fixes: is_extruder_used[1] returns false in copy/mirror modes (#13086) Related: Comment [28]/[30], Comment [18] (is_extruder_used in G-code header) --- src/libslic3r/GCode.cpp | 53 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 87e4f2d25f..cec0bf9550 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include @@ -2823,6 +2824,58 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato std::vector is_extruder_used(std::max(size_t(MAXIMUM_EXTRUDER_NUMBER), print.config().filament_diameter.size()), 0); for (unsigned int extruder : tool_ordering.all_extruders()) is_extruder_used[extruder] = true; + + // Orca iMEX: Also mark secondary carriages as used in parallel modes + // so is_extruder_used[N] is true for all physical tools in the active mode + if (print.config().is_imex.value && !print.objects().empty()) { + // Get the active mode for the first plate (all plates should have same mode in a single print) + // Assumes all objects share the same mode — validated upstream by the UI + const std::string& raw_mode = print.objects().front()->config().imex_parallel_mode.value; + std::string active_mode = raw_mode.empty() ? "primary" : raw_mode; + + // Look up the mode's active tools from printer config + const auto* mode_names_opt = print.config().option("imex_mode_names"); + const auto* tools_opt = print.config().option("imex_mode_active_tools"); + + if (mode_names_opt && tools_opt && !mode_names_opt->values.empty() && !tools_opt->values.empty()) { + // Linear scan to find the matching mode (fine for small fixed sets of modes) + for (size_t i = 0; i < mode_names_opt->values.size(); ++i) { + if (i < tools_opt->values.size() && mode_names_opt->values[i] == active_mode) { + const std::string& active_tools_str = tools_opt->values[i]; + // Skip empty tool strings (no active tools in this mode) + if (active_tools_str.empty()) continue; + + // Parse "idx:P,idx:C,idx:M" format to extract tool indices + std::istringstream ss(active_tools_str); + std::string token; + while (std::getline(ss, token, ',')) { + token.erase(std::remove_if(token.begin(), token.end(), ::isspace), token.end()); + if (token.empty()) continue; + try { + auto colon = token.find(':'); + int idx; + if (colon != std::string::npos) { + idx = std::stoi(token.substr(0, colon)); + } else { + idx = std::stoi(token); + } + // Mark this tool as used (idx is 0-based tool index) + // idx < 0 would require a leading '-' in the string, which would fail stoi first + if (idx >= 0 && idx < (int)is_extruder_used.size()) { + is_extruder_used[idx] = true; + } + } catch (const std::invalid_argument&) { + // not a valid integer token, skip + } catch (const std::out_of_range&) { + // index out of integer range, skip + } + } + break; + } + } + } + } + this->placeholder_parser().set("is_extruder_used", new ConfigOptionBools(is_extruder_used)); { From ad0b42fd373ca2b2a3abe9167b2064121dbb1a3b Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Mon, 13 Apr 2026 17:45:11 -0400 Subject: [PATCH 045/434] feat: iMEX temperature handling and pre-slice warnings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Temperature emission (GCode.cpp, PrintConfig.cpp, ConfigManipulation.cpp): - Set temperatures for all active tools in layer_change_gcode for iMEX parallel modes (primary + secondary carriages) - Fix filament temperature commands so Layer 1 temperatures only emit on the first layer; subsequent layers use normal layer-change temperatures - Remove stray temperature commands that fired outside the intended context - Consolidate iMEX temperature handling into the second-layer transition; clean up emission logic and naming throughout Pre-slice warning dialogs (PartPlate.cpp/hpp, Plater.cpp): - Collect per-plate IMEX warnings before slicing: multi-material conflict, bed temperature mismatch between carriages (>5 °C delta), and filament type incompatibility - Show a dismissible Yes/No dialog from both "Slice Plate" and "Slice All" actions; No returns to 3D view, Yes proceeds to slice - Refresh plate thumbnails after the panel switch so previously-generated thumbnails are not left black Co-Authored-By: Claude Sonnet 4.6 --- src/libslic3r/GCode.cpp | 136 ++++++++++-------- src/libslic3r/PrintConfig.cpp | 12 +- src/slic3r/GUI/ConfigManipulation.cpp | 3 + src/slic3r/GUI/PartPlate.cpp | 18 +-- src/slic3r/GUI/PartPlate.hpp | 6 +- src/slic3r/GUI/Plater.cpp | 190 ++++++++++++++++++++------ 6 files changed, 247 insertions(+), 118 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index cec0bf9550..9c191ce4af 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2391,6 +2391,46 @@ static BambuBedType to_bambu_bed_type(BedType type) return bambu_bed_type; } +// Orca IMEX: Returns the tool indices active in the current IMEX mode. +// In copy/mirror parallel modes, secondary carriages (T1-T3) never receive tool-change +// commands — the firmware mirrors the primary's moves — so they don't appear in +// tool_ordering.all_extruders(). This helper parses imex_mode_active_tools to enumerate them. +// Format: "0:P,1:C,2:M,3:M" — only the leading integer index is used. +static std::vector get_imex_active_tools(const Print& print) +{ + std::vector active_tools; + + if (!print.config().is_imex.value || print.objects().empty()) + return active_tools; + + const std::string& raw_mode = print.objects().front()->config().imex_parallel_mode.value; + const std::string active_mode = raw_mode.empty() ? "primary" : raw_mode; + + const auto* mode_names_opt = print.config().option("imex_mode_names"); + const auto* tools_opt = print.config().option("imex_mode_active_tools"); + + if (!mode_names_opt || !tools_opt || mode_names_opt->values.empty() || tools_opt->values.empty()) + return active_tools; + + for (size_t i = 0; i < mode_names_opt->values.size(); ++i) { + if (i >= tools_opt->values.size() || mode_names_opt->values[i] != active_mode) + continue; + std::istringstream ss(tools_opt->values[i]); + std::string token; + while (std::getline(ss, token, ',')) { + token.erase(std::remove_if(token.begin(), token.end(), ::isspace), token.end()); + if (token.empty()) continue; + try { + int idx = std::stoi(token); // leading integer before optional ':role' suffix + if (idx >= 0 && idx < (int)MAXIMUM_EXTRUDER_NUMBER) + active_tools.push_back(idx); + } catch (...) {} + } + break; + } + return active_tools; +} + void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGeneratorCallback thumbnail_cb) { PROFILE_FUNC(); @@ -2825,56 +2865,13 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato for (unsigned int extruder : tool_ordering.all_extruders()) is_extruder_used[extruder] = true; - // Orca iMEX: Also mark secondary carriages as used in parallel modes - // so is_extruder_used[N] is true for all physical tools in the active mode - if (print.config().is_imex.value && !print.objects().empty()) { - // Get the active mode for the first plate (all plates should have same mode in a single print) - // Assumes all objects share the same mode — validated upstream by the UI - const std::string& raw_mode = print.objects().front()->config().imex_parallel_mode.value; - std::string active_mode = raw_mode.empty() ? "primary" : raw_mode; - - // Look up the mode's active tools from printer config - const auto* mode_names_opt = print.config().option("imex_mode_names"); - const auto* tools_opt = print.config().option("imex_mode_active_tools"); - - if (mode_names_opt && tools_opt && !mode_names_opt->values.empty() && !tools_opt->values.empty()) { - // Linear scan to find the matching mode (fine for small fixed sets of modes) - for (size_t i = 0; i < mode_names_opt->values.size(); ++i) { - if (i < tools_opt->values.size() && mode_names_opt->values[i] == active_mode) { - const std::string& active_tools_str = tools_opt->values[i]; - // Skip empty tool strings (no active tools in this mode) - if (active_tools_str.empty()) continue; - - // Parse "idx:P,idx:C,idx:M" format to extract tool indices - std::istringstream ss(active_tools_str); - std::string token; - while (std::getline(ss, token, ',')) { - token.erase(std::remove_if(token.begin(), token.end(), ::isspace), token.end()); - if (token.empty()) continue; - try { - auto colon = token.find(':'); - int idx; - if (colon != std::string::npos) { - idx = std::stoi(token.substr(0, colon)); - } else { - idx = std::stoi(token); - } - // Mark this tool as used (idx is 0-based tool index) - // idx < 0 would require a leading '-' in the string, which would fail stoi first - if (idx >= 0 && idx < (int)is_extruder_used.size()) { - is_extruder_used[idx] = true; - } - } catch (const std::invalid_argument&) { - // not a valid integer token, skip - } catch (const std::out_of_range&) { - // index out of integer range, skip - } - } - break; - } - } - } - } + // Orca IMEX: Also mark secondary carriages as used in parallel modes + // so is_extruder_used[N] is true for all physical tools in the active mode. + // Secondary carriages never appear in tool_ordering.all_extruders() because + // the firmware duplicates the primary's moves — no tool-change commands are emitted. + for (int tool_idx : get_imex_active_tools(print)) + if (tool_idx < (int)is_extruder_used.size()) + is_extruder_used[tool_idx] = true; this->placeholder_parser().set("is_extruder_used", new ConfigOptionBools(is_extruder_used)); @@ -4409,6 +4406,7 @@ std::string GCode::generate_skirt(const Print &print, // In non-sequential mode, process_layer is called per each print_z height with all object and support layers accumulated. // For multi-material prints, this routine minimizes extruder switches by gathering extruder specific extrusion paths // and performing the extruder specific extrusions together. + LayerResult GCode::process_layer( const Print &print, // Set of object & print layers of the same PrintObject and with the same print_z. @@ -4559,6 +4557,7 @@ LayerResult GCode::process_layer( + "\n"; config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); } + //BBS: set layer time fan speed after layer change gcode gcode += ";_SET_FAN_SPEED_CHANGING_LAYER\n"; @@ -4675,16 +4674,31 @@ LayerResult GCode::process_layer( 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()) { - if ((print.config().single_extruder_multi_material.value || m_ooze_prevention.enable) && - 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()); + // Transition from 1st to 2nd layer: set non-initial-layer nozzle temperatures. + // IMEX parallel modes: secondary carriages (T1-T3) never receive tool-change commands, + // so they're not in m_writer.extruders() and multiple_extruders==false (max id==0). + // Use the static set_temperature path for IMEX so the T index is always emitted. + if (print.config().is_imex.value) { + // All active tools need explicit temps — none receive tool-change commands, + // so we can't rely on the condition used for non-IMEX (temp != initial_layer_temp). + // A tool whose initial and regular temps are the same still needs to be set here. + const int num_nozzles = (int)print.config().nozzle_temperature.values.size(); + for (int tool_idx : get_imex_active_tools(print)) { + if (tool_idx >= num_nozzles) continue; + int temperature = print.config().nozzle_temperature.values[tool_idx]; + if (temperature > 0) + gcode += GCodeWriter::set_temperature(temperature, m_writer.config.gcode_flavor, false, tool_idx, "set IMEX tool temperature"); + } + } else { + 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()) + // 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()); + } } // BBS @@ -7534,6 +7548,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo gcode += this->placeholder_parser_process("filament_start_gcode", filament_start_gcode, new_filament_id, &config); 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)); // Orca: Adaptive PA @@ -7828,6 +7843,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo } check_add_eol(gcode); } + // Set the new extruder to the operating temperature. if (m_ooze_prevention.enable) gcode += m_ooze_prevention.post_toolchange(*this); diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 310d799e19..4bd3361afc 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -10511,16 +10511,16 @@ OtherSlicingStatesConfigDef::OtherSlicingStatesConfigDef() new_def("in_head_wrap_detect_zone", coBool, "In head wrap detect zone", "Indicates if the first layer overlaps with the head wrap zone."); def = this->add("imex_mode", coString); - def->label = L("iMEX active mode"); - def->tooltip = L("Name of the active iMEX parallel print mode for this plate (e.g. 'primary', 'mirror', 'copy'). Empty string if iMEX is not enabled."); + def->label = L("IMEX active mode"); + def->tooltip = L("Name of the active IMEX parallel print mode for this plate (e.g. 'primary', 'mirror', 'copy'). Empty string if IMEX is not enabled."); def = this->add("imex_mode_index", coInt); - def->label = L("iMEX active mode index"); - def->tooltip = L("Zero-based index of the active iMEX parallel print mode within imex_mode_names."); + def->label = L("IMEX active mode index"); + def->tooltip = L("Zero-based index of the active IMEX parallel print mode within imex_mode_names."); def = this->add("imex_mode_gcode", coString); - def->label = L("iMEX active mode G-code"); - def->tooltip = L("The raw mode G-code template for the active iMEX parallel print mode, after placeholder evaluation. Globals defined here flow into machine_start_gcode."); + def->label = L("IMEX active mode G-code"); + def->tooltip = L("The raw mode G-code template for the active IMEX parallel print mode, after placeholder evaluation. Globals defined here flow into machine_start_gcode."); } PrintStatisticsConfigDef::PrintStatisticsConfigDef() diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index e41886008b..f531aebafb 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -793,6 +793,9 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co bool has_ironing = (config->opt_enum("ironing_type") != IroningType::NoIroning); for (auto el : { "ironing_pattern", "ironing_flow", "ironing_spacing", "ironing_angle", "ironing_inset", "ironing_angle_fixed" }) toggle_line(el, has_ironing); + bool has_rectilinear_ironing = (config->opt_enum("ironing_pattern") == InfillPattern::ipRectilinear); + for (auto el : {"ironing_angle", "ironing_angle_fixed"}) + toggle_field(el, has_ironing && has_rectilinear_ironing); toggle_line("ironing_speed", has_ironing || has_support_ironing); diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index e599f69171..594af28f19 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -925,7 +925,7 @@ bool PartPlate::has_imex_placement_violations() bool PartPlate::has_imex_multimaterial_conflict() const { - // Condition 1: iMEX is active and the plate mode is non-primary + // Condition 1: IMEX is active and the plate mode is non-primary auto* pb = wxGetApp().preset_bundle; if (!pb) return false; auto* is_imex_opt = pb->printers.get_edited_preset().config.option("is_imex"); @@ -1254,8 +1254,8 @@ void PartPlate::calc_vertex_for_icons(int index, PickingModel &model) init_raycaster_from_model(model); } -// Positions the iMEX multi-material warning badge as a small overlay at the bottom-right -// corner of the iMEX mode icon. The icon slot index matches the one used in calc_vertex_for_icons. +// Positions the IMEX multi-material warning badge as a small overlay at the bottom-right +// corner of the IMEX mode icon. The icon slot index matches the one used in calc_vertex_for_icons. void PartPlate::calc_vertex_for_imex_warn_badge(int imex_icon_index, GLModel &model) { model.reset(); @@ -1268,7 +1268,7 @@ void PartPlate::calc_vertex_for_imex_warn_badge(int imex_icon_index, GLModel &mo float gap_y = PARTPLATE_ICON_GAP_Y * factor; float gap_top = PARTPLATE_ICON_GAP_TOP * factor; - // Centre of the iMEX mode icon (top-left corner = p after offset) + // Centre of the IMEX mode icon (top-left corner = p after offset) p += Vec2d(gap_left, -1 * (imex_icon_index * (size + gap_y) + gap_top)); // Badge is half the icon size, anchored to the bottom-right corner of the icon slot @@ -1282,7 +1282,7 @@ void PartPlate::calc_vertex_for_imex_warn_badge(int imex_icon_index, GLModel &mo poly.contour.append({ scale_(bp(0)) , scale_(bp(1) + badge)}); if (!init_model_from_poly(model, poly, GROUND_Z + 0.01f)) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "Unable to generate geometry for iMEX warn badge\n"; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "Unable to generate geometry for IMEX warn badge\n"; } /* @@ -1796,11 +1796,11 @@ void PartPlate::render_icons(bool bottom, bool only_name, int hover_id) } else { render_icon_texture(m_imex_mode_icon.model, m_partplate_list->m_imex_mode_texture); } - // Warning badge: iMEX parallel mode active alongside multi-material objects + // Warning badge: IMEX parallel mode active alongside multi-material objects if (has_imex_multimaterial_conflict()) { render_icon_texture(m_imex_warn_icon, m_partplate_list->m_imex_warn_texture); if (hover_id == (int)PLATE_IMEX_MODE_ID) - show_tooltip(_u8L("Warning: this plate uses a parallel iMEX mode with multi-material objects. Proceed with caution — verify your G-code handles this combination correctly.")); + show_tooltip(_u8L("Warning: this plate uses a parallel IMEX mode with multi-material objects. Proceed with caution — verify your G-code handles this combination correctly.")); } } } @@ -4682,11 +4682,11 @@ void PartPlateList::generate_icon_textures() } } - // iMEX multi-material conflict warning badge + // IMEX multi-material conflict warning badge { file_name = path + "obj_warning.svg"; if (!m_imex_warn_texture.load_from_svg_file(file_name, true, false, false, icon_size)) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":load file %1% failed (iMEX warn badge)") % file_name; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":load file %1% failed (IMEX warn badge)") % file_name; } } diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index 858e50fccd..e77e13fb60 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -156,7 +156,7 @@ private: PickingModel m_plate_name_edit_icon; PickingModel m_move_front_icon; PickingModel m_imex_mode_icon; - GLModel m_imex_warn_icon; // warning badge: iMEX parallel mode + multi-material conflict + GLModel m_imex_warn_icon; // warning badge: IMEX parallel mode + multi-material conflict GLModel m_plate_idx_icon; GLTexture m_texture; @@ -456,7 +456,7 @@ public: } // Returns true if any instance on this plate overlaps an IDEX/IQEX secondary or collision zone. bool has_imex_placement_violations(); - // Returns true when iMEX parallel mode is active (non-primary) AND the plate + // Returns true when IMEX parallel mode is active (non-primary) AND the plate // has objects assigned to more than one filament — a combination that warrants caution. bool has_imex_multimaterial_conflict() const; // Returns the primary-zone bounding box in mm when an IDEX/IQEX parallel mode is active. @@ -642,7 +642,7 @@ class PartPlateList : public ObjectBase GLTexture m_plate_name_edit_hovered_texture; GLTexture m_imex_mode_texture; GLTexture m_imex_mode_hovered_texture; - GLTexture m_imex_warn_texture; // warning badge for iMEX + multi-material conflict + GLTexture m_imex_warn_texture; // warning badge for IMEX + multi-material conflict GLTexture m_idx_textures[MAX_PLATE_COUNT]; // set render option bool render_bedtype_logo = true; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index cd4b04712c..b969d2c8c7 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -9778,25 +9779,131 @@ void Plater::priv::on_action_open_project(SimpleEvent&) } //BBS: GUI refactor: slice plate +// Orca IMEX: Collects all IMEX-related warnings for a plate before slicing. +// Returns one formatted bullet string per issue. Empty means no concerns. +// Checks: +// 1. Multi-material objects alongside a parallel mode (existing badge condition) +// 2. Bed temperature conflict: primary tool controls the bed; secondary tools warned +// if their optimal bed temp differs from the primary's by more than 5°C +// 3. Filament type mismatch: materials with incompatible requirements +static std::vector collect_imex_warnings(PartPlate* plate) +{ + std::vector warnings; + if (!plate) return warnings; + + // Check 1: multi-material conflict (same condition that drives the badge) + if (plate->has_imex_multimaterial_conflict()) + warnings.push_back(_L("Multi-material objects detected — secondary carriages may not behave as expected with multi-material prints.")); + + // Checks 2 & 3 only apply when a non-primary parallel mode is active + const std::string mode = plate->get_imex_mode(); + if (mode == "primary") return warnings; + + PresetBundle* bundle = wxGetApp().preset_bundle; + if (!bundle) return warnings; + + // Resolve active tool indices for this mode from printer config + const auto& printer_cfg = bundle->printers.get_edited_preset().config; + const auto* mode_names_opt = printer_cfg.option("imex_mode_names"); + const auto* tools_opt = printer_cfg.option("imex_mode_active_tools"); + + // Upper bound: filament_presets.size() is the practical limit, capped at MAXIMUM_EXTRUDER_NUMBER (64) + const size_t max_tool = std::min(bundle->filament_presets.size(), MAXIMUM_EXTRUDER_NUMBER); + + std::vector active_tools; + if (mode_names_opt && tools_opt) { + for (size_t i = 0; i < mode_names_opt->values.size(); ++i) { + if (i >= tools_opt->values.size() || mode_names_opt->values[i] != mode) continue; + std::istringstream ss(tools_opt->values[i]); + std::string token; + while (std::getline(ss, token, ',')) { + token.erase(std::remove_if(token.begin(), token.end(), ::isspace), token.end()); + if (token.empty()) continue; + try { + int idx = std::stoi(token); + if (idx >= 0 && (max_tool == 0 || (size_t)idx < max_tool)) + active_tools.push_back(idx); + } catch (...) {} + } + break; + } + } + + if (active_tools.size() < 2) return warnings; + + const int primary_tool = active_tools[0]; + const DynamicPrintConfig& full_cfg = bundle->full_config(); + // Bed temps are per-plate-type; resolve the active plate's bed type to get the right key. + const BedType bed_type = bundle->project_config.opt_enum("curr_bed_type"); + const std::string bed_temp_key = get_bed_temp_key(bed_type); + const auto* bed_temps = bed_temp_key.empty() ? nullptr : full_cfg.option(bed_temp_key); + const auto& filament_presets = bundle->filament_presets; + + // Primary tool's bed temp and filament type + const int primary_bed_temp = (bed_temps && primary_tool < (int)bed_temps->values.size()) + ? bed_temps->values[primary_tool] : 0; + std::string primary_display_type; + if (primary_tool < (int)filament_presets.size()) { + Preset* p = bundle->filaments.find_preset(filament_presets[primary_tool]); + if (p) p->get_filament_type(primary_display_type); + } + if (primary_display_type.empty()) primary_display_type = "unknown"; + + for (size_t i = 1; i < active_tools.size(); ++i) { + const int tool_idx = active_tools[i]; + + std::string secondary_display_type; + if (tool_idx < (int)filament_presets.size()) { + Preset* p = bundle->filaments.find_preset(filament_presets[tool_idx]); + if (p) p->get_filament_type(secondary_display_type); + } + if (secondary_display_type.empty()) secondary_display_type = "unknown"; + + // Check 2: bed temperature — primary wins, secondary may not get what it needs + if (bed_temps && primary_bed_temp > 0 && tool_idx < (int)bed_temps->values.size()) { + const int secondary_bed_temp = bed_temps->values[tool_idx]; + if (secondary_bed_temp > 0 && std::abs(secondary_bed_temp - primary_bed_temp) > 5) { + warnings.push_back(wxString::Format( + _L("Bed temperature conflict: T%d (%s) sets the bed to %d\u00B0C \u2014 " + "T%d (%s) requires %d\u00B0C. The primary tool controls the bed; " + "secondary tools print at whatever temperature it sets."), + primary_tool, primary_display_type, primary_bed_temp, + tool_idx, secondary_display_type, secondary_bed_temp)); + } + } + + // Check 3: filament type mismatch + if (primary_display_type != secondary_display_type && + primary_display_type != "unknown" && secondary_display_type != "unknown") { + warnings.push_back(wxString::Format( + _L("Filament type mismatch: T%d uses %s and T%d uses %s. " + "These materials have incompatible requirements and are not designed to print together."), + primary_tool, primary_display_type, + tool_idx, secondary_display_type)); + } + } + + return warnings; +} + void Plater::priv::on_action_slice_plate(SimpleEvent&) { if (q != nullptr) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received slice plate event\n" ; - - // iMEX parallel mode + multi-material caution check (current plate only) + // IMEX parallel mode warnings (current plate only) { PartPlate* plate = partplate_list.get_curr_plate(); - if (plate && plate->has_imex_multimaterial_conflict()) { + std::vector warnings = collect_imex_warnings(plate); + if (!warnings.empty()) { int plate_num = partplate_list.get_curr_plate_index() + 1; - wxString msg = wxString::Format( - _L("Plate %d has an IDEX/IQEX parallel mode active alongside multi-material objects.\n\n" - "Secondary tool heads operate at the firmware level and may not behave as expected with multi-material prints. " - "Proceed with caution and verify your G-code handles this combination correctly.\n\n" - "Continue slicing?"), - plate_num); - MessageDialog dlg(q, msg, _L("IDEX/IQEX Multi-Material Caution"), wxICON_WARNING | wxYES | wxNO); - if (dlg.ShowModal() != wxID_YES) + wxString msg = wxString::Format(_L("Plate %d has IDEX/IQEX parallel mode active with the following concerns:\n\n"), plate_num); + for (const wxString& w : warnings) + msg += L"\u2022 " + w + "\n\n"; + msg += _L("Continue slicing?"); + MessageDialog dlg(q, msg, _L("IDEX/IQEX Parallel Mode Warning"), wxICON_WARNING | wxYES | wxNO); + if (dlg.ShowModal() != wxID_YES) { + q->select_view_3D("3D"); return; + } } } @@ -9811,6 +9918,8 @@ void Plater::priv::on_action_slice_plate(SimpleEvent&) m_slice_all = false; q->reslice(); q->select_view_3D("Preview"); + // Regenerate any thumbnails that were wiped by the panel switch above. + q->update_all_plate_thumbnails(); } } @@ -9818,30 +9927,30 @@ void Plater::priv::on_action_slice_plate(SimpleEvent&) void Plater::priv::on_action_slice_all(SimpleEvent&) { if (q != nullptr) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received slice project event\n" ; - - // iMEX parallel mode + multi-material caution check (all plates) + // IMEX parallel mode warnings (all plates) { - std::vector conflict_plates; + wxString combined_msg; + std::vector warned_plates; int plate_count = partplate_list.get_plate_count(); for (int i = 0; i < plate_count; ++i) { PartPlate* plate = partplate_list.get_plate(i); - if (plate && plate->has_imex_multimaterial_conflict()) - conflict_plates.push_back(i + 1); // 1-based for display + std::vector warnings = collect_imex_warnings(plate); + if (warnings.empty()) continue; + warned_plates.push_back(plate); + combined_msg += wxString::Format(_L("Plate %d:\n"), i + 1); + for (const wxString& w : warnings) + combined_msg += L" \u2022 " + w + "\n"; + combined_msg += "\n"; } - if (!conflict_plates.empty()) { - wxString plate_list; - for (int n : conflict_plates) - plate_list += wxString::Format(" %d", n); - wxString msg = wxString::Format( - _L("Plate(s)%s have an IDEX/IQEX parallel mode active alongside multi-material objects.\n\n" - "Secondary tool heads operate at the firmware level and may not behave as expected with multi-material prints. " - "Proceed with caution and verify your G-code handles this combination correctly.\n\n" - "Continue slicing?"), - plate_list); - MessageDialog dlg(q, msg, _L("IDEX/IQEX Multi-Material Caution"), wxICON_WARNING | wxYES | wxNO); - if (dlg.ShowModal() != wxID_YES) + if (!combined_msg.empty()) { + wxString msg = _L("The following IDEX/IQEX parallel mode concerns were detected:\n\n") + + combined_msg + + _L("Continue slicing?"); + MessageDialog dlg(q, msg, _L("IDEX/IQEX Parallel Mode Warning"), wxICON_WARNING | wxYES | wxNO); + if (dlg.ShowModal() != wxID_YES) { + q->select_view_3D("3D"); return; + } } } @@ -9861,6 +9970,8 @@ void Plater::priv::on_action_slice_all(SimpleEvent&) q->reslice(); if (!m_is_publishing) q->select_view_3D("Preview"); + // Regenerate any thumbnails that were wiped by the panel switch above. + q->update_all_plate_thumbnails(); //BBS: wish to select all plates stats item preview->get_canvas3d()->_update_select_plate_toolbar_stats_item(true); } @@ -13394,7 +13505,6 @@ void Plater::invalid_all_plate_thumbnails() { if (using_exported_file() || skip_thumbnail_invalid) return; - BOOST_LOG_TRIVIAL(info) << "thumb: invalid all"; for (int i = 0; i < get_partplate_list().get_plate_count(); i++) { PartPlate* plate = get_partplate_list().get_plate(i); plate->thumbnail_data.reset(); @@ -15256,10 +15366,10 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy ThumbnailData* thumbnail_data = &p->partplate_list.get_plate(i)->thumbnail_data; if (p->partplate_list.get_plate(i)->thumbnail_data.is_valid() && using_exported_file()) { //no need to generate thumbnail - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": non need to re-generate thumbnail for gcode/exported mode of plate %1%")%i; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": non need to re-generate thumbnail for gcode/exported mode of plate %1%")%i; } else { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": re-generate thumbnail for plate %1%") % i; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": re-generate thumbnail for plate %1%") % i; const ThumbnailsParams thumbnail_params = { {}, false, true, true, true, i }; p->generate_thumbnail(p->partplate_list.get_plate(i)->thumbnail_data, THUMBNAIL_SIZE_3MF.first, THUMBNAIL_SIZE_3MF.second, thumbnail_params, Camera::EType::Ortho); @@ -15269,9 +15379,9 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy ThumbnailData *no_light_thumbnail_data = &p->partplate_list.get_plate(i)->no_light_thumbnail_data; if (p->partplate_list.get_plate(i)->no_light_thumbnail_data.is_valid() && using_exported_file()) { // no need to generate thumbnail - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": non need to re-generate thumbnail for gcode/exported mode of plate %1%") % i; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": non need to re-generate thumbnail for gcode/exported mode of plate %1%") % i; } else { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": re-generate thumbnail for plate %1%") % i; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": re-generate thumbnail for plate %1%") % i; const ThumbnailsParams thumbnail_params = {{}, false, true, true, true, i}; p->generate_thumbnail(p->partplate_list.get_plate(i)->no_light_thumbnail_data, THUMBNAIL_SIZE_3MF.first, THUMBNAIL_SIZE_3MF.second, thumbnail_params, Camera::EType::Ortho, Camera::ViewAngleType::Iso, false, true); @@ -15286,10 +15396,10 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy ThumbnailData* top_thumbnail = &p->partplate_list.get_plate(i)->top_thumbnail_data; if (top_thumbnail->is_valid() && using_exported_file()) { //no need to generate thumbnail - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": non need to re-generate top_thumbnail for gcode/exported mode of plate %1%")%i; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": non need to re-generate top_thumbnail for gcode/exported mode of plate %1%")%i; } else { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": re-generate top_thumbnail for plate %1%") % i; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": re-generate top_thumbnail for plate %1%") % i; const ThumbnailsParams thumbnail_params = { {}, false, true, false, true, i }; p->generate_thumbnail(p->partplate_list.get_plate(i)->top_thumbnail_data, THUMBNAIL_SIZE_3MF.first, THUMBNAIL_SIZE_3MF.second, thumbnail_params, Camera::EType::Ortho, Camera::ViewAngleType::Top_Plate, false); @@ -15299,10 +15409,10 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy ThumbnailData* picking_thumbnail = &p->partplate_list.get_plate(i)->pick_thumbnail_data; if (picking_thumbnail->is_valid() && using_exported_file()) { //no need to generate thumbnail - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": non need to re-generate pick_thumbnail for gcode/exported mode of plate %1%")%i; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": non need to re-generate pick_thumbnail for gcode/exported mode of plate %1%")%i; } else { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": re-generate pick_thumbnail for plate %1%") % i; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": re-generate pick_thumbnail for plate %1%") % i; const ThumbnailsParams thumbnail_params = { {}, false, true, false, true, i }; p->generate_thumbnail(p->partplate_list.get_plate(i)->pick_thumbnail_data, THUMBNAIL_SIZE_3MF.first, THUMBNAIL_SIZE_3MF.second, thumbnail_params, Camera::EType::Ortho, Camera::ViewAngleType::Top_Plate, true,true); @@ -16326,7 +16436,7 @@ void Plater::on_config_change(const DynamicPrintConfig &config) if (bed_shape_changed) set_bed_shape(); - // After any bed-shape or is_imex change, ensure the iMEX mode icon geometry and + // After any bed-shape or is_imex change, ensure the IMEX mode icon geometry and // raycaster are correct. set_shape() short-circuits when the bed is unchanged, // so we call refresh_imex_icons() explicitly whenever is_imex is active. // Always done AFTER set_bed_shape() so m_shape is current. 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 046/434] 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 857bfbfb82a0ded4a6f9413afd53e9195aad5298 Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Tue, 14 Apr 2026 09:17:23 -0400 Subject: [PATCH 047/434] fix: guard against empty option_set in OG_CustomCtrl widget-only lines update_visibility() and render() both called front() on an empty vector when a CtrlLine had no options (pure widget lines). Added an early-return path in update_visibility() and a null-guard in render(). Co-Authored-By: Claude Sonnet 4.6 --- src/slic3r/GUI/OG_CustomCtrl.cpp | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/OG_CustomCtrl.cpp b/src/slic3r/GUI/OG_CustomCtrl.cpp index da4564f537..3eb3ac2edc 100644 --- a/src/slic3r/GUI/OG_CustomCtrl.cpp +++ b/src/slic3r/GUI/OG_CustomCtrl.cpp @@ -723,6 +723,19 @@ void OG_CustomCtrl::CtrlLine::update_visibility(ConfigOptionMode mode) return; const std::vector