From 7ff6bc42b14ce245b14c53c0c18bc79e892dea99 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 18 Mar 2026 12:09:28 -0500 Subject: [PATCH] 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); }