Compare commits

..
Author SHA1 Message Date
Lam Wei Lun ce9eb05747 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-20 13:01:48 +08:00
8 changed files with 95 additions and 114 deletions

No files matched your search

+2 -2
View File
@@ -700,7 +700,7 @@ void GLVolume::render_with_outline(const GUI::Size& cnv_size)
} }
//BBS add render for simple case //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()) if (this->is_left_handed())
glFrontFace(GL_CW); glFrontFace(GL_CW);
@@ -734,7 +734,7 @@ void GLVolume::simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_obj
} }
} while (0); } 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; const bool brighten_selected = selected && !disabled && !force_native_color && !force_neutral_color;
// when force_transparent, we need to keep the alpha // 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); virtual void render_with_outline(const GUI::Size& cnv_size);
//BBS: add simple render function for thumbnail //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() { void set_bounding_boxes_as_dirty() {
m_transformed_bounding_box.reset(); 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); _render_bed(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward(), show_axes);
} }
m_frame_profiler.mark("bed"); 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); _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) if (m_design_canvas && show_bed)
// Design tab: replace the plate's corner-origin grid with the origin-centred CAD grid. // 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()); _render_shadows(camera.get_view_matrix(), camera.get_projection_matrix());
m_frame_profiler.mark("shadows"); m_frame_profiler.mark("shadows");
_render_objects(GLVolumeCollection::ERenderType::Opaque, !m_gizmos.is_running()); _render_objects(GLVolumeCollection::ERenderType::Opaque, !m_gizmos.is_running());
_render_below_plate_volumes();
m_frame_profiler.mark("objects"); m_frame_profiler.mark("objects");
_render_sla_slices(); _render_sla_slices();
_render_selection(); _render_selection();
@@ -8840,6 +8843,84 @@ void GLCanvas3D::_render_bed(const Transform3d& view_matrix, const Transform3d&
m_bed.render(*this, view_matrix, projection_matrix, bottom, scale_factor, show_axes); 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 bool GLCanvas3D::_hides_inactive_plates() const
{ {
return m_canvas_type == ECanvasType::CanvasView3D && !m_design_canvas && wxGetApp().focus_mode(); return m_canvas_type == ECanvasType::CanvasView3D && !m_design_canvas && wxGetApp().focus_mode();
@@ -11968,9 +12049,10 @@ bool GLCanvas3D::is_bed_visible() const
if (!m_main_toolbar.is_enabled()) if (!m_main_toolbar.is_enabled())
return true; return true;
const auto type = m_gizmos.get_current_type(); // The painting gizmos clip the selected instance at the plate (see InstancesHider), so the bed
return type != GLGizmosManager::FdmSupports && type != GLGizmosManager::Seam && // mesh would sit in front of the part below it. They draw the plate and its grid instead (see
type != GLGizmosManager::MmSegmentation && type != GLGizmosManager::FuzzySkin; // _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, 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_imex_ghost_tooltip();
void _render_section_view_caps(); void _render_section_view_caps();
void _render_wireframe_overlay(); void _render_wireframe_overlay();
bool _is_below_plate_view_active() const;
void _render_below_plate_volumes();
bool _is_xray_view_active() const; bool _is_xray_view_active() const;
void _render_xray_volumes(); void _render_xray_volumes();
//BBS: GUI refactor: add canvas size as parameters //BBS: GUI refactor: add canvas size as parameters
+4 -5
View File
@@ -7,7 +7,6 @@
#include <cstddef> #include <cstddef>
#include <glad/gl.h>
#include "slic3r/GUI/Selection.hpp" #include "slic3r/GUI/Selection.hpp"
#include <limits> #include <limits>
#include "libslic3r/Model.hpp" #include "libslic3r/Model.hpp"
@@ -174,6 +173,9 @@ void InstancesHider::on_update()
m_clippers.emplace_back(new MeshClipper); m_clippers.emplace_back(new MeshClipper);
m_clippers.back()->set_plane(ClippingPlane(-Vec3d::UnitZ(), z_min)); m_clippers.back()->set_plane(ClippingPlane(-Vec3d::UnitZ(), z_min));
m_clippers.back()->set_mesh(mesh->its); 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; m_old_meshes = meshes;
} }
@@ -214,10 +216,7 @@ void InstancesHider::render_cut() const
else else
clipper->set_limiting_plane(ClippingPlane::ClipsNothing()); clipper->set_limiting_plane(ClippingPlane::ClipsNothing());
glsafe(::glPushAttrib(GL_DEPTH_TEST)); clipper->render_contour(mv->is_model_part() ? ColorRGBA(0.8f, 0.3f, 0.0f, 1.0f) : color_from_model_volume(*mv));
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_id; ++clipper_id;
} }
-6
View File
@@ -2,7 +2,6 @@
#include "GeneratedConfig.hpp" #include "GeneratedConfig.hpp"
#include "libslic3r/Utils.hpp" #include "libslic3r/Utils.hpp"
#include "PluginAuditManager.hpp" #include "PluginAuditManager.hpp"
#include "PythonPluginBridge.hpp"
#include <atomic> #include <atomic>
#include <boost/filesystem/operations.hpp> #include <boost/filesystem/operations.hpp>
#include <boost/filesystem/path.hpp> #include <boost/filesystem/path.hpp>
@@ -665,11 +664,6 @@ bool PythonInterpreter::initialize()
// background-thread exceptions) to <data_dir>/log/python_*.log. // background-thread exceptions) to <data_dir>/log/python_*.log.
install_python_stderr_redirect(); install_python_stderr_redirect();
// Attribute plugin-spawned Python threads to the plugin/capability that created them.
// Must run while the GIL is still held here, after the audit hook is in place and
// before the GIL is released below.
install_python_thread_audit_shim();
// Release the GIL so other threads can acquire it via PyGILState_Ensure. // Release the GIL so other threads can acquire it via PyGILState_Ensure.
// Without this, calls from background threads will block trying to acquire the GIL. // Without this, calls from background threads will block trying to acquire the GIL.
m_main_thread_state = PyEval_SaveThread(); m_main_thread_state = PyEval_SaveThread();
-87
View File
@@ -603,93 +603,6 @@ request aborts the plugin load.)pbdoc");
} }
void install_python_thread_audit_shim()
{
// Runs with the GIL held from PythonInterpreter::initialize(). CPython's own thread bootstrap
// calls the target with no knowledge of the audit context that created it: the calling
// thread is the only place the plugin key and capability are still available. Capture them
// here, then re-establish the same ScopedPluginAuditContext inside the spawned thread before
// the plugin body runs. The identity lives only in this C++ closure -- no Python-visible
// setter is added.
try {
py::object thread_module = py::module_::import("_thread");
const int installed = PyObject_HasAttrString(thread_module.ptr(), "_orca_thread_audit_shim_installed");
if (installed == 1) {
BOOST_LOG_TRIVIAL(debug) << "[AUDIT] Python thread attribution shim already installed";
return;
}
if (installed < 0)
PyErr_Clear();
py::object original = py::reinterpret_steal<py::object>(
PyObject_GetAttrString(thread_module.ptr(), "start_new_thread"));
if (!original || !PyCallable_Check(original.ptr())) {
PyErr_Clear();
BOOST_LOG_TRIVIAL(error) << "[AUDIT] _thread.start_new_thread is missing or not callable; "
"plugin threads will run without audit attribution";
return;
}
py::object shim = py::cpp_function(
[original](py::object function, py::object args, py::object kwargs) -> py::object {
const std::string plugin_key = PluginAuditManager::instance().current_plugin();
if (plugin_key.empty()) {
// Not called from a plugin: preserve the stock implementation untouched.
return kwargs.is_none() ? original(function, args) : original(function, args, kwargs);
}
const std::string capability = PluginAuditManager::instance().current_capability();
py::object guarded = py::cpp_function(
[function, plugin_key, capability](py::args call_args, py::kwargs call_kwargs) -> py::object {
ScopedPluginAuditContext audit_scope(plugin_key, capability);
PyObject* result = PyObject_Call(function.ptr(), call_args.ptr(), call_kwargs.ptr());
if (result == nullptr)
throw py::error_already_set();
return py::reinterpret_steal<py::object>(result);
});
return kwargs.is_none() ? original(guarded, args) : original(guarded, args, kwargs);
},
py::arg("function"), py::arg("args"), py::arg("kwargs") = py::none());
if (PyObject_SetAttrString(thread_module.ptr(), "start_new_thread", shim.ptr()) < 0) {
PyErr_Clear();
BOOST_LOG_TRIVIAL(error)
<< "[AUDIT] Failed to install Python thread attribution shim on _thread.start_new_thread";
return;
}
if (PyObject_SetAttrString(thread_module.ptr(), "_orca_thread_audit_shim_installed", Py_True) < 0)
PyErr_Clear();
// threading.py caches the function at import time (as `_start_new_thread` in 3.12, and as
// `start_new_thread` in older versions). Patch the cached reference too when the module is
// already loaded; otherwise its Thread.start() would keep calling the unwrapped original.
py::object sys_modules = py::module_::import("sys").attr("modules");
PyObject* threading_module = PyDict_GetItemString(sys_modules.ptr(), "threading");
if (threading_module && threading_module != Py_None) {
const char* cached_names[] = {"_start_new_thread", "start_new_thread"};
for (const char* cached_name : cached_names) {
const int has_cached_name = PyObject_HasAttrString(threading_module, cached_name);
if (has_cached_name < 0) {
PyErr_Clear();
continue;
}
if (has_cached_name != 1)
continue;
if (PyObject_SetAttrString(threading_module, cached_name, shim.ptr()) < 0) {
PyErr_Clear();
BOOST_LOG_TRIVIAL(warning) << "[AUDIT] Failed to update threading." << cached_name
<< " with the Python thread attribution shim";
}
}
}
BOOST_LOG_TRIVIAL(info) << "[AUDIT] Python thread attribution shim installed";
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(error) << "[AUDIT] Failed to install Python thread attribution shim: " << ex.what();
}
}
} // namespace Slic3r } // namespace Slic3r
#ifdef ORCA_PYTHON_STUBGEN_MODULE #ifdef ORCA_PYTHON_STUBGEN_MODULE
-9
View File
@@ -13,15 +13,6 @@ namespace Slic3r { class PluginCapabilityInterface; }
namespace Slic3r { namespace Slic3r {
// Install the thread-attribution shim: wraps `_thread.start_new_thread` (and the reference
// cached by an already-imported `threading` module) so a thread spawned from inside a plugin
// call runs with that call's plugin key/capability audit scope. The captured identity is held
// in a private C++ wrapper; nothing is exposed to Python that could set it.
//
// Call with the GIL held, after PluginAuditManager::install_hook() and before the interpreter
// releases the GIL. Safe to call more than once per interpreter.
void install_python_thread_audit_shim();
// One materialized capability returned from finalize_plugin_capture: the C++ instance // One materialized capability returned from finalize_plugin_capture: the C++ instance
// and its resolved get_name() (cached while the GIL was held). // and its resolved get_name() (cached while the GIL was held).
struct CapturedCapability struct CapturedCapability