feat/vtk-3d-view #7

Merged
gaozheng merged 301 commits from feat/vtk-3d-view into main 2026-06-27 18:43:52 +08:00
6 changed files with 214 additions and 10 deletions
Showing only changes of commit 1978a31fa7 - Show all commits

View File

@ -24,6 +24,7 @@
#include "Glyphs.hpp" #include "Glyphs.hpp"
#include "Theme.hpp" #include "Theme.hpp"
#include "dto/NavDto.hpp" #include "dto/NavDto.hpp"
#include "panels/ObjectTreeSelection.hpp"
namespace geopro::app { namespace geopro::app {
@ -33,6 +34,7 @@ constexpr int kRoleConfType = Qt::UserRole + 3; // 1=GS 2=TM
constexpr int kRoleTypeId = Qt::UserRole + 4; // 类型 id编辑调 getDynamicForm 用) constexpr int kRoleTypeId = Qt::UserRole + 4; // 类型 id编辑调 getDynamicForm 用)
constexpr int kRoleIsRoot = Qt::UserRole + 5; // 项目根标记(按 GS 处理,但仅 新建GS/TM/属性) constexpr int kRoleIsRoot = Qt::UserRole + 5; // 项目根标记(按 GS 处理,但仅 新建GS/TM/属性)
constexpr int kRoleChildCount = Qt::UserRole + 6; // §6.1 计数徽标GS 直接子节点数(>0 才显示) constexpr int kRoleChildCount = Qt::UserRole + 6; // §6.1 计数徽标GS 直接子节点数(>0 才显示)
constexpr int kRoleGsDsOn = Qt::UserRole + 7; // GS 自身直挂 ds 开关bool三态聚合用spec §6
constexpr int kConfTypeGs = 1; // GS工区 constexpr int kConfTypeGs = 1; // GS工区
constexpr int kConfTypeTm = 2; // TM 叶子 constexpr int kConfTypeTm = 2; // TM 叶子
@ -120,7 +122,9 @@ void addNodes(QTreeWidgetItem* parent, const std::vector<data::dto::StructTreeNo
item->setIcon(0, makeGlyph(Glyph::SurveyLine, iconColor, iconPx)); item->setIcon(0, makeGlyph(Glyph::SurveyLine, iconColor, iconPx));
} else { } else {
item->setData(0, kRoleConfType, kConfTypeGs); // GS item->setData(0, kRoleConfType, kConfTypeGs); // GS
item->setFlags(item->flags() | Qt::ItemIsUserCheckable | Qt::ItemIsAutoTristate); // 停用 Qt::ItemIsAutoTristateGS 三态由 recomputeGsState 手动聚合(自身 ds 开关 + 子 TM
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
item->setData(0, kRoleGsDsOn, false); // GS 自身直挂 ds 开关初始关
item->setCheckState(0, Qt::Unchecked); item->setCheckState(0, Qt::Unchecked);
item->setIcon(0, makeGlyph(Glyph::WorkArea, iconColor, iconPx)); item->setIcon(0, makeGlyph(Glyph::WorkArea, iconColor, iconPx));
} }
@ -158,6 +162,23 @@ ObjectTreePanel::ObjectTreePanel(QWidget* parent) : QWidget(parent) {
QObject::connect(tree_, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem* item, int) { QObject::connect(tree_, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem* item, int) {
if (pressOnCheckbox_) { // 点的是复选框:只切换勾选态,不当作「选中」(不重载数据集列表) if (pressOnCheckbox_) { // 点的是复选框:只切换勾选态,不当作「选中」(不重载数据集列表)
pressOnCheckbox_ = false; pressOnCheckbox_ = false;
// GS 复选框点击基于点击前聚合dsOn + 子 TM 均未被本次点击改写)翻转全开/全关spec §6
// Qt 因 ItemIsUserCheckable 已临时 toggle 了 GS 自身 checkState但随后 recomputeGsState 会覆盖回正确三态。
if (item->data(0, kRoleConfType).toInt() == kConfTypeGs && !item->data(0, kRoleIsRoot).toBool()) {
const bool dsOn = item->data(0, kRoleGsDsOn).toBool();
int total = 0, checked = 0;
for (int i = 0; i < item->childCount(); ++i) {
QTreeWidgetItem* c = item->child(i);
if (c->data(0, kRoleConfType).toInt() != kConfTypeTm) continue;
++total;
if (c->checkState(0) == Qt::Checked) ++checked;
}
const bool turnOn = (aggregateGsState(dsOn, checked, total) == GsCheck::Unchecked);
item->setData(0, kRoleGsDsOn, turnOn);
setAllChildTmChecked(item, turnOn);
recomputeGsState(item);
emitCheckedSources();
}
return; return;
} }
const bool isRoot = item->data(0, kRoleIsRoot).toBool(); const bool isRoot = item->data(0, kRoleIsRoot).toBool();
@ -170,23 +191,24 @@ ObjectTreePanel::ObjectTreePanel(QWidget* parent) : QWidget(parent) {
if (isRoot) return; // 项目根:不联动数据列表/异常(仅右键操作 + 属性占位) if (isRoot) return; // 项目根:不联动数据列表/异常(仅右键操作 + 属性占位)
emit objectClicked(id, confType); emit objectClicked(id, confType);
}); });
// 勾选变化GS 级联会触发多次 itemChanged用 0ms 单发合并成一次「收集勾选叶子并发射」。 // 勾选变化TM 勾选/GS 级联触发多次 itemChanged用 0ms 单发合并;
// 合并里先按子 TM 重算每个 GS 三态(手动聚合),再收集勾选源并集发射。
QObject::connect(tree_, &QTreeWidget::itemChanged, this, [this](QTreeWidgetItem*, int) { QObject::connect(tree_, &QTreeWidget::itemChanged, this, [this](QTreeWidgetItem*, int) {
if (checkPending_) return; if (checkPending_) return;
checkPending_ = true; checkPending_ = true;
QTimer::singleShot(0, this, [this]() { QTimer::singleShot(0, this, [this]() {
checkPending_ = false; checkPending_ = false;
QStringList tmIds; std::function<void(QTreeWidgetItem*)> recompAll = [&](QTreeWidgetItem* node) {
std::function<void(QTreeWidgetItem*)> walk = [&](QTreeWidgetItem* node) {
for (int i = 0; i < node->childCount(); ++i) { for (int i = 0; i < node->childCount(); ++i) {
QTreeWidgetItem* c = node->child(i); QTreeWidgetItem* c = node->child(i);
if (c->data(0, kRoleConfType).toInt() == 2 && c->checkState(0) == Qt::Checked) if (c->data(0, kRoleConfType).toInt() == kConfTypeGs &&
tmIds << c->data(0, kRoleObjId).toString(); !c->data(0, kRoleIsRoot).toBool())
walk(c); recomputeGsState(c); // 内部 SignalBlocker不会再触发 itemChanged
recompAll(c);
} }
}; };
walk(tree_->invisibleRootItem()); recompAll(tree_->invisibleRootItem());
emit checkedTmsChanged(tmIds); emitCheckedSources();
}); });
}); });
@ -228,6 +250,29 @@ ObjectTreePanel::ObjectTreePanel(QWidget* parent) : QWidget(parent) {
menu.addSeparator(); menu.addSeparator();
// 「编辑」弹窗通道已移除GS/TM 统一走「属性」面板(右上对象属性)就地编辑,避免双编辑入口。 // 「编辑」弹窗通道已移除GS/TM 统一走「属性」面板(右上对象属性)就地编辑,避免双编辑入口。
if (isGs) { if (isGs) {
// GS「选择」子菜单分别开关「自身直挂 ds」与「全部子 TM」spec §6 三态手动维护)。
QMenu* sel = menu.addMenu(QStringLiteral("选择"));
int tmCount = 0;
for (int i = 0; i < item->childCount(); ++i)
if (item->child(i)->data(0, kRoleConfType).toInt() == kConfTypeTm) ++tmCount;
QAction* dsAct = sel->addAction(QStringLiteral("ds"));
dsAct->setCheckable(true);
dsAct->setChecked(item->data(0, kRoleGsDsOn).toBool());
QObject::connect(dsAct, &QAction::triggered, this, [this, item](bool on) {
item->setData(0, kRoleGsDsOn, on);
recomputeGsState(item);
emitCheckedSources();
});
QAction* tmAct = sel->addAction(QStringLiteral("tm"));
tmAct->setCheckable(true);
tmAct->setChecked(allTmChecked(item));
tmAct->setEnabled(tmCount > 0);
QObject::connect(tmAct, &QAction::triggered, this, [this, item](bool on) {
setAllChildTmChecked(item, on);
recomputeGsState(item);
emitCheckedSources();
});
menu.addSeparator();
// GS 节点:新建检测对象 / 新建方法对象。TM 节点上不显示「新建检测对象」——xlsxtm 上新建GS 无效。) // GS 节点:新建检测对象 / 新建方法对象。TM 节点上不显示「新建检测对象」——xlsxtm 上新建GS 无效。)
add(QStringLiteral("新建检测对象"), QStringLiteral("newGs")); add(QStringLiteral("新建检测对象"), QStringLiteral("newGs"));
add(QStringLiteral("新建方法对象"), QStringLiteral("newTm")); add(QStringLiteral("新建方法对象"), QStringLiteral("newTm"));
@ -262,6 +307,81 @@ bool ObjectTreePanel::eventFilter(QObject* watched, QEvent* event) {
return QWidget::eventFilter(watched, event); return QWidget::eventFilter(watched, event);
} }
// ── GS 三态手动维护spec §6停用 AutoTristate 后GS 复选框状态完全由此派生。──
void ObjectTreePanel::recomputeGsState(QTreeWidgetItem* gs) {
if (!gs || gs->data(0, kRoleConfType).toInt() != kConfTypeGs || gs->data(0, kRoleIsRoot).toBool())
return;
const bool dsOn = gs->data(0, kRoleGsDsOn).toBool();
int total = 0, checked = 0;
for (int i = 0; i < gs->childCount(); ++i) {
QTreeWidgetItem* c = gs->child(i);
if (c->data(0, kRoleConfType).toInt() != kConfTypeTm) continue;
++total;
if (c->checkState(0) == Qt::Checked) ++checked;
}
const GsCheck s = aggregateGsState(dsOn, checked, total);
const QSignalBlocker block(tree_); // setCheckState 不再触发 itemChanged 递归
gs->setCheckState(0, s == GsCheck::Checked ? Qt::Checked
: s == GsCheck::Partial ? Qt::PartiallyChecked
: Qt::Unchecked);
}
void ObjectTreePanel::setAllChildTmChecked(QTreeWidgetItem* gs, bool checked) {
if (!gs) return;
const Qt::CheckState st = checked ? Qt::Checked : Qt::Unchecked;
const QSignalBlocker block(tree_);
for (int i = 0; i < gs->childCount(); ++i) {
QTreeWidgetItem* c = gs->child(i);
if (c->data(0, kRoleConfType).toInt() == kConfTypeTm) c->setCheckState(0, st);
}
}
bool ObjectTreePanel::allTmChecked(QTreeWidgetItem* gs) const {
if (!gs) return false;
int total = 0, checked = 0;
for (int i = 0; i < gs->childCount(); ++i) {
QTreeWidgetItem* c = gs->child(i);
if (c->data(0, kRoleConfType).toInt() != kConfTypeTm) continue;
++total;
if (c->checkState(0) == Qt::Checked) ++checked;
}
return total > 0 && checked == total;
}
// 收集勾选源并集:勾选的 TM(confType=2) + ds 开的 GS(confType=1) + 项目根直挂 ds(固定纳入)
// 按 {id,confType} 去重,发 checkedSourcesChanged兼发旧 checkedTmsChangedTask 12 删)。
void ObjectTreePanel::emitCheckedSources() {
if (!tree_) return;
std::vector<geopro::data::DataSource> src;
QStringList tmIds;
std::function<void(QTreeWidgetItem*)> walk = [&](QTreeWidgetItem* node) {
for (int i = 0; i < node->childCount(); ++i) {
QTreeWidgetItem* c = node->child(i);
const int ct = c->data(0, kRoleConfType).toInt();
if (ct == kConfTypeTm && c->checkState(0) == Qt::Checked) {
const QString id = c->data(0, kRoleObjId).toString();
src.push_back({id.toStdString(), kConfTypeTm});
tmIds << id;
}
if (ct == kConfTypeGs && !c->data(0, kRoleIsRoot).toBool() && c->data(0, kRoleGsDsOn).toBool())
src.push_back({c->data(0, kRoleObjId).toString().toStdString(), kConfTypeGs});
walk(c);
}
};
walk(tree_->invisibleRootItem());
// 项目根直挂 ds 固定纳入spec §6项目根无复选框、直挂 ds 固定显示)。
if (tree_->topLevelItemCount() > 0) {
QTreeWidgetItem* root = tree_->topLevelItem(0);
if (root->data(0, kRoleIsRoot).toBool())
src.push_back({root->data(0, kRoleObjId).toString().toStdString(), kConfTypeGs});
}
const auto deduped = dedupeSources(std::move(src));
QList<geopro::data::DataSource> list;
for (const auto& s : deduped) list.push_back(s);
emit checkedSourcesChanged(list);
emit checkedTmsChanged(tmIds);
}
// ── 快速筛选:遍历所有 TM 叶子,对其 setCheckState。批量改用 SignalBlocker 屏蔽逐项 itemChanged // ── 快速筛选:遍历所有 TM 叶子,对其 setCheckState。批量改用 SignalBlocker 屏蔽逐项 itemChanged
// 末尾手动触发一次 itemChanged 让既有 0ms 合并逻辑收集并发射 checkedTmsChanged。── // 末尾手动触发一次 itemChanged 让既有 0ms 合并逻辑收集并发射 checkedTmsChanged。──
void ObjectTreePanel::setAllTmsChecked(bool checked) { void ObjectTreePanel::setAllTmsChecked(bool checked) {

View File

@ -1,10 +1,12 @@
#pragma once #pragma once
#include <QList>
#include <QStringList> #include <QStringList>
#include <QWidget> #include <QWidget>
#include <vector> #include <vector>
#include "repo/RepoTypes.hpp" #include "repo/RepoTypes.hpp"
class QTreeWidget; class QTreeWidget;
class QTreeWidgetItem;
class QLabel; class QLabel;
namespace geopro::app { namespace geopro::app {
@ -46,13 +48,22 @@ signals:
// 单击行(含项目根,带 typeId/name/isRoot驱动对象属性面板的可编辑表单。 // 单击行(含项目根,带 typeId/name/isRoot驱动对象属性面板的可编辑表单。
void objectSelectedForEdit(const QString& objectId, int confType, const QString& typeId, void objectSelectedForEdit(const QString& objectId, int confType, const QString& typeId,
const QString& name, bool isRoot); const QString& name, bool isRoot);
// 当前全部被勾选的 TM 叶子 id已合并发射 // 当前全部被勾选的 TM 叶子 id已合并发射【旧信号Task 12 接线切换后删除】
void checkedTmsChanged(const QStringList& tmObjectIds); void checkedTmsChanged(const QStringList& tmObjectIds);
// 当前全部被勾选的数据源并集TM + GS 自身 ds 开 + 项目根直挂 ds按 {id,confType} 去重spec §6
void checkedSourcesChanged(const QList<geopro::data::DataSource>& sources);
// 右键菜单动作action 取值见 .cppobjectId/confType/typeId 为右键命中项name 用于确认框/标题)。 // 右键菜单动作action 取值见 .cppobjectId/confType/typeId 为右键命中项name 用于确认框/标题)。
void contextActionRequested(const QString& action, const QString& objectId, int confType, void contextActionRequested(const QString& action, const QString& objectId, int confType,
const QString& typeId, const QString& name); const QString& typeId, const QString& name);
private: private:
// GS 三态(停用 Qt::ItemIsAutoTristate手动维护据自身 ds 开关 + 子 TM 勾选聚合spec §6
void recomputeGsState(QTreeWidgetItem* gs);
void setAllChildTmChecked(QTreeWidgetItem* gs, bool checked); // 批量设子 TM内部 SignalBlocker
bool allTmChecked(QTreeWidgetItem* gs) const; // GS 下子 TM 是否全勾(无子 TM=false
// 收集勾选源并集TM + GS 自身 ds + 项目根直挂 ds去重后发 checkedSourcesChanged兼发旧 checkedTmsChanged
void emitCheckedSources();
QTreeWidget* tree_ = nullptr; // Qt 原生标准树(复选框/箭头由 Fusion 绘制,清晰可控) QTreeWidget* tree_ = nullptr; // Qt 原生标准树(复选框/箭头由 Fusion 绘制,清晰可控)
QLabel* hint_ = nullptr; QLabel* hint_ = nullptr;
bool checkPending_ = false; // 勾选合并发射防重入 bool checkPending_ = false; // 勾选合并发射防重入

View File

@ -0,0 +1,31 @@
#pragma once
#include <vector>
#include "repo/RepoTypes.hpp"
namespace geopro::app {
enum class GsCheck { Unchecked, Partial, Checked };
// GS 复选框三态 = [自身 ds 开关] [子 TM 勾选] 的聚合spec §6
// 无子 TMtotalTmCount==0时退化为仅看 dsOn。
inline GsCheck aggregateGsState(bool dsOn, int checkedTmCount, int totalTmCount) {
const bool anyOn = dsOn || checkedTmCount > 0;
if (!anyOn) return GsCheck::Unchecked;
const bool tmAll = (totalTmCount == 0) || (checkedTmCount == totalTmCount);
if (dsOn && tmAll) return GsCheck::Checked;
return GsCheck::Partial;
}
// 对象树勾选产出的数据源并集按 {id,confType} 去重保序spec §6
inline std::vector<geopro::data::DataSource> dedupeSources(std::vector<geopro::data::DataSource> in) {
std::vector<geopro::data::DataSource> out;
for (const auto& s : in) {
bool dup = false;
for (const auto& o : out)
if (o.id == s.id && o.confType == s.confType) { dup = true; break; }
if (!dup) out.push_back(s);
}
return out;
}
} // namespace geopro::app

View File

@ -21,6 +21,9 @@ struct DsRow {
int structParentConfType = 0; // 1=GS/项目根 2=TM int structParentConfType = 0; // 1=GS/项目根 2=TM
}; };
struct DsPage { std::vector<DsRow> rows; int total = 0; }; struct DsPage { std::vector<DsRow> rows; int total = 0; };
// 对象树勾选产出的数据源spec §6。confType: 1=GS/项目根, 2=TM。
struct DataSource { std::string id; int confType = 0; };
struct TmNode { std::string id, name, confCode; std::vector<DsNode> dss; }; struct TmNode { std::string id, name, confCode; std::vector<DsNode> dss; };
struct GsNode { std::string id, name; std::vector<TmNode> tms; }; struct GsNode { std::string id, name; std::vector<TmNode> tms; };
struct Project { std::string id, name; std::vector<GsNode> gss; }; struct Project { std::string id, name; std::vector<GsNode> gss; };

View File

@ -174,6 +174,8 @@ target_sources(geopro_tests PRIVATE
app/test_dataset_category.cpp app/test_dataset_category.cpp
${CMAKE_SOURCE_DIR}/src/app/DatasetCategory.cpp ${CMAKE_SOURCE_DIR}/src/app/DatasetCategory.cpp
) )
# GS aggregateGsState + dedupeSourcesheader-only
target_sources(geopro_tests PRIVATE app/test_object_tree_selection.cpp)
# measurement / id / / Qt6::Core JSON + core model # measurement / id / / Qt6::Core JSON + core model
target_sources(geopro_tests PRIVATE target_sources(geopro_tests PRIVATE
app/test_scatter_data_ops.cpp app/test_scatter_data_ops.cpp

View File

@ -0,0 +1,37 @@
#include <gtest/gtest.h>
#include "panels/ObjectTreeSelection.hpp"
using namespace geopro::app;
using geopro::data::DataSource;
TEST(AggregateGsState, AllOnIsChecked) {
EXPECT_EQ(aggregateGsState(true, 3, 3), GsCheck::Checked);
}
TEST(AggregateGsState, AllOffIsUnchecked) {
EXPECT_EQ(aggregateGsState(false, 0, 3), GsCheck::Unchecked);
}
TEST(AggregateGsState, DsOnTmNoneIsPartial) {
EXPECT_EQ(aggregateGsState(true, 0, 3), GsCheck::Partial); // 只 GS 自身 ds
}
TEST(AggregateGsState, DsOffSomeTmIsPartial) {
EXPECT_EQ(aggregateGsState(false, 1, 3), GsCheck::Partial); // 部分子 TM
}
TEST(AggregateGsState, DsOnSomeTmIsPartial) {
EXPECT_EQ(aggregateGsState(true, 2, 3), GsCheck::Partial); // ds 开但 TM 未满
}
TEST(AggregateGsState, NoTmFallsBackToDsOnly) {
EXPECT_EQ(aggregateGsState(true, 0, 0), GsCheck::Checked); // 无子 TM → 仅看 ds 开关
EXPECT_EQ(aggregateGsState(false, 0, 0), GsCheck::Unchecked);
}
TEST(DedupeSources, RemovesDuplicateByIdAndConfType) {
std::vector<DataSource> in = {{"t1", 2}, {"g1", 1}, {"t1", 2}, {"g1", 1}, {"t2", 2}};
const auto out = dedupeSources(in);
ASSERT_EQ(out.size(), 3u);
EXPECT_EQ(out[0].id, "t1"); EXPECT_EQ(out[0].confType, 2);
EXPECT_EQ(out[1].id, "g1"); EXPECT_EQ(out[1].confType, 1);
EXPECT_EQ(out[2].id, "t2");
}
TEST(DedupeSources, SameIdDifferentConfTypeKept) {
std::vector<DataSource> in = {{"x", 1}, {"x", 2}};
EXPECT_EQ(dedupeSources(in).size(), 2u);
}