From 817d73c0e5284a564c3c6ab6adf52227a30d8af7 Mon Sep 17 00:00:00 2001 From: ExPikaPaka Date: Mon, 21 Sep 2026 14:55:39 +0200 Subject: [PATCH] Faster slicing of colour-painted layers, texture panel tools row A layer split into ~1000 colour fragments (a colour texture baked over a large top face) made several per-fragment loops redo whole-layer ClipperLib work, so slicing took ~33 min; it now takes ~3 min with the same output. - make_fills: clip the layer's no-overlap area to each expolygon's box before intersecting - discover_vertical_shells: small-piece filter compares only against the nearby part of the layer - bridge_over_infill: whole-layer union/diff/intersections restricted to the candidate's neighbourhood; fill boundary expanded once per spacing; anchor tree built only from lines crossing the scan range; bbox pre-check in the collision test; limiting outline taken directly instead of through expand(..., 0.3 * flow.spacing()), which offsets by 0.135 scaled units (flow.spacing() is in mm) and only cost a whole-layer pass per candidate - Bake job: include GUI.hpp for show_error() - Texture panel: select/erase whole model next to the paint tools, brush size slider back on the same row --- src/libslic3r/Fill/Fill.cpp | 10 +- src/libslic3r/PrintObject.cpp | 115 +++++++++++++++--- .../GUI/Gizmos/GLGizmoTextureDisplacement.cpp | 27 ++-- .../GUI/Jobs/TextureDisplacementBakeJob.cpp | 1 + 4 files changed, 123 insertions(+), 30 deletions(-) diff --git a/src/libslic3r/Fill/Fill.cpp b/src/libslic3r/Fill/Fill.cpp index 28fabed8af..0515befe6a 100644 --- a/src/libslic3r/Fill/Fill.cpp +++ b/src/libslic3r/Fill/Fill.cpp @@ -1409,7 +1409,15 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive: // Orca: Reuse the body origin used for bridge anchoring, resetting it for each surface. f->set_bounding_box(infill_bounding_box(*this, surface_fill, expoly, bbox)); - f->no_overlap_expolygons = intersection_ex(surface_fill.no_overlap_expolygons, ExPolygons() = {expoly}, ApplySafetyOffset::Yes); + // Only the part of the layer-wide no-overlap area under this expolygon matters, so clip it to the + // expolygon's box first (padded past the safety offset, which grows the clip side). The result is + // identical; the cost is not: a layer split into many small fills, e.g. by colour painting, + // otherwise intersects every one of them with the whole layer. + BoundingBox no_overlap_bbox = get_extents(expoly); + no_overlap_bbox.offset(SCALED_EPSILON); + f->no_overlap_expolygons = intersection_ex( + ClipperUtils::clip_clipper_polygons_with_subject_bbox(surface_fill.no_overlap_expolygons, no_overlap_bbox), + ExPolygons() = {expoly}, ApplySafetyOffset::Yes); if (params.symmetric_infill_y_axis) { params.symmetric_y_axis = f->extended_object_bounding_box().center().x(); expoly.symmetric_y(params.symmetric_y_axis); diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 3b7e889472..3dadd4c9c0 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -2580,15 +2580,34 @@ void PrintObject::discover_vertical_shells() // the in-model condition is there due to small sloping surfaces, e.g. top of the hull of the benchy // 2. the area does not fully cover an internal polygon // This is there mainly for a very thin parts, where the solid layers would be missing if the part area is quite small + // Both tests below compare a small piece against the whole layer. Done literally, that is + // quadratic in the number of pieces, which is what a layer split up by colour painting has, + // so each is restricted to the part of the layer near the piece with an identical result: + // object_volume is clipped to the piece's box, and only the internal polygons whose box meets + // the expanded piece take part in the count, since the others pass through the difference + // unchanged and add the same number to both sides of it. + std::vector internal_bboxes; + internal_bboxes.reserve(internal_volume.size()); + for (const Polygon &poly : internal_volume) + internal_bboxes.emplace_back(get_extents(poly)); regularized_shell.erase(std::remove_if(regularized_shell.begin(), regularized_shell.end(), - [&internal_volume, &min_perimeter_infill_spacing, + [&internal_volume, &internal_bboxes, &min_perimeter_infill_spacing, &object_volume](const ExPolygon &p) { - return (p.area() < min_perimeter_infill_spacing * scaled(1.5) || - (p.area() < min_perimeter_infill_spacing * scaled(8.0) && - diff(to_polygons(p), object_volume).empty())) && - diff(internal_volume, - expand(to_polygons(p), min_perimeter_infill_spacing)) - .size() >= internal_volume.size(); + const bool small = p.area() < min_perimeter_infill_spacing * scaled(1.5) || + (p.area() < min_perimeter_infill_spacing * scaled(8.0) && + diff(to_polygons(p), + ClipperUtils::clip_clipper_polygons_with_subject_bbox( + object_volume, get_extents(p).inflated(SCALED_EPSILON))) + .empty()); + if (!small) + return false; + const Polygons expanded = expand(to_polygons(p), min_perimeter_infill_spacing); + const BoundingBox bbox = get_extents(expanded); + Polygons nearby; + for (size_t i = 0; i < internal_volume.size(); ++i) + if (internal_bboxes[i].overlap(bbox)) + nearby.emplace_back(internal_volume[i]); + return diff(nearby, expanded).size() >= nearby.size(); }), regularized_shell.end()); } @@ -3159,6 +3178,16 @@ void PrintObject::bridge_over_infill() vertical_lines[i].b = Point{x, y_max}; } + // The vertical lines only span the bridged area's x range, so anchors entirely outside it can never be + // hit. Leaving them out gives the same intersections without building a tree over the whole layer's + // boundary for every bridge. + const coord_t scan_x_min = bb_x.min.x(); + const coord_t scan_x_max = bb_x.min.x() + coord_t(n_vlines) * scan_spacing; + anchors.erase(std::remove_if(anchors.begin(), anchors.end(), + [scan_x_min, scan_x_max](const Line &l) { + return std::max(l.a.x(), l.b.x()) < scan_x_min || std::min(l.a.x(), l.b.x()) > scan_x_max; + }), + anchors.end()); auto anchors_and_walls_tree = AABBTreeLines::LinesDistancer{std::move(anchors)}; auto bridged_area_tree = AABBTreeLines::LinesDistancer{to_lines(bridged_area)}; @@ -3403,28 +3432,61 @@ void PrintObject::bridge_over_infill() std::vector expanded_surfaces; expanded_surfaces.reserve(surfaces_by_layer[lidx].size()); + // The expanded fill boundary depends only on the bridging flow, and total_fill_area is not + // modified below, so build it once per spacing rather than once per candidate. A layer split + // into many candidates (e.g. by colour painting) otherwise repeats a layer-wide offset for each. + std::map boundary_by_spacing; + // expansion_area is a clean, non-overlapping set, so uniting it with a bridge or cutting a bridge + // out of it only changes the polygons near that bridge. The rest are passed through untouched + // instead of being fed to ClipperLib with the whole layer again for every candidate. + const auto split_near = [](const Polygons &polys, const BoundingBox &bbox, Polygons &far) { + Polygons near; + for (const Polygon &p : polys) + (get_extents(p).overlap(bbox) ? near : far).emplace_back(p); + return near; + }; for (const CandidateSurface &candidate : surfaces_by_layer[lidx]) { const auto ®ion_config = candidate.region->region().config(); const bool turning_pattern = region_config.sparse_infill_pattern == ipHilbertCurve || region_config.sparse_infill_pattern == ipOctagramSpiral; const Flow &flow = candidate.region->bridging_flow(frSolidInfill, true); Polygons area_to_be_bridge = expand(candidate.new_polys, flow.scaled_spacing()); - area_to_be_bridge = intersection(area_to_be_bridge, deep_infill_area); + // deep_infill_area and internal_unsupported_area cover the whole layer; only their part under + // this candidate can change the results, so they are clipped to its box first. + if (!area_to_be_bridge.empty()) + area_to_be_bridge = intersection(area_to_be_bridge, + ClipperUtils::clip_clipper_polygons_with_subject_bbox( + deep_infill_area, get_extents(area_to_be_bridge).inflated(SCALED_EPSILON))); area_to_be_bridge.erase(std::remove_if(area_to_be_bridge.begin(), area_to_be_bridge.end(), - [internal_unsupported_area](const Polygon &p) { - return intersection({p}, internal_unsupported_area).empty(); + [&internal_unsupported_area](const Polygon &p) { + return intersection({p}, ClipperUtils::clip_clipper_polygons_with_subject_bbox( + internal_unsupported_area, + get_extents(p).inflated(SCALED_EPSILON))) + .empty(); }), area_to_be_bridge.end()); - Polygons limiting_area = union_(area_to_be_bridge, expansion_area); - if (area_to_be_bridge.empty()) continue; - Polylines boundary_plines = to_polylines(expand(total_fill_area, 1.3 * flow.scaled_spacing())); + Polygons limiting_area; + const Polygons near_expansion = split_near(expansion_area, get_extents(area_to_be_bridge).inflated(SCALED_EPSILON), + limiting_area); + append(limiting_area, union_(area_to_be_bridge, near_expansion)); + + auto boundary_it = boundary_by_spacing.find(flow.scaled_spacing()); + if (boundary_it == boundary_by_spacing.end()) + boundary_it = boundary_by_spacing + .emplace(flow.scaled_spacing(), to_polylines(expand(total_fill_area, 1.3 * flow.scaled_spacing()))) + .first; + Polylines boundary_plines = boundary_it->second; { - Polylines limiting_plines = to_polylines(expand(limiting_area, 0.3*flow.spacing())); + // No offset here: flow.spacing() is in mm, so the expand(limiting_area, 0.3 * flow.spacing()) + // this used to be moved the outline by 0.135 scaled units - nothing beyond rounding - while + // costing a whole-layer ClipperLib pass for every candidate. limiting_area is already a clean + // union, so its own outline is the same boundary. + Polylines limiting_plines = to_polylines(limiting_area); boundary_plines.insert(boundary_plines.end(), limiting_plines.begin(), limiting_plines.end()); } @@ -3498,9 +3560,12 @@ void PrintObject::bridge_over_infill() // Check collision with other expanded surfaces { bool reconstruct = false; - Polygons tmp_expanded_area = expand(bridging_area, 3.0 * flow.scaled_spacing()); + Polygons tmp_expanded_area = expand(bridging_area, 3.0 * flow.scaled_spacing()); + const BoundingBox tmp_expanded_bbox = get_extents(tmp_expanded_area); for (const CandidateSurface &s : expanded_surfaces) { - if (!intersection(s.new_polys, tmp_expanded_area).empty()) { + // Surfaces whose boxes miss each other cannot intersect, which is most pairs on a busy layer. + if (get_extents(s.new_polys).overlap(tmp_expanded_bbox) && + !intersection(s.new_polys, tmp_expanded_area).empty()) { bridging_angle = s.bridge_angle; reconstruct = true; break; @@ -3524,10 +3589,20 @@ void PrintObject::bridge_over_infill() bridging_area = union_(bridging_area, construct_anchored_polygon(bridging_area, to_lines(boundary_plines), flow, bridging_angle, scan_spacing, true)); } - bridging_area = intersection(bridging_area, limiting_area); - bridging_area = intersection(bridging_area, total_fill_area); - bridging_area = diff(bridging_area, total_top_area); - expansion_area = diff(expansion_area, bridging_area); + // Each of these meets one bridge with the whole layer, so the layer side is first cut down to the + // bridge's box (and expansion_area split as above); the result is the same. + if (!bridging_area.empty()) { + const BoundingBox bridging_bbox = get_extents(bridging_area).inflated(SCALED_EPSILON); + bridging_area = intersection(bridging_area, ClipperUtils::clip_clipper_polygons_with_subject_bbox(limiting_area, bridging_bbox)); + bridging_area = intersection(bridging_area, ClipperUtils::clip_clipper_polygons_with_subject_bbox(total_fill_area, bridging_bbox)); + bridging_area = diff(bridging_area, ClipperUtils::clip_clipper_polygons_with_subject_bbox(total_top_area, bridging_bbox)); + } + if (!bridging_area.empty()) { + Polygons kept; + const Polygons cut = split_near(expansion_area, get_extents(bridging_area).inflated(SCALED_EPSILON), kept); + append(kept, diff(cut, bridging_area)); + expansion_area = std::move(kept); + } #ifdef DEBUG_BRIDGE_OVER_INFILL debug_draw(std::to_string(lidx) + "_" + std::to_string(cluster_idx) + "_" + std::to_string(job_idx) + "_" + "_expanded_bridging" + std::to_string(r), diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp index 34b47c1825..604c3786f8 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp @@ -5576,14 +5576,15 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float m_erase_mode = true; } - // ---- Tools: brush / face / connected area on the left, the whole-model actions on the right, and the - // active tool's own control on the line below ---- + // ---- Tools: brush / face / connected area, then the whole-model actions, then the active tool's own + // control filling the rest of the row ---- // "Face" and "Connected area" reuse the exact same selection machinery every other paint gizmo has // (single-facet click, and angle-limited flood fill respectively). { const bool is_brush_mode = m_tool_type == ToolType::BRUSH && m_cursor_type != TriangleSelector::CursorType::POINTER; const bool is_face_mode = m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER; const bool is_area_mode = m_tool_type == ToolType::SMART_FILL; + const float row_y = ImGui::GetCursorPosY(); if (icon_toggle(801, "texture_displacement_brush.svg", is_brush_mode, icon_md, _L("Brush"), _L("Brush - paint over the surface by dragging"))) { m_tool_type = ToolType::BRUSH; @@ -5603,16 +5604,14 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float m_cursor_type = TriangleSelector::CursorType::POINTER; } - // Whole model: paint every face with the active layer, or clear its paint from all of them. Actions - // rather than tools, so they sit apart at the right end of the row. + // Whole model: paint every face with the active layer, or clear its paint from all of them. const wxString whole_na = busy ? _L("Wait for the bake to finish.") : active == nullptr ? _L("Add a layer first.") : wxString(); const wxString erase_na = !whole_na.empty() ? whole_na : !slot_painted(m_active_layer_slot) ? _L("The active layer has no paint yet.") : wxString(); - ImGui::SameLine(); - ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), ImGui::GetWindowContentRegionMax().x - (2.f * icon_md + gap_s))); + ImGui::SameLine(0.f, gap_s); if (icon_toggle(806, "texture_displacement_select_all.svg", false, icon_md, _L("Select whole model"), _L("Select whole model - paint every face of the model with the active layer"), whole_na)) select_whole_model(); @@ -5632,28 +5631,38 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float m_parent.set_as_dirty(); } + // The active tool's control fills the rest of the row, each part centred on the (taller) tool icons. + const float row_end = ImGui::GetWindowContentRegionMax().x; + const auto centre_on_row = [&](float h) { ImGui::SetCursorPosY(row_y + std::round((icon_md - h) * 0.5f)); }; + vsep(icon_md); if (is_brush_mode) { - ImGui::SetNextItemWidth(-(3.f * gap_s + 1.f + 2.f * icon_sm)); + ImGui::SetNextItemWidth(std::max(1.f, row_end - ImGui::GetCursorPosX() - (3.f * gap_s + 1.f + 2.f * icon_sm))); + centre_on_row(frame_h); ImGui::SliderFloat("##cursor_radius", &m_cursor_radius, CursorRadiusMin, CursorRadiusMax, "%.2f mm", ImGuiSliderFlags_AlwaysClamp); hover_tip(m_desc.at("cursor_size")); vsep(icon_sm); const bool is_circle = m_cursor_type == TriangleSelector::CursorType::CIRCLE; + centre_on_row(icon_sm); if (icon_toggle(804, "circle_paint.svg", is_circle, icon_sm, m_desc.at("circle"), _L("Circle - paints everything under the brush as seen from the camera"))) m_cursor_type = TriangleSelector::CursorType::CIRCLE; ImGui::SameLine(0.f, gap_s); + centre_on_row(icon_sm); if (icon_toggle(805, "menu_obj_sphere.svg", !is_circle, icon_sm, m_desc.at("sphere"), _L("Sphere - paints only within a ball around the point under the cursor"))) m_cursor_type = TriangleSelector::CursorType::SPHERE; } else if (is_area_mode) { - ImGui::SetNextItemWidth(-1.f); + ImGui::SetNextItemWidth(std::max(1.f, row_end - ImGui::GetCursorPosX())); + centre_on_row(frame_h); ImGui::SliderFloat("##smart_fill_angle", &m_smart_fill_angle, SmartFillAngleMin, SmartFillAngleMax, "%.0f°", ImGuiSliderFlags_AlwaysClamp); hover_tip(_u8L("Angle threshold - the fill stops at edges sharper than this")); } else { + centre_on_row(frame_h); ImGui::AlignTextToFramePadding(); - ImGui::TextDisabled("%s", _u8L("Click a triangle to paint it").c_str()); + ImGui::TextDisabled("%s", ellipsize(_u8L("Click a triangle to paint it"), + std::max(0.f, row_end - ImGui::GetCursorPosX())).c_str()); } } diff --git a/src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.cpp b/src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.cpp index f48ec6b28c..6e3976a297 100644 --- a/src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.cpp +++ b/src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.cpp @@ -6,6 +6,7 @@ #include "libslic3r/TriangleSelector.hpp" #include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI_ObjectList.hpp" #include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"