Sketcher: Speed up large bulk Selection in edit (#26663)

* Sketcher: Speed up large bulk Selection in edit

* Update ViewProviderSketch.cpp

* Update src/Mod/Sketcher/Gui/TaskSketcherConstraints.cpp

---------

Co-authored-by: Kacper Donat <kadet1090@gmail.com>
This commit is contained in:
PaddleStroke
2026-01-05 18:41:45 +01:00
committed by GitHub
parent f64408de2e
commit 0fa707c523
7 changed files with 295 additions and 196 deletions

View File

@@ -34,6 +34,7 @@
#include <Inventor/SbColor.h>
#include <App/Datums.h>
#include <App/Document.h>
#include <Gui/ViewParams.h>
#include "ViewProviderPlane.h"
@@ -158,13 +159,36 @@ void ViewProviderPlane::setLabelVisibility(bool val)
labelSwitch->whichChild = val ? SO_SWITCH_ALL : SO_SWITCH_NONE;
}
void ViewProviderPlane::onSelectionChanged(const SelectionChanges&)
void ViewProviderPlane::onSelectionChanged(const SelectionChanges& msg)
{
isSelected = Gui::Selection().isSelected(getObject());
isHovered = Gui::Selection().getPreselection().Object.getSubObject() == getObject()
|| Gui::Selection().getPreselection().Object.getObject() == getObject();
bool isSelectedOrHoveredBefore = isSelected || isHovered;
updatePlaneSize();
if (msg.Type == Gui::SelectionChanges::ClrSelection) {
isSelected = false;
}
auto obj = getObject();
if (!obj) {
return;
}
auto doc = obj->getDocument();
if (!doc) {
return;
}
const char* nameInDoc = getObject()->getNameInDocument();
if (nameInDoc && strcmp(msg.pDocName, doc->getName()) == 0
&& strcmp(msg.pObjectName, nameInDoc) == 0) {
isSelected = Gui::Selection().isSelected(getObject());
isHovered = Gui::Selection().getPreselection().Object.getSubObject() == getObject()
|| Gui::Selection().getPreselection().Object.getObject() == getObject();
}
bool isSelectedOrHovered = isSelected || isHovered;
if (isSelectedOrHoveredBefore != isSelectedOrHovered) {
updatePlaneSize();
}
}
void ViewProviderPlane::updatePlaneSize()

View File

