From a7441c7f482392acac81b8d4c8534bf174a7f0c5 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 11 Mar 2026 00:27:14 -0500 Subject: [PATCH] stage in changes from off-plate-gravity and remove stuff I didn't need --- resources/shaders/110/gouraud.fs | 1 + resources/shaders/110/gouraud.vs | 5 +- resources/shaders/110/mm_gouraud.fs | 3 +- resources/shaders/110/mm_gouraud.vs | 1 + resources/shaders/140/gouraud.fs | 1 + resources/shaders/140/gouraud.vs | 5 +- resources/shaders/140/mm_gouraud.fs | 3 +- resources/shaders/140/mm_gouraud.vs | 1 + src/libslic3r/ExPolygon.hpp | 5 ++ src/libslic3r/Preset.cpp | 2 +- src/libslic3r/PrintConfig.cpp | 24 ++++++++ src/libslic3r/PrintConfig.hpp | 3 + src/libslic3r/Support/SupportMaterial.cpp | 22 ++++++-- src/libslic3r/Support/TreeSupport.cpp | 18 +++++- src/libslic3r/Support/TreeSupport3D.cpp | 16 +++++- src/libslic3r/TriangleSelector.cpp | 12 ++-- src/libslic3r/TriangleSelector.hpp | 6 +- src/slic3r/GUI/3DBed.cpp | 59 ++++++++++++++++++++ src/slic3r/GUI/3DBed.hpp | 4 +- src/slic3r/GUI/3DScene.cpp | 18 +++++- src/slic3r/GUI/GUI_Factories.cpp | 4 +- src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp | 20 +++++-- src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp | 1 + src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp | 28 ++++++++-- src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp | 3 + src/slic3r/GUI/Tab.cpp | 2 + 26 files changed, 230 insertions(+), 37 deletions(-) diff --git a/resources/shaders/110/gouraud.fs b/resources/shaders/110/gouraud.fs index e602d6067d..12f4feca3e 100644 --- a/resources/shaders/110/gouraud.fs +++ b/resources/shaders/110/gouraud.fs @@ -26,6 +26,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform vec4 uniform_color; diff --git a/resources/shaders/110/gouraud.vs b/resources/shaders/110/gouraud.vs index ff3a190c79..e2de2b63e5 100644 --- a/resources/shaders/110/gouraud.vs +++ b/resources/shaders/110/gouraud.vs @@ -23,6 +23,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform mat4 view_model_matrix; @@ -71,8 +72,8 @@ void main() // Point in homogenous coordinates. world_pos = volume_world_matrix * vec4(v_position, 1.0); - // z component of normal vector in world coordinate used for slope shading - world_normal_z = slope.actived ? (normalize(slope.volume_world_normal_matrix * v_normal)).z : 0.0; + // dot product of world normal with up direction, used for slope shading + world_normal_z = slope.actived ? dot(normalize(slope.volume_world_normal_matrix * v_normal), slope.up_direction) : 0.0; gl_Position = projection_matrix * position; // Fill in the scalars for fragment shader clipping. Fragments with any of these components lower than zero are discarded. diff --git a/resources/shaders/110/mm_gouraud.fs b/resources/shaders/110/mm_gouraud.fs index 821af13f03..431c936e9c 100644 --- a/resources/shaders/110/mm_gouraud.fs +++ b/resources/shaders/110/mm_gouraud.fs @@ -37,6 +37,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform SlopeDetection slope; @@ -85,7 +86,7 @@ void main() color = LightBlue; alpha = 1.0; } - else if( transformed_normal.z < slope.normal_z - EPSILON) + else if( dot(transformed_normal, slope.up_direction) < slope.normal_z - EPSILON) { color = color * 0.5 + LightRed * 0.5; alpha = 1.0; diff --git a/resources/shaders/110/mm_gouraud.vs b/resources/shaders/110/mm_gouraud.vs index b0cea9cfd6..c54ae4bff5 100644 --- a/resources/shaders/110/mm_gouraud.vs +++ b/resources/shaders/110/mm_gouraud.vs @@ -24,6 +24,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform SlopeDetection slope; void main() diff --git a/resources/shaders/140/gouraud.fs b/resources/shaders/140/gouraud.fs index bbfb76f7a1..0d9fa021fe 100644 --- a/resources/shaders/140/gouraud.fs +++ b/resources/shaders/140/gouraud.fs @@ -26,6 +26,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform vec4 uniform_color; diff --git a/resources/shaders/140/gouraud.vs b/resources/shaders/140/gouraud.vs index 72dd4ff1bf..c69dcd11df 100644 --- a/resources/shaders/140/gouraud.vs +++ b/resources/shaders/140/gouraud.vs @@ -23,6 +23,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform mat4 view_model_matrix; @@ -71,8 +72,8 @@ void main() // Point in homogenous coordinates. world_pos = volume_world_matrix * vec4(v_position, 1.0); - // z component of normal vector in world coordinate used for slope shading - world_normal_z = slope.actived ? (normalize(slope.volume_world_normal_matrix * v_normal)).z : 0.0; + // dot product of world normal with up direction, used for slope shading + world_normal_z = slope.actived ? dot(normalize(slope.volume_world_normal_matrix * v_normal), slope.up_direction) : 0.0; gl_Position = projection_matrix * position; // Fill in the scalars for fragment shader clipping. Fragments with any of these components lower than zero are discarded. diff --git a/resources/shaders/140/mm_gouraud.fs b/resources/shaders/140/mm_gouraud.fs index c5fe86efc5..7c77d5565e 100644 --- a/resources/shaders/140/mm_gouraud.fs +++ b/resources/shaders/140/mm_gouraud.fs @@ -37,6 +37,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform SlopeDetection slope; @@ -87,7 +88,7 @@ void main() color = LightBlue; alpha = 1.0; } - else if( transformed_normal.z < slope.normal_z - EPSILON) + else if( dot(transformed_normal, slope.up_direction) < slope.normal_z - EPSILON) { color = color * 0.5 + LightRed * 0.5; alpha = 1.0; diff --git a/resources/shaders/140/mm_gouraud.vs b/resources/shaders/140/mm_gouraud.vs index b191e35fa8..191613f957 100644 --- a/resources/shaders/140/mm_gouraud.vs +++ b/resources/shaders/140/mm_gouraud.vs @@ -24,6 +24,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform SlopeDetection slope; void main() diff --git a/src/libslic3r/ExPolygon.hpp b/src/libslic3r/ExPolygon.hpp index 87a1146d41..7a51f70ef7 100644 --- a/src/libslic3r/ExPolygon.hpp +++ b/src/libslic3r/ExPolygon.hpp @@ -377,6 +377,11 @@ inline void translate(ExPolygons &expolys, const Point &p) { expoly.translate(p); } +inline void translate(Polygons &polys, const Point &p) { + for (Polygon &poly : polys) + poly.translate(p); +} + inline void polygons_append(Polygons &dst, const ExPolygon &src) { dst.reserve(dst.size() + src.holes.size() + 1); diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index f8001d2806..8317eca70a 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1010,7 +1010,7 @@ static std::vector s_Preset_machine_limits_options { static std::vector s_Preset_printer_options { "printer_technology", - "printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "gcode_flavor", + "printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "build_plate_tilt_x", "build_plate_tilt_y", "gcode_flavor", "fan_kickstart", "fan_speedup_time", "fan_speedup_overhangs", "single_extruder_multi_material", "manual_filament_change", "file_start_gcode", "machine_start_gcode", "machine_end_gcode", "before_layer_change_gcode", "printing_by_object_gcode", "layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode", "change_filament_gcode", "change_extrusion_role_gcode", "printer_model", "printer_variant", "printer_extruder_id", "printer_extruder_variant", "extruder_variant_list", "default_nozzle_volume_type", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index d4711a0da9..c9d2f588da 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -5922,6 +5922,30 @@ void PrintConfigDef::init_fff_params() def->mode = comSimple; def->set_default_value(new ConfigOptionFloatOrPercent(50., true)); + def = this->add("build_plate_tilt_x", coFloat); + def->label = L("Build plate tilt X"); + def->category = L("Support"); + def->tooltip = L("Tilt angle of the build plate along the X axis. " + "A positive value tilts the plate so the +X side is higher, shifting gravity toward -X and increasing overhangs on the +X side. " + "A negative value tilts the -X side higher. Set to 0 for no X-axis tilt."); + def->sidetext = u8"\u00B0"; + def->min = -45; + def->max = 45; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0.)); + + def = this->add("build_plate_tilt_y", coFloat); + def->label = L("Build plate tilt Y"); + def->category = L("Support"); + def->tooltip = L("Tilt angle of the build plate along the Y axis. " + "A positive value tilts the plate so the +Y side is higher, shifting gravity toward -Y and increasing overhangs on the +Y side. " + "A negative value tilts the -Y side higher. Set to 0 for no Y-axis tilt."); + def->sidetext = u8"\u00B0"; + def->min = -45; + def->max = 45; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0.)); + def = this->add("tree_support_branch_angle", coFloat); def->label = L("Tree support branch angle"); def->category = L("Support"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 7d40ef0706..6b9ad65185 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1409,6 +1409,9 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( PrintConfig, (MachineEnvelopeConfig, GCodeConfig), + // Build plate tilt for off-axis gravity support generation (printer-level setting). + ((ConfigOptionFloat, build_plate_tilt_x)) + ((ConfigOptionFloat, build_plate_tilt_y)) //BBS ((ConfigOptionInts, additional_cooling_fan_speed)) ((ConfigOptionBool, reduce_crossing_wall)) diff --git a/src/libslic3r/Support/SupportMaterial.cpp b/src/libslic3r/Support/SupportMaterial.cpp index d1fa5d8627..c230924ac8 100644 --- a/src/libslic3r/Support/SupportMaterial.cpp +++ b/src/libslic3r/Support/SupportMaterial.cpp @@ -1392,6 +1392,10 @@ static inline ExPolygons detect_overhangs( const double threshold_rad = Geometry::deg2rad(thresh_angle); const bool bridge_no_support = object_config.bridge_no_support.value; const coordf_t xy_expansion = scale_(object_config.support_expansion.value); + // Build plate tilt: compute per-layer XY shift for tilted gravity direction + const double tilt_x_rad = Geometry::deg2rad(print_config.build_plate_tilt_x.value); + const double tilt_y_rad = Geometry::deg2rad(print_config.build_plate_tilt_y.value); + const bool has_tilt = std::abs(tilt_x_rad) > EPSILON || std::abs(tilt_y_rad) > EPSILON; float lower_layer_offset = 0; if (layer_id == 0) @@ -1441,9 +1445,17 @@ static inline ExPolygons detect_overhangs( // Overhang polygons for this layer and region. Polygons diff_polygons; Polygons layerm_polygons = to_polygons(layerm->slices.surfaces); + // Apply build plate tilt: shift lower layer polygons to simulate tilted gravity + Polygons effective_lower = lower_layer_polygons; + if (has_tilt) { + const double lh = lower_layer.height; + Point tilt_shift(coord_t(scale_(lh * tan(tilt_y_rad))), + coord_t(scale_(lh * tan(tilt_x_rad)))); + translate(effective_lower, tilt_shift); + } if (lower_layer_offset == 0.f) { // Support everything. - diff_polygons = diff(layerm_polygons, lower_layer_polygons); + diff_polygons = diff(layerm_polygons, effective_lower); if (buildplate_only) { // Don't support overhangs above the top surfaces. // This step is done before the contact surface is calculated by growing the overhang region. @@ -1452,9 +1464,9 @@ static inline ExPolygons detect_overhangs( } else if (auto_normal_support) { // Get the regions needing a suport, collapse very tiny spots. //FIXME cache the lower layer offset if this layer has multiple regions. - diff_polygons = + diff_polygons = diff(layerm_polygons, - expand(lower_layer_polygons, lower_layer_offset, SUPPORT_SURFACES_OFFSET_PARAMETERS)); + expand(effective_lower, lower_layer_offset, SUPPORT_SURFACES_OFFSET_PARAMETERS)); if (buildplate_only && ! annotations.buildplate_covered[layer_id].empty()) { // Don't support overhangs above the top surfaces. // This step is done before the contact surface is calculated by growing the overhang region. @@ -1462,9 +1474,9 @@ static inline ExPolygons detect_overhangs( } if (! diff_polygons.empty()) { // Offset the support regions back to a full overhang, restrict them to the full overhang. - // This is done to increase size of the supporting columns below, as they are calculated by + // This is done to increase size of the supporting columns below, as they are calculated by // propagating these contact surfaces downwards. - diff_polygons = diff(intersection(expand(diff_polygons, lower_layer_offset, SUPPORT_SURFACES_OFFSET_PARAMETERS), layerm_polygons), lower_layer_polygons); + diff_polygons = diff(intersection(expand(diff_polygons, lower_layer_offset, SUPPORT_SURFACES_OFFSET_PARAMETERS), layerm_polygons), effective_lower); } //FIXME add user defined filtering here based on minimal area or minimum radius or whatever. diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 50272009cd..864a0eb0e0 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -671,6 +671,11 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) double thresh_angle = config.support_threshold_angle.value > EPSILON ? config.support_threshold_angle.value + 1 : 30; thresh_angle = std::min(thresh_angle, 89.); // should be smaller than 90 const double threshold_rad = Geometry::deg2rad(thresh_angle); + // Build plate tilt: compute per-layer XY shift for tilted gravity direction + const PrintConfig& print_cfg = m_object->print()->config(); + const double tilt_x_rad = Geometry::deg2rad(print_cfg.build_plate_tilt_x.value); + const double tilt_y_rad = Geometry::deg2rad(print_cfg.build_plate_tilt_y.value); + const bool has_tilt = std::abs(tilt_x_rad) > EPSILON || std::abs(tilt_y_rad) > EPSILON; // FIXME this is a fudge constant! double support_tree_tip_diameter = 0.8; auto enforcer_overhang_offset = scaled(support_tree_tip_diameter); @@ -813,8 +818,19 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) ExPolygons& curr_polys = layer->lslices_extrudable; ExPolygons& lower_polys = lower_layer->lslices_extrudable; + // Apply build plate tilt: shift lower layer polygons to simulate tilted gravity + ExPolygons shifted_lower; + if (has_tilt) { + shifted_lower = lower_polys; // copy + const double lh = lower_layer->height; + Point tilt_shift(coord_t(scale_(lh * tan(tilt_y_rad))), + coord_t(scale_(lh * tan(tilt_x_rad)))); + translate(shifted_lower, tilt_shift); + } + const ExPolygons &effective_lower = has_tilt ? shifted_lower : lower_polys; + // normal overhang - ExPolygons lower_layer_offseted = offset_ex(lower_polys, support_offset_scaled, SUPPORT_SURFACES_OFFSET_PARAMETERS); + ExPolygons lower_layer_offseted = offset_ex(effective_lower, support_offset_scaled, SUPPORT_SURFACES_OFFSET_PARAMETERS); overhangs_all_layers[layer_nr] = std::move(diff_ex(curr_polys, lower_layer_offseted)); double duration{ std::chrono::duration_cast(clock_::now() - t0).count() }; diff --git a/src/libslic3r/Support/TreeSupport3D.cpp b/src/libslic3r/Support/TreeSupport3D.cpp index 307e4d314d..2c24b933b2 100644 --- a/src/libslic3r/Support/TreeSupport3D.cpp +++ b/src/libslic3r/Support/TreeSupport3D.cpp @@ -208,6 +208,10 @@ static std::vector>> group_me const bool support_threshold_auto = support_threshold == 0; // +1 makes the threshold inclusive double tan_threshold = support_threshold_auto ? 0. : tan(M_PI * double(support_threshold + 1) / 180.); + // Build plate tilt: compute per-layer XY shift for tilted gravity direction + const double tilt_x_rad = Geometry::deg2rad(print_config.build_plate_tilt_x.value); + const double tilt_y_rad = Geometry::deg2rad(print_config.build_plate_tilt_y.value); + const bool has_tilt = std::abs(tilt_x_rad) > EPSILON || std::abs(tilt_y_rad) > EPSILON; //FIXME this is a fudge constant! auto enforcer_overhang_offset = scaled(config.tree_support_tip_diameter.value); const coordf_t radius_sample_resolution = g_config_tree_support_collision_resolution; @@ -229,7 +233,7 @@ static std::vector>> group_me size_t num_overhang_layers = support_auto ? num_object_layers : std::min(num_object_layers, std::max(size_t(support_enforce_layers), enforcers_layers.size())); tbb::parallel_for(tbb::blocked_range(1, num_overhang_layers), [&print_object, &config, &print_config, &enforcers_layers, &blockers_layers, - support_auto, support_enforce_layers, support_threshold_auto, tan_threshold, enforcer_overhang_offset, num_raft_layers, radius_sample_resolution, &throw_on_cancel, &out] + support_auto, support_enforce_layers, support_threshold_auto, tan_threshold, enforcer_overhang_offset, num_raft_layers, radius_sample_resolution, has_tilt, tilt_x_rad, tilt_y_rad, &throw_on_cancel, &out] (const tbb::blocked_range &range) { for (LayerIndex layer_id = range.begin(); layer_id < range.end(); ++ layer_id) { const Layer ¤t_layer = *print_object.get_layer(layer_id); @@ -253,7 +257,15 @@ static std::vector>> group_me lower_layer_offset = external_perimeter_width - float(scale_(config.support_threshold_overlap.get_abs_value(unscale_(external_perimeter_width)))); } else lower_layer_offset = scaled(lower_layer.height / tan_threshold); - Polygons lower_layer_offseted = offset(lower_layer.lslices_extrudable, lower_layer_offset); + // Apply build plate tilt: shift lower layer polygons to simulate tilted gravity + Polygons lower_src = to_polygons(lower_layer.lslices_extrudable); + if (has_tilt) { + const double lh = lower_layer.height; + Point tilt_shift(coord_t(scale_(lh * tan(tilt_y_rad))), + coord_t(scale_(lh * tan(tilt_x_rad)))); + translate(lower_src, tilt_shift); + } + Polygons lower_layer_offseted = offset(lower_src, lower_layer_offset); overhangs = diff(current_layer.lslices_extrudable, lower_layer_offseted); if (lower_layer_offset == 0) { raw_overhangs = overhangs; diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp index a6d19f505c..695ac4c2ba 100644 --- a/src/libslic3r/TriangleSelector.cpp +++ b/src/libslic3r/TriangleSelector.cpp @@ -233,7 +233,7 @@ int TriangleSelector::select_unsplit_triangle(const Vec3f &hit, int facet_idx) c return this->select_unsplit_triangle(hit, facet_idx, neighbors); } -void TriangleSelector::select_patch(int facet_start, std::unique_ptr &&cursor, EnforcerBlockerType new_state, const Transform3d& trafo_no_translate, bool triangle_splitting, float highlight_by_angle_deg) +void TriangleSelector::select_patch(int facet_start, std::unique_ptr &&cursor, EnforcerBlockerType new_state, const Transform3d& trafo_no_translate, bool triangle_splitting, float highlight_by_angle_deg, const Vec3f &up_direction) { assert(facet_start < m_orig_size_indices); @@ -294,8 +294,8 @@ void TriangleSelector::select_patch(int facet_start, std::unique_ptr &&c int facet = facets_to_check[facet_idx]; const Vec3f& facet_normal = m_face_normals[m_triangles[facet].source_triangle]; Matrix3f normal_matrix = static_cast(trafo_no_translate.matrix().block(0, 0, 3, 3).inverse().transpose().cast()); - float world_normal_z = (normal_matrix* facet_normal).normalized().z(); - if (!visited[facet] && (highlight_by_angle_deg == 0.f || world_normal_z < highlight_angle_limit)) { + float world_normal_dot = (normal_matrix * facet_normal).normalized().dot(up_direction); + if (!visited[facet] && (highlight_by_angle_deg == 0.f || world_normal_dot < highlight_angle_limit)) { if (select_triangle(facet, new_state, triangle_splitting)) { // add neighboring facets to list to be processed later for (int neighbor_idx : m_neighbors[facet]) @@ -320,7 +320,7 @@ bool TriangleSelector::is_facet_clipped(int facet_idx, const ClippingPlane &clp) void TriangleSelector::seed_fill_select_triangles(const Vec3f &hit, int facet_start, const Transform3d& trafo_no_translate, const ClippingPlane &clp, float seed_fill_angle, float highlight_by_angle_deg, - bool force_reselection) + bool force_reselection, const Vec3f &up_direction) { assert(facet_start < m_orig_size_indices); @@ -344,8 +344,8 @@ void TriangleSelector::seed_fill_select_triangles(const Vec3f &hit, int facet_st const Vec3f &facet_normal = m_face_normals[m_triangles[current_facet].source_triangle]; Matrix3f normal_matrix = static_cast(trafo_no_translate.matrix().block(0, 0, 3, 3).inverse().transpose().cast()); - float world_normal_z = (normal_matrix * facet_normal).normalized().z(); - if (!visited[current_facet] && (highlight_by_angle_deg == 0.f || world_normal_z < highlight_angle_limit)) { + float world_normal_dot = (normal_matrix * facet_normal).normalized().dot(up_direction); + if (!visited[current_facet] && (highlight_by_angle_deg == 0.f || world_normal_dot < highlight_angle_limit)) { if (m_triangles[current_facet].is_split()) { for (int split_triangle_idx = 0; split_triangle_idx <= m_triangles[current_facet].number_of_split_sides(); ++split_triangle_idx) { assert(split_triangle_idx < int(m_triangles[current_facet].children.size())); diff --git a/src/libslic3r/TriangleSelector.hpp b/src/libslic3r/TriangleSelector.hpp index 50bbdd4ed0..5b20647b84 100644 --- a/src/libslic3r/TriangleSelector.hpp +++ b/src/libslic3r/TriangleSelector.hpp @@ -308,7 +308,8 @@ public: EnforcerBlockerType new_state, // enforcer or blocker? const Transform3d &trafo_no_translate, // matrix to get from mesh to world without translation bool triangle_splitting, // If triangles will be split base on the cursor or not - float highlight_by_angle_deg = 0.f); // The maximal angle of overhang. If it is set to a non-zero value, it is possible to paint only the triangles of overhang defined by this angle in degrees. + float highlight_by_angle_deg = 0.f, // The maximal angle of overhang. If it is set to a non-zero value, it is possible to paint only the triangles of overhang defined by this angle in degrees. + const Vec3f &up_direction = Vec3f::UnitZ()); // Up direction for overhang detection (accounts for build plate tilt) void seed_fill_select_triangles(const Vec3f &hit, // point where to start int facet_start, // facet of the original mesh (unsplit) that the hit point belongs to @@ -316,7 +317,8 @@ public: const ClippingPlane &clp, // Clipping plane to limit painting to not clipped facets only float seed_fill_angle, // the maximal angle between two facets to be painted by the same color float highlight_by_angle_deg = 0.f, // The maximal angle of overhang. If it is set to a non-zero value, it is possible to paint only the triangles of overhang defined by this angle in degrees. - bool force_reselection = false); // force reselection of the triangle mesh even in cases that mouse is pointing on the selected triangle + bool force_reselection = false, // force reselection of the triangle mesh even in cases that mouse is pointing on the selected triangle + const Vec3f &up_direction = Vec3f::UnitZ()); // Up direction for overhang detection (accounts for build plate tilt) void bucket_fill_select_triangles(const Vec3f &hit, // point where to start int facet_start, // facet of the original mesh (unsplit) that the hit point belongs to diff --git a/src/slic3r/GUI/3DBed.cpp b/src/slic3r/GUI/3DBed.cpp index 544a3f8f12..3f5cd306ad 100644 --- a/src/slic3r/GUI/3DBed.cpp +++ b/src/slic3r/GUI/3DBed.cpp @@ -394,6 +394,8 @@ void Bed3D::render_internal(GLCanvas3D& canvas, const Transform3d& view_matrix, case Type::Custom: { render_custom(canvas, view_matrix, projection_matrix, bottom); break; } } + render_gravity_arrow(view_matrix, projection_matrix); + glsafe(::glDisable(GL_DEPTH_TEST)); } @@ -731,6 +733,63 @@ void Bed3D::render_custom(GLCanvas3D& canvas, const Transform3d& view_matrix, co render_texture(bottom, canvas);*/ } +void Bed3D::render_gravity_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix) +{ + const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + double tilt_x_deg = cfg.opt_float("build_plate_tilt_x"); + double tilt_y_deg = cfg.opt_float("build_plate_tilt_y"); + if (tilt_x_deg == 0. && tilt_y_deg == 0.) { + m_gravity_arrow.reset(); + return; + } + + // Gravity direction (matching the slicer's tilt convention) + double tilt_x_rad = Geometry::deg2rad(tilt_x_deg); + double tilt_y_rad = Geometry::deg2rad(tilt_y_deg); + Vec3d gravity_dir = Vec3d(-tan(tilt_y_rad), -tan(tilt_x_rad), -1.0).normalized(); + + // Build the arrow model (same dimensions as the axis arrows) + if (!m_gravity_arrow.is_initialized()) { + const float stem_length = Axes::DefaultStemLength; + const float tip_radius = Axes::DefaultTipRadius; + const float tip_length = Axes::DefaultTipLength; + const float stem_radius = stem_length / 75.f; // same ratio as axis cylinders + m_gravity_arrow.init_from(stilized_arrow(16, tip_radius, tip_length, stem_radius, stem_length)); + } + + // The arrow model points along +Z by default. Compute rotation to align with gravity_dir. + // Rotation axis = cross(+Z, gravity_dir), angle = acos(dot(+Z, gravity_dir)) + Vec3d from = Vec3d::UnitZ(); + Vec3d to = gravity_dir; + double dot = from.dot(to); + Transform3d rot = Transform3d::Identity(); + if (dot < -0.9999) { + // Nearly opposite — rotate 180° around X + rot = Eigen::AngleAxisd(M_PI, Vec3d::UnitX()) * rot; + } else if (dot < 0.9999) { + Vec3d axis = from.cross(to).normalized(); + double angle = std::acos(std::clamp(dot, -1.0, 1.0)); + rot = Eigen::AngleAxisd(angle, axis) * rot; + } + + GLShaderProgram* shader = wxGetApp().get_shader("flat"); + if (shader == nullptr) + return; + + glsafe(::glEnable(GL_DEPTH_TEST)); + shader->start_using(); + + const Camera& camera = wxGetApp().plater()->get_camera(); + Transform3d model_matrix = rot; + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * model_matrix); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + + m_gravity_arrow.set_color({ 1.0f, 0.85f, 0.0f, 1.0f }); // yellow + m_gravity_arrow.render(); + + shader->stop_using(); +} + void Bed3D::render_default(bool bottom, const Transform3d& view_matrix, const Transform3d& projection_matrix) { // m_texture.reset(); diff --git a/src/slic3r/GUI/3DBed.hpp b/src/slic3r/GUI/3DBed.hpp index b791635fd3..b16ead575b 100644 --- a/src/slic3r/GUI/3DBed.hpp +++ b/src/slic3r/GUI/3DBed.hpp @@ -110,6 +110,7 @@ private: //GLTexture m_temp_texture; GLModel m_model; Vec3d m_model_offset{ Vec3d::Zero() }; + GLModel m_gravity_arrow; Axes m_axes; float m_scale_factor{ 1.0f }; @@ -177,7 +178,8 @@ private: void render_model(const Transform3d& view_matrix, const Transform3d& projection_matrix); void render_custom(GLCanvas3D& canvas, const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom); void render_default(bool bottom, const Transform3d& view_matrix, const Transform3d& projection_matrix); - + void render_gravity_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix); + // BBS: remove the bed picking logic // void register_raycasters_for_picking(const GLModel::Geometry& geometry, const Transform3d& trafo); }; diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index 1a1f652e71..e36f053667 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -1078,13 +1078,27 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type, bool enable_support; int support_threshold_angle = get_selection_support_threshold_angle(enable_support); - + float normal_z = -::cos(Geometry::deg2rad((float) support_threshold_angle)); - + + // Compute up direction accounting for build plate tilt + Vec3f up_direction = Vec3f::UnitZ(); + { + const DynamicPrintConfig& prt_cfg = GUI::wxGetApp().preset_bundle->printers.get_edited_preset().config; + double tilt_x_deg = prt_cfg.opt_float("build_plate_tilt_x"); + double tilt_y_deg = prt_cfg.opt_float("build_plate_tilt_y"); + if (tilt_x_deg != 0. || tilt_y_deg != 0.) { + double tilt_x_rad = Geometry::deg2rad(tilt_x_deg); + double tilt_y_rad = Geometry::deg2rad(tilt_y_deg); + up_direction = Vec3f(float(tan(tilt_y_rad)), float(tan(tilt_x_rad)), 1.f).normalized(); + } + } + shader->set_uniform("volume_world_matrix", volume.first->world_matrix()); shader->set_uniform("slope.actived", m_slope.isGlobalActive && !volume.first->is_modifier && !volume.first->is_wipe_tower); shader->set_uniform("slope.volume_world_normal_matrix", static_cast(volume.first->world_matrix().matrix().block(0, 0, 3, 3).inverse().transpose().cast())); shader->set_uniform("slope.normal_z", normal_z); + shader->set_uniform("slope.up_direction", up_direction); #if ENABLE_ENVIRONMENT_MAP unsigned int environment_texture_id = GUI::wxGetApp().plater()->get_environment_texture_id(); diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index 5c3c0267ee..3cc39fe1ce 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -64,6 +64,7 @@ static SettingsFactory::Bundle FREQ_SETTINGS_BUNDLE_FFF = { L("Support") , { "enable_support", "support_type", "support_threshold_angle", "support_threshold_overlap", "support_base_pattern", "support_on_build_plate_only","support_critical_regions_only", "support_remove_small_overhang", + "build_plate_tilt_x", "build_plate_tilt_y", "support_base_pattern_spacing", "support_expansion"}}, //BBS { L("Flush options") , { "flush_into_infill", "flush_into_objects", "flush_into_support"} } @@ -94,7 +95,8 @@ std::map> SettingsFactory::OBJECT_C {"support_bottom_z_distance", "",19},{"support_top_z_distance", "",20},{"support_base_pattern", "",21},{"support_base_pattern_spacing", "",22}, {"support_interface_top_layers", "",23},{"support_interface_bottom_layers", "",24},{"support_interface_spacing", "",25},{"support_bottom_interface_spacing", "",26}, {"support_object_xy_distance", "",27}, {"bridge_no_support", "",28},{"max_bridge_length", "",29},{"support_critical_regions_only", "",30},{"support_remove_small_overhang","",31}, - {"support_object_first_layer_gap","",32} + {"build_plate_tilt_x","",32},{"build_plate_tilt_y","",33}, + {"support_object_first_layer_gap","",34} }}, { L("Speed"), {{"support_speed", "",12}, {"support_interface_speed", "",13} }} diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp index 02f1b4be5f..88781f6006 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp @@ -537,6 +537,14 @@ int GLGizmoFdmSupports::get_selection_support_threshold_angle() return auto_support ? support_threshold_angle : 0; } +std::pair GLGizmoFdmSupports::get_build_plate_tilt() +{ + const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + double tilt_x = cfg.opt_float("build_plate_tilt_x"); + double tilt_y = cfg.opt_float("build_plate_tilt_y"); + return {tilt_x, tilt_y}; +} + void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block) { float threshold = (float(M_PI)/180.f)*threshold_deg; @@ -544,6 +552,12 @@ void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block) const ModelObject* mo = m_c->selection_info()->model_object(); const ModelInstance* mi = mo->instances[selection.get_instance_idx()]; + // Compute gravity direction accounting for build plate tilt + auto [tilt_x_deg, tilt_y_deg] = get_build_plate_tilt(); + double tilt_x_rad = tilt_x_deg * M_PI / 180.0; + double tilt_y_rad = tilt_y_deg * M_PI / 180.0; + Vec3d gravity_dir = Vec3d(-tan(tilt_y_rad), -tan(tilt_x_rad), -1.0).normalized(); + int mesh_id = -1; for (const ModelVolume* mv : mo->volumes) { if (! mv->is_model_part()) @@ -552,10 +566,8 @@ void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block) ++mesh_id; const Transform3d trafo_matrix = mi->get_matrix_no_offset() * mv->get_matrix_no_offset(); - Vec3f down = (trafo_matrix.inverse() * (-Vec3d::UnitZ())).cast().normalized(); - Vec3f limit = (trafo_matrix.inverse() * Vec3d(std::sin(threshold), 0, -std::cos(threshold))).cast().normalized(); - - float dot_limit = limit.dot(down); + Vec3f down = (trafo_matrix.inverse() * gravity_dir).cast().normalized(); + float dot_limit = std::cos(threshold); // Now calculate dot product of vert_direction and facets' normals. int idx = 0; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp index ec0faeab80..0afdba7f19 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp @@ -59,6 +59,7 @@ private: void select_facets_by_angle(float threshold, bool block); // BBS int get_selection_support_threshold_angle(); + std::pair get_build_plate_tilt(); int m_support_threshold_angle = -1; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp index 7d3f40bd12..71e4c45f4a 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp @@ -73,6 +73,18 @@ GLGizmoPainterBase::ClippingPlaneDataWrapper GLGizmoPainterBase::get_clipping_pl return clp_data_out; } +Vec3f GLGizmoPainterBase::get_tilt_up_direction() const +{ + const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + double tilt_x_deg = cfg.opt_float("build_plate_tilt_x"); + double tilt_y_deg = cfg.opt_float("build_plate_tilt_y"); + if (tilt_x_deg == 0. && tilt_y_deg == 0.) + return Vec3f::UnitZ(); + double tilt_x_rad = Geometry::deg2rad(tilt_x_deg); + double tilt_y_rad = Geometry::deg2rad(tilt_y_deg); + return Vec3f(float(tan(tilt_y_rad)), float(tan(tilt_x_rad)), 1.f).normalized(); +} + void GLGizmoPainterBase::render_triangles(const Selection& selection) const { auto* shader = wxGetApp().get_shader("mm_gouraud"); @@ -119,11 +131,15 @@ void GLGizmoPainterBase::render_triangles(const Selection& selection) const float normal_z = -::cos(Geometry::deg2rad(m_highlight_by_angle_threshold_deg)); Matrix3f normal_matrix = static_cast(trafo_matrix.matrix().block(0, 0, 3, 3).inverse().transpose().cast()); + // Compute up direction accounting for build plate tilt + Vec3f up_direction = get_tilt_up_direction(); + shader->set_uniform("volume_world_matrix", trafo_matrix); shader->set_uniform("volume_mirrored", is_left_handed); shader->set_uniform("slope.actived", m_parent.is_using_slope()); shader->set_uniform("slope.volume_world_normal_matrix", normal_matrix); shader->set_uniform("slope.normal_z", normal_z); + shader->set_uniform("slope.up_direction", up_direction); m_triangle_selectors[mesh_id]->render(m_imgui, trafo_matrix); if (is_left_handed) @@ -691,7 +707,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous mi->get_assemble_transformation().get_matrix() * mo->volumes[m_rr.mesh_id]->get_matrix() : mi->get_transformation().get_matrix() * mo->volumes[m_rr.mesh_id]->get_matrix(); m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, int(m_rr.facet), trafo_matrix_not_translate, this->get_clipping_plane_in_volume_coordinates(trafo_matrix), m_smart_fill_angle, - m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, true); + m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, true, get_tilt_up_direction()); m_triangle_selectors[m_rr.mesh_id]->request_update_render_data(); m_seed_fill_last_mesh_id = m_rr.mesh_id; } @@ -803,7 +819,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous std::unique_ptr cursor = TriangleSelector::SinglePointCursor::cursor_factory(phr.z_world, camera_pos, m_cursor_height, trafo_matrix, clp); m_triangle_selectors[mesh_idx]->select_patch(int(phr.first_facet_idx), std::move(cursor), new_state, trafo_matrix_not_translate, - m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f); + m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, get_tilt_up_direction()); m_triangle_selectors[mesh_idx]->request_update_render_data(true); m_last_mouse_click = _mouse_position; @@ -855,7 +871,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous m_triangle_selectors[mesh_idx]->seed_fill_apply_on_triangles(new_state); if (m_tool_type == ToolType::SMART_FILL) m_triangle_selectors[mesh_idx]->seed_fill_select_triangles(mesh_hit, facet_idx, trafo_matrix_not_translate, clp, m_smart_fill_angle, - m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, true); + m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, true, get_tilt_up_direction()); else if (m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER) // BBS: add infill_angle parameter m_triangle_selectors[mesh_idx]->bucket_fill_select_triangles(mesh_hit, facet_idx, clp, -1.f, false, true); @@ -874,12 +890,12 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous camera_pos, m_cursor_radius, m_cursor_type, trafo_matrix, clp); m_triangle_selectors[mesh_idx]->select_patch(int(first_position.facet_idx), std::move(cursor), new_state, trafo_matrix_not_translate, - m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f); + m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, get_tilt_up_direction()); } else { for (auto first_position_it = projected_mouse_positions.cbegin(); first_position_it != projected_mouse_positions.cend() - 1; ++first_position_it) { auto second_position_it = first_position_it + 1; std::unique_ptr cursor = TriangleSelector::DoublePointCursor::cursor_factory(first_position_it->mesh_hit, second_position_it->mesh_hit, camera_pos, m_cursor_radius, m_cursor_type, trafo_matrix, clp); - m_triangle_selectors[mesh_idx]->select_patch(int(first_position_it->facet_idx), std::move(cursor), new_state, trafo_matrix_not_translate, m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f); + m_triangle_selectors[mesh_idx]->select_patch(int(first_position_it->facet_idx), std::move(cursor), new_state, trafo_matrix_not_translate, m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, get_tilt_up_direction()); } } } @@ -953,7 +969,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous const TriangleSelector::ClippingPlane &clp = this->get_clipping_plane_in_volume_coordinates(trafo_matrix); if (m_tool_type == ToolType::SMART_FILL) m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, int(m_rr.facet), trafo_matrix_not_translate, clp, m_smart_fill_angle, - m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f); + m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, false, get_tilt_up_direction()); else if (m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER) // BBS: add infill_angle parameter m_triangle_selectors[m_rr.mesh_id]->bucket_fill_select_triangles(m_rr.hit, int(m_rr.facet), clp, -1.f, false); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp index 5fc1a9e18f..9dbed7df7e 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp @@ -281,6 +281,9 @@ protected: bool m_paint_on_overhangs_only = false; float m_highlight_by_angle_threshold_deg = 0.f; + // Returns the up direction accounting for build plate tilt (default: UnitZ) + Vec3f get_tilt_up_direction() const; + GLModel m_circle; Vec2d m_old_center{ Vec2d::Zero() }; float m_old_cursor_radius{ 0.0f }; diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 6141d96885..70265aa81d 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4365,6 +4365,8 @@ void TabPrinter::build_fff() optgroup->append_single_option_line(option, "printer_basic_information_printable_space#excluded-bed-area"); // optgroup->append_single_option_line("printable_area"); optgroup->append_single_option_line("printable_height", "printer_basic_information_printable_space#printable-height"); + optgroup->append_single_option_line("build_plate_tilt_x"); + optgroup->append_single_option_line("build_plate_tilt_y"); optgroup->append_single_option_line("support_multi_bed_types","printer_basic_information_printable_space#support-multi-bed-types"); optgroup->append_single_option_line("best_object_pos", "printer_basic_information_printable_space#best-object-position"); // todo: for multi_extruder test