From 1f94559cc7822003eff79d4a8df334eb613bd9a0 Mon Sep 17 00:00:00 2001 From: PaddleStroke Date: Mon, 5 Jan 2026 18:22:10 +0100 Subject: [PATCH] Sketcher: Fix selection & zoom lag in large sketches (#26671) * Sketcher: Fix selection & zoom lag in large sketches * Update src/Mod/Sketcher/Gui/EditModeConstraintCoinManager.cpp Co-authored-by: Kacper Donat --------- Co-authored-by: Kacper Donat --- .../Gui/EditModeConstraintCoinManager.cpp | 123 ++++++++++++------ 1 file changed, 83 insertions(+), 40 deletions(-) diff --git a/src/Mod/Sketcher/Gui/EditModeConstraintCoinManager.cpp b/src/Mod/Sketcher/Gui/EditModeConstraintCoinManager.cpp index 8c907b3299..c09e53ed87 100644 --- a/src/Mod/Sketcher/Gui/EditModeConstraintCoinManager.cpp +++ b/src/Mod/Sketcher/Gui/EditModeConstraintCoinManager.cpp @@ -28,6 +28,9 @@ #include #include #include +#include +#include +#include #include #include @@ -2492,64 +2495,104 @@ void EditModeConstraintCoinManager::drawConstraintIcons(const GeoListFacade& geo void EditModeConstraintCoinManager::combineConstraintIcons(IconQueue iconQueue) { // getScaleFactor gives us a ratio of pixels per some kind of real units - float maxDistSquared = pow(ViewProviderSketchCoinAttorney::getScaleFactor(viewProvider), 2); + float scale = ViewProviderSketchCoinAttorney::getScaleFactor(viewProvider); + float maxDistSquared = pow(scale, 2); // There's room for optimisation here; we could reuse the combined icons... combinedConstrBoxes.clear(); - while (!iconQueue.empty()) { - // A group starts with an item popped off the back of our initial queue + // Grid size needs to be slightly larger than the max merge distance to ensure + // we catch neighbors. + float gridSize = std::max(1.0f, std::abs(scale) * 1.1f); + + // 2. FILTERING & PREPARATION + // We create a list of valid indices. + std::vector validIndices; + validIndices.reserve(iconQueue.size()); + + for (size_t i = 0; i < iconQueue.size(); ++i) { + if (!iconQueue[i].visible) { + clearCoinImage(iconQueue[i].destination); + continue; + } + + // Symmetric constraints render alone (original logic) + if (iconQueue[i].type == QStringLiteral("Constraint_Symmetric")) { + drawTypicalConstraintIcon(iconQueue[i]); + continue; + } + + validIndices.push_back(static_cast(i)); + } + + // 3. SPATIAL HASHING (The Grid) + // Map: Pair(GridX, GridY) -> List of indices in iconQueue + std::map, std::vector> grid; + + for (int idx : validIndices) { + const auto& icon = iconQueue[idx]; + int gx = static_cast(std::floor(icon.position[0] / gridSize)); + int gy = static_cast(std::floor(icon.position[1] / gridSize)); + grid[{gx, gy}].push_back(idx); + } + + // 4. CLUSTERING (Reversed Iteration) + std::vector processed(iconQueue.size(), false); + + for (auto it = validIndices.rbegin(); it != validIndices.rend(); ++it) { + int startIdx = *it; + if (processed[startIdx]) { + continue; + } + + // Start a new group with the "Anchor" IconQueue thisGroup; - thisGroup.push_back(iconQueue.back()); - constrIconQueueItem init = iconQueue.back(); - iconQueue.pop_back(); + thisGroup.reserve(5); - // we group only icons not being Symmetry icons, because we want those on the line - // and only icons that are visible - if (init.type != QStringLiteral("Constraint_Symmetric") && init.visible) { + // Stack for recursive search (Chain reaction) + std::stack searchStack; + searchStack.push(startIdx); + processed[startIdx] = true; - IconQueue::iterator i = iconQueue.begin(); + while (!searchStack.empty()) { + int currentIdx = searchStack.top(); + searchStack.pop(); + thisGroup.push_back(iconQueue[currentIdx]); + // Check neighbors of current icon + const auto& currentIcon = iconQueue[currentIdx]; + int cx = static_cast(std::floor(currentIcon.position[0] / gridSize)); + int cy = static_cast(std::floor(currentIcon.position[1] / gridSize)); - while (i != iconQueue.end()) { - if ((*i).visible) { - bool addedToGroup = false; - - for (IconQueue::iterator j = thisGroup.begin(); j != thisGroup.end(); ++j) { - float distSquared = pow(i->position[0] - j->position[0], 2) - + pow(i->position[1] - j->position[1], 2); - if (distSquared <= maxDistSquared - && (*i).type != QStringLiteral("Constraint_Symmetric")) { - // Found an icon in iconQueue that's close enough to - // a member of thisGroup, so move it into thisGroup - thisGroup.push_back(*i); - i = iconQueue.erase(i); - addedToGroup = true; - break; - } + // Check 3x3 grid neighborhood + for (int dx = -1; dx <= 1; ++dx) { + for (int dy = -1; dy <= 1; ++dy) { + auto gridIt = grid.find({cx + dx, cy + dy}); + if (gridIt == grid.end()) { + continue; } - if (addedToGroup) { - if (i == iconQueue.end()) { - // We just got the last icon out of iconQueue - break; + // Iterate over potential matches in this cell + for (int neighborIdx : gridIt->second) { + if (processed[neighborIdx]) { + continue; } - else { - // Start looking through the iconQueue again, in case - // we have an icon that's now close enough to thisGroup - i = iconQueue.begin(); + + const auto& otherIcon = iconQueue[neighborIdx]; + + float distSq = pow(currentIcon.position[0] - otherIcon.position[0], 2) + + pow(currentIcon.position[1] - otherIcon.position[1], 2); + + if (distSq <= maxDistSquared) { + processed[neighborIdx] = true; + searchStack.push(neighborIdx); } } - else { - ++i; - } - } - else { // if !visible we skip it - i++; } } } + // 5. DRAW if (thisGroup.size() == 1) { drawTypicalConstraintIcon(thisGroup[0]); }