@@ -29,10 +29,12 @@
#include <QRegularExpression>
#include <QRegularExpressionMatch>
#include <QString>
#include <QTimer>
#include <QStyledItemDelegate>
#include <QWidgetAction>
#include <boost/core/ignore_unused.hpp>
#include <cmath>
#include <cstring>
#include <limits>
#include <App/Application.h>
@@ -1335,39 +1337,23 @@ void TaskSketcherConstraints::onSelectionChanged(const Gui::SelectionChanges& ms
return;
}
QRegularExpression rx(QStringLiteral("^Constraint(\\d+)$"));
QRegularExpressionMatch match;
QString expr = QString::fromLatin1(msg.pSubName);
boost::ignore_unused(expr.indexOf(rx, 0, &match));
if (match.hasMatch()) {// is a constraint
bool ok;
int ConstrId = match.captured(1).toInt(&ok) - 1;
if (ok) {
int countItems = ui->listWidgetConstraints->count();
for (int i = 0; i < countItems; i++) {
ConstraintItem* item =
static_cast<ConstraintItem*>(ui->listWidgetConstraints->item(i));
if (item->ConstraintNbr == ConstrId) {
auto tmpBlock = ui->listWidgetConstraints->blockSignals(true);
item->setSelected(select);
ui->listWidgetConstraints->blockSignals(tmpBlock);
SketcherGui::scrollTo(ui->listWidgetConstraints, i, select);
break;
}
}
if (std::strncmp(msg.pSubName, "Constraint", 10) == 0) {
int id = std::atoi(msg.pSubName + 10) - 1;
if (specialFilterMode == SpecialFilterType::Selected) {
updateSelectionFilter();
bool block =
this->blockSelection(true);// avoid to be notified by itself
updateList();
this->blockSelection(block);
auto it = constraintMap.find(id);
if (it != constraintMap.end()) {
selectionBuffer.push_back({it->second, select});
if (!selectionUpdateTimerPending) {
selectionUpdateTimerPending = true;
QTimer::singleShot(0, this, &TaskSketcherConstraints::processSelectionBuffer);
}
}
}
else if (ui->filterBox->checkState() == Qt::Checked && specialFilterMode == SpecialFilterType::Associated) {
int geoid = Sketcher::GeoEnum::GeoUndef;
Sketcher::PointPos pointpos = Sketcher::PointPos::none;
QString expr = QString::fromLatin1(msg.pSubName);
getSelectionGeoId(expr, geoid, pointpos);
if (geoid != Sketcher::GeoEnum::GeoUndef
@@ -1384,6 +1370,43 @@ void TaskSketcherConstraints::onSelectionChanged(const Gui::SelectionChanges& ms
}
}
void TaskSketcherConstraints::processSelectionBuffer()
{
selectionUpdateTimerPending = false;
if (selectionBuffer.empty()) {
return;
}
QSignalBlocker block(ui->listWidgetConstraints);
QItemSelection selectionObj;
for (const auto& update : selectionBuffer) {
// NOTE: We trust the buffer has valid pointers (lifetime matches widget)
if (update.select) {
QModelIndex idx = ui->listWidgetConstraints->model()->index(ui->listWidgetConstraints->row(update.item), 0);
selectionObj.select(idx, idx);
} else {
update.item->setSelected(false);
}
}
ui->listWidgetConstraints->selectionModel()->select(selectionObj, QItemSelectionModel::Select);
// Scroll only if single item selected
if (selectionBuffer.size() == 1 && selectionBuffer[0].select) {
SketcherGui::scrollTo(ui->listWidgetConstraints, ui->listWidgetConstraints->row(selectionBuffer[0].item), true);
}
if (specialFilterMode == SpecialFilterType::Selected) {
updateSelectionFilter();
// avoid to be notified by itself
bool block = this->blockSelection(true);
updateList();
this->blockSelection(block);
}
selectionBuffer.clear();
}
void TaskSketcherConstraints::deferredUpdateList()
{
updateAssociatedConstraintsFilter();
@@ -1683,20 +1706,13 @@ bool TaskSketcherConstraints::isConstraintFiltered(QListWidgetItem* item)
void TaskSketcherConstraints::slotConstraintsChanged()
{
assert(sketchView);
constraintMap.clear();
// Build up ListView with the constraints
const Sketcher::SketchObject* sketch = sketchView->getSketchObject();
const std::vector<Sketcher::Constraint*>& vals = sketch->Constraints.getValues();
/* Update constraint number and virtual space check status */
for (int i = 0; i < ui->listWidgetConstraints->count(); ++i) {
ConstraintItem* it = dynamic_cast<ConstraintItem*>(ui->listWidgetConstraints->item(i));
assert(it);
it->ConstraintNbr = i;
it->value = QVariant();
}
/* Remove entries, if any */
for (std::size_t i = ui->listWidgetConstraints->count(); i > vals.size(); --i)
delete ui->listWidgetConstraints->takeItem(i - 1);
@@ -1708,7 +1724,10 @@ void TaskSketcherConstraints::slotConstraintsChanged()
/* Update the states */
auto tmpBlock = ui->listWidgetConstraints->blockSignals(true);
for (int i = 0; i < ui->listWidgetConstraints->count(); ++i) {
ConstraintItem* it = static_cast<ConstraintItem*>(ui->listWidgetConstraints->item(i));
auto* it = static_cast<ConstraintItem*>(ui->listWidgetConstraints->item(i));
it->ConstraintNbr = i;
it->value = QVariant();
constraintMap[it->ConstraintNbr] = it;
it->updateVirtualSpaceStatus();
}
ui->listWidgetConstraints->blockSignals(tmpBlock);

View File

@@ -34,6 +34,7 @@
#include "ConstraintFilters.h"
class ConstraintItem;
namespace App
{
@@ -221,6 +222,19 @@ private:
// selected geometry
ConstraintFilterList* filterList;
boost::signals2::scoped_connection changedSketchView;
// Buffering structures
std::unordered_map<int, ConstraintItem*> constraintMap;
struct PendingSelectionUpdate
{
ConstraintItem* item;
bool select;
};
std::vector<PendingSelectionUpdate> selectionBuffer;
bool selectionUpdateTimerPending = false;
void processSelectionBuffer();
};
} // namespace SketcherGui

View File

@@ -32,9 +32,11 @@
#include <QRegularExpressionMatch>
#include <QShortcut>
#include <QString>
#include <QTimer>
#include <QWidgetAction>
#include <boost/core/ignore_unused.hpp>
#include <limits>
#include <cstring>
#include <fmt/format.h>
@@ -1480,150 +1482,121 @@ void TaskSketcherElements::updateVisibility()
}
}
/*------------------*/
// clang-format on
void TaskSketcherElements::onSelectionChanged(const Gui::SelectionChanges& msg)
{
// update the listwidget
auto updateListWidget = [this](auto& modified_item) {
QSignalBlocker sigblk(this->ui->listWidgetElements);
if (modified_item == nullptr) {
return;
}
bool is_selected = modified_item->isSelected();
const bool should_be_selected = modified_item->isLineSelected
|| modified_item->isStartingPointSelected || modified_item->isEndPointSelected
|| modified_item->isMidPointSelected;
if (msg.Type == Gui::SelectionChanges::ClrSelection) {
clearWidget();
return;
}
// If an element is already selected and a new subelement gets selected
// (eg., if you select the arc of a circle then select the center as
// well), the new subelement won't get highlighted in the list until you
// mouseover the list. To avoid this, we deselect first to trigger a
// redraw.
if (should_be_selected && is_selected) {
modified_item->setSelected(false);
is_selected = false;
}
if (msg.Type != Gui::SelectionChanges::AddSelection
&& msg.Type != Gui::SelectionChanges::RmvSelection) {
return;
}
if (should_be_selected != is_selected) {
modified_item->setSelected(should_be_selected);
}
};
// is it this object??
if (strcmp(msg.pDocName, sketchView->getSketchObject()->getDocument()->getName()) != 0
|| strcmp(msg.pObjectName, sketchView->getSketchObject()->getNameInDocument()) != 0
|| !msg.pSubName) {
return;
}
switch (msg.Type) {
case Gui::SelectionChanges::ClrSelection: {
clearWidget();
return;
}
case Gui::SelectionChanges::AddSelection:
case Gui::SelectionChanges::RmvSelection: {
bool select = (msg.Type == Gui::SelectionChanges::AddSelection);
// is it this object??
if (strcmp(msg.pDocName, sketchView->getSketchObject()->getDocument()->getName()) != 0
|| strcmp(msg.pObjectName, sketchView->getSketchObject()->getNameInDocument()) != 0) {
return;
}
if (!msg.pSubName) {
return;
}
ElementItem* modified_item = nullptr;
QString expr = QString::fromLatin1(msg.pSubName);
std::string shapetype(msg.pSubName);
// if-else edge vertex
if (shapetype.starts_with("Edge")) {
QRegularExpression rx(QStringLiteral("^Edge(\\d+)$"));
QRegularExpressionMatch match;
boost::ignore_unused(expr.indexOf(rx, 0, &match));
if (!match.hasMatch()) {
return;
}
bool ok;
int ElementId = match.captured(1).toInt(&ok) - 1;
if (!ok) {
return;
}
int countItems = ui->listWidgetElements->count();
// TODO: This and the loop below get slow when we have a lot of items.
// Perhaps we should also maintain a map so that we can look up items
// by element number.
for (int i = 0; i < countItems; i++) {
auto* item = static_cast<ElementItem*>(ui->listWidgetElements->item(i));
if (item->ElementNbr == ElementId) {
item->isLineSelected = select;
modified_item = item;
SketcherGui::scrollTo(ui->listWidgetElements, i, select);
break;
}
}
}
else if (shapetype.starts_with("ExternalEdge")) {
QRegularExpression rx(QStringLiteral("^ExternalEdge(\\d+)$"));
QRegularExpressionMatch match;
boost::ignore_unused(expr.indexOf(rx, 0, &match));
if (!match.hasMatch()) {
return;
}
bool ok;
int ElementId = -match.captured(1).toInt(&ok) - 2;
if (!ok) {
return;
}
int countItems = ui->listWidgetElements->count();
for (int i = 0; i < countItems; i++) {
auto* item = static_cast<ElementItem*>(ui->listWidgetElements->item(i));
if (item->ElementNbr == ElementId) {
item->isLineSelected = select;
modified_item = item;
break;
}
}
}
else if (shapetype.starts_with("Vertex")) {
QRegularExpression rx(QStringLiteral("^Vertex(\\d+)$"));
QRegularExpressionMatch match;
boost::ignore_unused(expr.indexOf(rx, 0, &match));
if (!match.hasMatch()) {
return;
}
bool ok;
int ElementId = match.captured(1).toInt(&ok) - 1;
if (!ok) {
return;
}
// Get the GeoID&Pos
int GeoId;
Sketcher::PointPos PosId;
sketchView->getSketchObject()->getGeoVertexIndex(ElementId, GeoId, PosId);
int countItems = ui->listWidgetElements->count();
for (int i = 0; i < countItems; i++) {
auto* item = static_cast<ElementItem*>(ui->listWidgetElements->item(i));
if (item->ElementNbr == GeoId) {
modified_item = item;
switch (PosId) {
case Sketcher::PointPos::start:
item->isStartingPointSelected = select;
break;
case Sketcher::PointPos::end:
item->isEndPointSelected = select;
break;
case Sketcher::PointPos::mid:
item->isMidPointSelected = select;
break;
default:
break;
}
break;
}
}
}
updateListWidget(modified_item);
bool select = (msg.Type == Gui::SelectionChanges::AddSelection);
const char* subName = msg.pSubName;
ElementItem* modified_item = nullptr;
// Format: "Edge123" -> offset 4
if (std::strncmp(subName, "Edge", 4) == 0) {
int id = std::atoi(subName + 4) - 1;
auto it = elementMap.find(id);
if (it != elementMap.end()) {
modified_item = it->second;
modified_item->isLineSelected = select;
}
}
// Format: "ExternalEdge123" -> offset 12
else if (std::strncmp(subName, "ExternalEdge", 12) == 0) {
int id = -std::atoi(subName + 12) - 2;
auto it = elementMap.find(id);
if (it != elementMap.end()) {
modified_item = it->second;
modified_item->isLineSelected = select;
}
}
// Format: "Vertex123" -> offset 6
else if (std::strncmp(subName, "Vertex", 6) == 0) {
int vtId = std::atoi(subName + 6) - 1;
int GeoId;
Sketcher::PointPos PosId;
sketchView->getSketchObject()->getGeoVertexIndex(vtId, GeoId, PosId);
auto it = elementMap.find(GeoId);
if (it != elementMap.end()) {
modified_item = it->second;
switch (PosId) {
case Sketcher::PointPos::start:
modified_item->isStartingPointSelected = select;
break;
case Sketcher::PointPos::end:
modified_item->isEndPointSelected = select;
break;
case Sketcher::PointPos::mid:
modified_item->isMidPointSelected = select;
break;
default:
break;
}
}
}
// 3. Queue the UI update instead of applying immediately
if (modified_item) {
selectionBuffer.push_back({modified_item, select});
if (!updateTimerPending) {
updateTimerPending = true;
// Schedule processing for the next event loop cycle (0ms)
QTimer::singleShot(0, this, &TaskSketcherElements::processSelectionBuffer);
}
default:
return;
}
}
// clang-format off
void TaskSketcherElements::processSelectionBuffer()
{
updateTimerPending = false;
if (selectionBuffer.empty()) {
return;
}
QSignalBlocker sigblk(ui->listWidgetElements);
QItemSelection selectionObj;
for (const auto& update : selectionBuffer) {
bool should_be = update.item->isLineSelected ||
update.item->isStartingPointSelected ||
update.item->isEndPointSelected ||
update.item->isMidPointSelected;
if (should_be) {
QModelIndex idx = ui->listWidgetElements->model()->index(ui->listWidgetElements->row(update.item), 0);
selectionObj.select(idx, idx);
}
else {
update.item->setSelected(false);
}
}
// Apply all selections in ONE go
ui->listWidgetElements->selectionModel()->select(selectionObj, QItemSelectionModel::Select);
// Only scroll if a single item was clicked/selected
if (selectionBuffer.size() == 1 && selectionBuffer[0].select) {
SketcherGui::scrollTo(ui->listWidgetElements, ui->listWidgetElements->row(selectionBuffer[0].item), true);
}
selectionBuffer.clear();
}
void TaskSketcherElements::onListWidgetElementsItemPressed(QListWidgetItem* it)
{
@@ -1901,6 +1874,7 @@ void TaskSketcherElements::slotElementsChanged()
const std::vector<Part::Geometry*>& vals = sketch->Geometry.getValues();
ui->listWidgetElements->clear();
elementMap.clear();
using GeometryState = ElementItem::GeometryState;
@@ -2008,6 +1982,8 @@ void TaskSketcherElements::slotElementsChanged()
ui->listWidgetElements->addItem(itemN);
elementMap[itemN->ElementNbr] = itemN;
setItemVisibility(itemN);
}
@@ -2112,6 +2088,8 @@ void TaskSketcherElements::slotElementsChanged()
ui->listWidgetElements->addItem(itemN);
elementMap[itemN->ElementNbr] = itemN;
setItemVisibility(itemN);
}
}

View File

@@ -25,6 +25,7 @@
#ifndef GUI_TASKVIEW_TaskSketcherElements_H
#define GUI_TASKVIEW_TaskSketcherElements_H
#include <unordered_map>
#include <QListWidget>
#include <QStyledItemDelegate>
@@ -165,6 +166,19 @@ private:
ElementFilterList* filterList;
bool isNamingBoxChecked;
// Buffering to speed up large selections
std::unordered_map<int, ElementItem*> elementMap;
struct PendingUpdate
{
ElementItem* item;
bool select;
};
std::vector<PendingUpdate> selectionBuffer;
bool updateTimerPending = false;
void processSelectionBuffer();
};
} // namespace SketcherGui

View File

@@ -2296,7 +2296,9 @@ void ViewProviderSketch::onSelectionChanged(const Gui::SelectionChanges& msg)
int ConstrId =
Sketcher::PropertyConstraintList::getIndexFromConstraintName(shapetype);
selection.SelConstraintSet.insert(ConstrId);
editCoinManager->drawConstraintIcons();
if (!selection.selectionBuffering) {
editCoinManager->drawConstraintIcons();
}
}
updateColor();
}
@@ -2594,13 +2596,21 @@ void ViewProviderSketch::doBoxSelection(const SbVec2s& startPos, const SbVec2s&
if (corners[0].getValue()[0] > corners[1].getValue()[0])
touchMode = true;
auto selectVertex = [this](int vertexid) {
std::stringstream ss;
ss << "Vertex" << vertexid;
addSelection2(ss.str());
std::vector<std::string> batchSelection;
batchSelection.reserve(geomlist.size());
auto addConvertedName = [this, sketchObject, &batchSelection](const std::string& suffix) {
std::string finalName = editSubName + sketchObject->convertSubName(suffix);
batchSelection.push_back(finalName);
};
auto selectEdge = [this](int edgeid) {
auto selectVertex = [&addConvertedName](int vertexid) {
std::stringstream ss;
ss << "Vertex" << vertexid;
addConvertedName(ss.str());
};
auto selectEdge = [&addConvertedName](int edgeid) {
std::stringstream ss;
if (edgeid >= 0) {
ss << "Edge" << edgeid;
@@ -2608,7 +2618,7 @@ void ViewProviderSketch::doBoxSelection(const SbVec2s& startPos, const SbVec2s&
else {
ss << "ExternalEdge" << -edgeid - 1;
}
addSelection2(ss.str());
addConvertedName(ss.str());
};
auto selectVertexIfInsideBox = [&polygon, &VertexId, &selectVertex](const Base::Vector3d & point) {
@@ -2651,6 +2661,8 @@ void ViewProviderSketch::doBoxSelection(const SbVec2s& startPos, const SbVec2s&
}
};
selection.selectionBuffering = true;
for (std::vector<Part::Geometry*>::const_iterator it = geomlist.begin();
it != geomlist.end() - 2;
++it, ++GeoId) {
@@ -2753,17 +2765,29 @@ void ViewProviderSketch::doBoxSelection(const SbVec2s& startPos, const SbVec2s&
Base::Vector3d pnt0 = proj(Plm.getPosition());
if (polygon.Contains(Base::Vector2d(pnt0.x, pnt0.y))) {
std::stringstream ss;
ss << "RootPoint";
addSelection2(ss.str());
addConvertedName("RootPoint");
}
if (!batchSelection.empty()) {
Gui::Selection().addSelections(
getSketchObject()->getDocument()->getName(),
getSketchObject()->getNameInDocument(),
batchSelection
);
}
selection.selectionBuffering = false;
editCoinManager->drawConstraintIcons();
updateColor();
}
void ViewProviderSketch::updateColor()
{
assert(isInEditMode());
editCoinManager->updateColor();
if (!selection.selectionBuffering) {
editCoinManager->updateColor();
}
}
bool ViewProviderSketch::selectAll()
@@ -2809,6 +2833,18 @@ bool ViewProviderSketch::selectAll()
Gui::Selection().clearSelection();
std::vector<std::string> batchSelection;
// Heuristic reservation: Geometry count * 3 (start/end/edge) + Constraint count
batchSelection.reserve(
sketchObject->Geometry.getValues().size() * 3 + sketchObject->Constraints.getSize()
);
auto addConvertedName = [this, sketchObject, &batchSelection](const std::string& suffix) {
std::string finalName = editSubName + sketchObject->convertSubName(suffix);
batchSelection.push_back(finalName);
};
selection.selectionBuffering = true;
if (focusOnElementWidget || noWidgetSelected) {
int intGeoCount = sketchObject->getHighestCurveIndex() + 1;
int extGeoCount = sketchObject->getExternalGeometryCount();
@@ -2817,16 +2853,17 @@ bool ViewProviderSketch::selectAll()
int GeoId = 0;
auto selectVertex = [this](int geoId, Sketcher::PointPos pos) {
auto selectVertex = [this, &addConvertedName](int geoId, Sketcher::PointPos pos) {
int vertexId = this->getSketchObject()->getVertexIndexGeoPos(geoId, pos);
addSelection2(fmt::format("Vertex{}", vertexId + 1));
addConvertedName(fmt::format("Vertex{}", vertexId + 1));
};
auto selectEdge = [this](int GeoId) {
auto selectEdge = [&addConvertedName](int GeoId) {
if (GeoId >= 0) {
addSelection2(fmt::format("Edge{}", GeoId + 1));
} else {
addSelection2(fmt::format("ExternalEdge{}", GeoEnum::RefExt - GeoId + 1));
addConvertedName(fmt::format("Edge{}", GeoId + 1));
}
else {
addConvertedName(fmt::format("ExternalEdge{}", GeoEnum::RefExt - GeoId + 1));
}
};
@@ -2868,7 +2905,7 @@ bool ViewProviderSketch::selectAll()
}
if (!focusOnElementWidget) {
addSelection2("RootPoint");
addConvertedName("RootPoint");
}
if (hasUnselectedGeometry) {
@@ -2882,10 +2919,22 @@ bool ViewProviderSketch::selectAll()
if (focusedList && std::ranges::find(ids, i) == ids.end()) {
continue;
}
addSelection2(fmt::format("Constraint{}", i + 1));
addConvertedName(fmt::format("Constraint{}", i + 1));
}
}
if (!batchSelection.empty()) {
Gui::Selection().addSelections(
getSketchObject()->getDocument()->getName(),
getSketchObject()->getNameInDocument(),
batchSelection
);
}
selection.selectionBuffering = false;
editCoinManager->drawConstraintIcons();
updateColor();
return true;
}

View File

@@ -451,6 +451,7 @@ private:
std::set<int> SelPointSet; // Indices as PreselectPoint (and -1 for rootpoint)
std::set<int> SelCurvSet; // also holds cross axes at -1 and -2
std::set<int> SelConstraintSet; // ConstraintN, N = index + 1.
bool selectionBuffering {false};
};
//@}