Compare commits

...
Author SHA1 Message Date
Hanif Koh 9fe75c776d Refuse a Filament Flow Ratio of 0 Instead of Writing NaN Extrusion
A used filament with a flow ratio of 0 sliced in the GUI and wrote
E-92233720368547.758 on every extrusion move: GCode::_extrude multiplied
the flow by the ratio and divided by it again, giving 0 * 0 / 0 = NaN.
The CLI already rejected the value through the config check, but
Print::validate never looked at it.

Print::validate now stops the slice with an error naming the filament
when a used filament's flow ratio is 0 or nil, and _extrude converts the
flow to extrusion length with the bare filament cross-section, so the
ratio is applied once without a divide.
2026-10-21 14:19:17 +08:00
Lam Wei Lun a06ed2d182 Unify the below-plate view across the painting gizmos
The painting gizmos clip the selected instance at the plate, so a sinking
object stopped at the bed with nothing to show how it continues below it, and
the plate disappeared along with the bed mesh. Give every paint tool the same
view of that region:

- Draw the part of the selected instance below the plate at 50% opacity with
  depth writes off, so it reads as the object going through the plate.
- Keep the plate and its grid while the bed mesh is hidden, instead of losing
  every reference plane in the scene.
- Mark the plate intersection with a 0.4 mm outline rather than an opaque
  filled cap, which covered the model and hid the ghost from above.
- Gate the bed mesh and the below-plate pass on the same predicate. Texture
  displacement derives from GLGizmoPainterBase like the other painters but was
  missing from the gizmo type list in is_bed_visible(), so it kept its bed.
- Add GLVolume::simple_render()'s ban_color_volume so the ghost keeps its alpha
  for volumes with MMU segmentation facets, whose sub-models carry their own
  opaque filament colours, and stop gating the ghost on is_active: the colour
  painter marks the whole instance inactive and draws it in its own pass.
2026-10-21 13:04:16 +08:00
9 changed files with 163 additions and 16 deletions

No files matched your search

+3 -4
View File
@@ -9214,10 +9214,9 @@ std::string GCode::_extrude(const ExtrusionPath &path, const std::string &path_d
effective_height = static_cast<float>(m_sub_layer_height);
}
// Effective extrusion length per distance unit = (filament_flow_ratio/cross_section) * mm3_per_mm / print flow ratio
// m_writer.extruder()->e_per_mm3() below is (filament flow ratio / cross-sectional area)
double e_per_mm = m_writer.filament()->e_per_mm3() * _mm3_per_mm;
e_per_mm /= filament_flow_ratio;
// _mm3_per_mm already includes the filament flow ratio, so convert it with the bare cross-section
// rather than e_per_mm3(), which would apply the ratio a second time.
double e_per_mm = _mm3_per_mm / m_writer.filament()->filament_crossection();
// set speed
if (speed == -1) {
+17
View File
@@ -1882,6 +1882,23 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
if (extruders.empty())
return { L("No extrusions under current settings.") };
// A flow ratio of 0 would extrude nothing, and a nil one would write NaN extrusion lengths.
{
const std::vector<double> &flow_ratios = m_config.filament_flow_ratio.values;
const std::vector<int> &self_index = m_config.filament_self_index.values;
for (size_t column = 0; column < flow_ratios.size(); ++column) {
// filament_self_index maps a column to its filament once the filament arrays hold one column per variant
const unsigned int filament_id = self_index.size() == flow_ratios.size() ? self_index[column] - 1 : column;
if (flow_ratios[column] > 0. || !std::binary_search(extruders.begin(), extruders.end(), filament_id))
continue;
StringObjectException err;
// TRN %1% is the filament number as shown in the filament list, starting from 1.
err.string = Slic3r::format(L("The flow ratio of filament %1% must be greater than 0."), filament_id + 1);
err.opt_key = "filament_flow_ratio";
return err;
}
}
// IDEX/IQEX: block multi-color in non-primary parallel modes when the active
// configuration can't physically support it (no within-gantry toolchange path
// or MMU lane sharing). See imex_multicolor_block_reason() for the full rule.
+2 -2
View File
@@ -700,7 +700,7 @@ void GLVolume::render_with_outline(const GUI::Size& cnv_size)
}
//BBS add render for simple case
void GLVolume::simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_objects, std::vector<ColorRGBA>& extruder_colors, bool ban_light)
void GLVolume::simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_objects, std::vector<ColorRGBA>& extruder_colors, bool ban_light, bool ban_color_volume)
{
if (this->is_left_handed())
glFrontFace(GL_CW);
@@ -734,7 +734,7 @@ void GLVolume::simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_obj
}
} while (0);
if (color_volume && !picking) {
if (color_volume && !picking && !ban_color_volume) {
const bool brighten_selected = selected && !disabled && !force_native_color && !force_neutral_color;
// when force_transparent, we need to keep the alpha
+1 -1
View File
@@ -370,7 +370,7 @@ public:
virtual void render_with_outline(const GUI::Size& cnv_size);
//BBS: add simple render function for thumbnail
void simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_objects, std::vector<ColorRGBA>& extruder_colors, bool ban_light =false);
void simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_objects, std::vector<ColorRGBA>& extruder_colors, bool ban_light =false, bool ban_color_volume = false);
void set_bounding_boxes_as_dirty() {
m_transformed_bounding_box.reset();
+86 -4
View File
@@ -2804,7 +2804,9 @@ void GLCanvas3D::_render_scene(const Camera& camera, const Size& cnv_size)
_render_bed(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward(), show_axes);
}
m_frame_profiler.mark("bed");
if (show_bed) //BBS: add outline logic
// Exception: the painting gizmos hide the bed mesh on purpose and draw the plate instead,
// so there the plate list is the only bed reference in the scene.
if (show_bed || _is_below_plate_view_active()) //BBS: add outline logic
_render_platelist(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward(), only_current, only_body, hover_id, true, show_grid);
if (m_design_canvas && show_bed)
// Design tab: replace the plate's corner-origin grid with the origin-centred CAD grid.
@@ -2816,6 +2818,7 @@ void GLCanvas3D::_render_scene(const Camera& camera, const Size& cnv_size)
_render_shadows(camera.get_view_matrix(), camera.get_projection_matrix());
m_frame_profiler.mark("shadows");
_render_objects(GLVolumeCollection::ERenderType::Opaque, !m_gizmos.is_running());
_render_below_plate_volumes();
m_frame_profiler.mark("objects");
_render_sla_slices();
_render_selection();
@@ -8824,6 +8827,84 @@ void GLCanvas3D::_render_bed(const Transform3d& view_matrix, const Transform3d&
m_bed.render(*this, view_matrix, projection_matrix, bottom, scale_factor, show_axes);
}
bool GLCanvas3D::_is_below_plate_view_active() const
{
return dynamic_cast<const GLGizmoPainterBase*>(m_gizmos.get_current()) != nullptr && m_gizmos.is_hiding_instances();
}
// The painting gizmo clips the selected object at the plate, so the part under the plate is not drawn.
// This pass draws that part of the selected instance again at 50% opacity, depth writes off.
void GLCanvas3D::_render_below_plate_volumes()
{
if (!_is_below_plate_view_active() || m_volumes.empty())
return;
GLShaderProgram* shader = wxGetApp().get_shader("gouraud");
if (shader == nullptr)
return;
const Camera& camera = wxGetApp().plater()->get_camera();
const Transform3d& view_matrix = camera.get_view_matrix();
const Transform3d& proj_matrix = camera.get_projection_matrix();
const int object_idx = m_selection.get_object_idx();
const int instance_idx = m_selection.get_instance_idx();
// Depth test stays on so whatever is already in the depth buffer hides the ghost; depth writes
// are off and back faces are culled, so each pixel blends only the near surface once.
const GLboolean cull_face = ::glIsEnabled(GL_CULL_FACE);
glsafe(::glEnable(GL_DEPTH_TEST));
glsafe(::glEnable(GL_CULL_FACE));
glsafe(::glCullFace(GL_BACK));
glsafe(::glDepthMask(GL_FALSE));
glsafe(::glEnable(GL_BLEND));
glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
shader->start_using();
// Keep z <= SINKING_Z_THRESHOLD, the complement of the clip the painting gizmo applies.
shader->set_uniform("z_range", std::array<float, 2>{ -FLT_MAX, float(SINKING_Z_THRESHOLD) });
shader->set_uniform("clipping_plane", m_volumes.get_clipping_plane());
shader->set_uniform("use_color_clip_plane", false);
shader->set_uniform("is_outline", false);
shader->set_uniform("slope.actived", false);
shader->set_uniform("print_volume.type", -1);
shader->set_uniform("extruder_printable_heights", std::array<float, 3>{ 0.0f, 0.0f, 0.0f });
shader->set_uniform("shadow_intensity", 0.0f);
// simple_render() needs these to reach the plain surface; with ban_color_volume it ignores both.
ModelObjectPtrs& model_objects = wxGetApp().model().objects;
std::vector<ColorRGBA> extruder_colors = wxGetApp().plater()->get_extruders_colors();
for (GLVolume* volume : m_volumes.volumes) {
// is_active cannot gate this pass: the color painter marks the whole instance inactive and
// draws it in its own gizmo pass. The instance and print-part checks below are what pick
// this ghost out of the scene.
if (volume->object_idx() != object_idx || volume->instance_idx() != instance_idx ||
volume->is_modifier || volume->is_wipe_tower || volume->volume_idx() < 0)
continue;
volume->set_render_color();
ColorRGBA color = volume->render_color;
color.a(0.5f);
volume->model.set_color(color);
const Transform3d model_matrix = volume->world_matrix();
shader->set_uniform("view_model_matrix", view_matrix * model_matrix);
shader->set_uniform("projection_matrix", proj_matrix);
shader->set_uniform("volume_world_matrix", model_matrix);
const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose();
shader->set_uniform("view_normal_matrix", view_normal_matrix);
// Painted facets would send render() through the MMU segmentation sub-models, which carry
// their own opaque filament colours; the ghost draws the plain surface so the alpha holds.
volume->simple_render(shader, model_objects, extruder_colors, false, true);
}
shader->stop_using();
glsafe(::glDisable(GL_BLEND));
glsafe(::glDepthMask(GL_TRUE));
if (cull_face == GL_FALSE)
glsafe(::glDisable(GL_CULL_FACE));
}
bool GLCanvas3D::_hides_inactive_plates() const
{
return m_canvas_type == ECanvasType::CanvasView3D && !m_design_canvas && wxGetApp().focus_mode();
@@ -11952,9 +12033,10 @@ bool GLCanvas3D::is_bed_visible() const
if (!m_main_toolbar.is_enabled())
return true;
const auto type = m_gizmos.get_current_type();
return type != GLGizmosManager::FdmSupports && type != GLGizmosManager::Seam &&
type != GLGizmosManager::MmSegmentation && type != GLGizmosManager::FuzzySkin;
// The painting gizmos clip the selected instance at the plate (see InstancesHider), so the bed
// mesh would sit in front of the part below it. They draw the plate and its grid instead (see
// _render_scene) and the below-plate part as a translucent ghost.
return !_is_below_plate_view_active();
}
Vec3d GLCanvas3D::get_camera_pan_anchor(Camera& camera, ECameraNavigationType navigation_type,
+2
View File
@@ -1552,6 +1552,8 @@ private:
void _render_imex_ghost_tooltip();
void _render_section_view_caps();
void _render_wireframe_overlay();
bool _is_below_plate_view_active() const;
void _render_below_plate_volumes();
bool _is_xray_view_active() const;
void _render_xray_volumes();
//BBS: GUI refactor: add canvas size as parameters
+4 -5
View File
@@ -7,7 +7,6 @@
#include <cstddef>
#include <glad/gl.h>
#include "slic3r/GUI/Selection.hpp"
#include <limits>
#include "libslic3r/Model.hpp"
@@ -174,6 +173,9 @@ void InstancesHider::on_update()
m_clippers.emplace_back(new MeshClipper);
m_clippers.back()->set_plane(ClippingPlane(-Vec3d::UnitZ(), z_min));
m_clippers.back()->set_mesh(mesh->its);
// Outline only: a filled cap would read as a surface the object does not have, and it
// covers the model below it. 0.4 mm matches the cut gizmo's contour width.
m_clippers.back()->set_behaviour(false, 0.4);
}
m_old_meshes = meshes;
}
@@ -214,10 +216,7 @@ void InstancesHider::render_cut() const
else
clipper->set_limiting_plane(ClippingPlane::ClipsNothing());
glsafe(::glPushAttrib(GL_DEPTH_TEST));
glsafe(::glDisable(GL_DEPTH_TEST));
clipper->render_cut(mv->is_model_part() ? ColorRGBA(0.8f, 0.3f, 0.0f, 1.0f) : color_from_model_volume(*mv));
glsafe(::glPopAttrib());
clipper->render_contour(mv->is_model_part() ? ColorRGBA(0.8f, 0.3f, 0.0f, 1.0f) : color_from_model_volume(*mv));
++clipper_id;
}
+23
View File
@@ -4,6 +4,7 @@
#include <catch2/catch_message.hpp>
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/GCodeReader.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/ModelArrange.hpp"
#include "libslic3r/Print.hpp"
@@ -89,6 +90,28 @@ TEST_CASE("The exclude object tolerance defaults to the former fixed 0.1 mm", "[
Catch::Matchers::WithinAbs(0.1, 1e-9));
}
TEST_CASE("The filament flow ratio scales the extruded length once", "[GCode]")
{
// Retraction off, so every positive relative E move is extrusion and scales with the ratio.
auto extruded_length = [](double flow_ratio) {
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({ { "use_relative_e_distances", "1" }, { "retraction_length", "0" }, { "wipe", "0" } });
config.set_key_value("filament_flow_ratio", new ConfigOptionFloatsNullable{ flow_ratio });
double total = 0.;
GCodeReader reader;
reader.parse_buffer(Test::slice({ Test::cube(20) }, config),
[&](GCodeReader &, const GCodeReader::GCodeLine &line) {
if ((line.cmd_is("G1") || line.cmd_is("G2") || line.cmd_is("G3")) && line.has_e() && line.e() > 0.f)
total += line.e();
});
return total;
};
const double full = extruded_length(1.);
REQUIRE(full > 0.);
CHECK_THAT(extruded_length(0.9) / full, Catch::Matchers::WithinRel(0.9, 1e-3));
}
// Firmwares read the compatibility header lines (Anycubic's GoKlipper heats the bed with
// first_layer_bed_temperature), so they must describe the filament the plate prints, not filament 1.
TEST_CASE("The G-code header names the first layer temperatures of the filament the plate prints", "[GCode]")
+25
View File
@@ -547,6 +547,31 @@ TEST_CASE("Print::validate tolerates a null warnings pointer", "[Print][validate
CHECK(err.string.empty());
}
TEST_CASE("Print::validate refuses a used filament with a flow ratio of 0", "[Print][validate][Regression]")
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_key_value("filament_flow_ratio", new ConfigOptionFloatsNullable{ 0. });
std::vector<StringObjectException> warnings;
StringObjectException err = validate_cubes(config, warnings);
CHECK_FALSE(err.string.empty());
CHECK_FALSE(err.is_warning);
CHECK(err.opt_key == "filament_flow_ratio");
}
TEST_CASE("Print::validate ignores a flow ratio of 0 on an unused filament", "[Print][validate]")
{
// The cube prints with filament 1, so filament 2's ratio is never used.
DynamicPrintConfig config = multifilament_config(2, { { "filament_self_index", "1,2" } });
config.set_key_value("filament_flow_ratio", new ConfigOptionFloatsNullable{ 1., 0. });
std::vector<StringObjectException> warnings;
StringObjectException err = validate_cubes(config, warnings);
CHECK(err.string.empty());
}
TEST_CASE("Purge tower selection keeps ordinary printers on the classic path", "[Print][PurgeTower][Regression]")
{
DynamicPrintConfig config = multifilament_config(2, {