diff --git a/docs/superpowers/STATUS.md b/docs/superpowers/STATUS.md index 053c45b..4997300 100644 --- a/docs/superpowers/STATUS.md +++ b/docs/superpowers/STATUS.md @@ -18,7 +18,8 @@ - 二维地图 = `MapLineActor`:测线 `lat/lon` 轨迹**红线**俯视(浅底),像地图。 - 三维视图 = `CurtainActor`:沿测线的**竖直断面墙**(分段色带,z 纵向夸张×3,沿弯曲测线弯)。中央工具条**仅**「二维地图/三维视图」(对齐原型,无体素按钮)。 - **下方 数据详情**:工具条「原数据/网格数据」切换 +「显示异常」开关(对齐原型命名)。单击数据集 → 网格数据=`GridContourActor` 平面剖面(#18,colorBar 真实非均匀分段值上色,纵向夸张×1.5);原数据=`ScatterActor` 彩色方块散点(#17,x=距离/y=深度取负,用散点自带色阶);显示异常=`AnomalyActor` 在上图叠加异常 dashed 折线(同纵向夸张对齐)。 - - **右 属性**:名称/网格 nx×ny/vmin·vmax。 + - **右上 异常列表**(对齐原型):单击数据集→列该数据集异常(颜色块+名称(类型)+派生「位置/深/尺寸」),勾选框显隐,与数据详情异常叠加联动(取消勾选→该异常虚线隐藏)。 + - **右下 属性**:名称/网格 nx×ny/vmin·vmax/异常数。 - 单元测试累计 **36 个全绿**(core/data/net/render;含 Scatter 2 + Anomaly 4 + VoxelRegister 1、修复了陈旧的 Curtain mapper 类型断言);离屏 `verify_section/map/curtain_3d/scatter/section_anomaly/voxel_top/voxel_3d.png` 均核对正确(scatter 吻合 ref_17、异常吻合 ref_18、体素 footprint 吻合 ref voxel_hslice 的两臂支撑)。 ## 2. 各 Phase 完成度 @@ -73,12 +74,12 @@ 7. 多测线:当前样本仅 1 条 dd_section(grid1);多条共存机制已就绪,加数据即叠加。 8. 取景微调(数据详情/帘面的相机余量);纵向夸张倍数(剖面1.5/帘面3)可做成可调。 9. render 仍部分内联在 main.cpp;可逐步抽到 view/controller。 -10. **布局对齐原型**(权威参考 `http://prototype.geomative.cn/`,用户指定;截图存 `.playwright-mcp/`)。原型为**四区六面板**,当前 app 为简化版,缺: - - **左下「数据真实显示栏」**:选中 TM(测线)后列其采集批次(=DS 数据集),tab「数据/文件」。当前 app 把 DS 直接塞树里。 - - **右上「异常列表」**:异常条目(名称/深度/尺寸/电阻率 + 颜色标记 + 眼睛显隐),与数据详情「显示异常」联动。 - - **数据详情工具条**:原型还有 色阶配置/滤波处理/显示等值线标签/纵化容差滑块/显示电极/网格 等(当前仅原数据·网格数据·显示异常)。 - - **电极标记**:剖面顶部倒三角▼电极位 + 数值标签;**底图影像**(3D 卫星底图 + 测线落地)= DEM/底图任务(CRS 阻塞)。 - 诚实记录:已对齐者=中央二维/三维切换、数据详情原数据/网格数据/显示异常、对象树、属性。 +10. **布局对齐原型**(权威参考 `http://prototype.geomative.cn/`;截图存 `.playwright-mcp/`)。**计划见 `plans/2026-06-08-m1-prototype-layout.md`(六面板 + view/ 抽取,增量序列)**。进度: + - ✅ **增量1 右上「异常列表」**(2026-06-08,`panels/AnomalyListPanel`,与数据详情显隐联动;待人工复核)。 + - ⬜ **增量2 左下「数据列表」**+ 对象树到 TM 层(DS 移出树入数据列表)。 + - ⬜ **增量3 3D「视图详情」图层浮层**(体素的正确归宿;体素引擎已就绪,UI 待此接入)。 + - ⬜ **增量4 数据详情富工具条 + 电极标记 + 数值标签**;底图影像=DEM/底图任务。 + - 架构:新面板抽到 `src/app/panels/`(暂随 app 编译,如 login/),控制 main.cpp 体量;后续可升 `src/view/` 库。 ## 7. 渲染验证手段(务必用) diff --git a/docs/superpowers/plans/2026-06-08-m1-prototype-layout.md b/docs/superpowers/plans/2026-06-08-m1-prototype-layout.md new file mode 100644 index 0000000..b38af1d --- /dev/null +++ b/docs/superpowers/plans/2026-06-08-m1-prototype-layout.md @@ -0,0 +1,89 @@ +# M1 工作台布局对齐原型(六面板结构 + view/ 抽取) + +> **权威参考**:原型 `http://prototype.geomative.cn/`(用户指定;截图存 `.playwright-mcp/`)。 +> **背景**:当前 app 是简化版(左对象树 / 中二维三维+数据详情 / 右属性)。功能积木齐全 +> (帘面/测线线/#18/#17散点/异常/体素引擎),但缺原型那套"面板/图层"结构,导致没有正确的 +> 交互入口(曾误把体素塞成工具条开关,已移除 `42a7ed1`)。本计划按原型补齐面板。 +> **铁律**:渲染积木改动先 `render_verify` 出 PNG;app 交互须人工登录肉眼复核。 + +--- + +## 原型六面板(权威,逐一核对截图) + +1. **左上 对象显示栏**:项目 → GS-01北区/GS-02南区 → ERT1..4(测线 TM)。复选框 + 彩色线型图标 + + 数量徽标 + "+"。**注**:原型树到 **TM(测线)** 层;**DS(采集批次)在左下数据列表**,不在树里。 +2. **左下 数据真实显示栏 - [选中TM]**:tab「数据(n) / 文件(n)」;列该测线的**采集批次(DS)** + (名 #日期 + 道数)。筛选 + 上传图标。单击采集批次 → 驱动数据详情 + 属性。 +3. **中上 二维地图 / 三维视图**(工具条):右上「n/m 测线可见」+ 全屏。 + - 三维:卫星底图 + 测线落地线 + 电极 + 左上**「视图详情」图层浮层**(勾选显示哪些图层: + 测线/帘面/**体素**/地形)+ 右上 天地图底图选择 + 右下 坐标·比例尺。 + - 二维:测线俯视(底图瓦片 M1.5)。 +4. **中下 数据详情**(已有雏形):采集批次 tab;「原数据/网格数据」;工具条(异常标注/色阶配置/ + 滤波/显示异常/显示等值线标签/纵化容差/显示电极/显示等值线/网格…);剖面图 + 顶部电极▼ + + 数值标签 + 异常叠加;色阶条。 +5. **右上 异常列表 / 对象属性**(tab):异常条目(名 + 位置·深度·尺寸 + 颜色标记 + 眼睛显隐)。 + 筛选 + 添加。与数据详情异常叠加联动。 +6. **右下 属性**:数据集(采集批次/通道/状态/日期/测线/区)。 + +## 当前 app vs 原型差距 +- 树含 DS(应移到左下数据列表,树到 TM 止)。 +- 缺 左下数据列表、右上异常列表、3D视图详情图层浮层、电极标记、数值标签、底图、数据详情富工具条。 +- main.cpp 全内联(~430 行);再加面板会破 800 行铁律 → **须抽到 `src/view/panels/`**(spec §3)。 + +--- + +## 架构决策:抽到 view/ 面板(spec §3) +新增面板不再堆进 main.cpp。按 spec §3 建 `src/view/panels/`: +`ObjectTreePanel`、`DatasetListPanel`、`CentralViewPanel`(含 2D/3D + 图层浮层)、`DataDetailPanel`、 +`AnomalyListPanel`、`PropertyPanel`。main.cpp 仅装配 dock + 连线(或后续引入 controller)。 +**渐进**:每抽一个面板即构建+人工复核,绿了再下一个,避免一次性大爆破。 + +--- + +## 增量序列(每步可独立构建 + 提交;UI 须人工登录复核) + +### 增量 1:右上「异常列表」面板 ✅ 已完成(2026-06-08) +> `src/app/panels/AnomalyListPanel.{hpp,cpp}`(`populateAnomalyList`:颜色块+名称(类型)+派生「位置/深/尺寸」, +> 可勾选默认显示,UserRole 存异常下标)。右上 dock「异常列表」+ 右下「属性」(上下分)。单击数据集→重填列表 +> (QSignalBlocker 防回灌)+ hiddenAnoms 清空;勾选变化→更新 hiddenAnoms→rebuildDetail 逐异常按下标过滤显隐。 +> 属性补"异常 N 个"。app 构建干净;**待人工登录复核**(列表出 3 异常、取消勾选→对应虚线隐藏)。 +> 提交见下。**原计划如下(存档)**: +- `src/view/panels/AnomalyListPanel.{hpp,cpp}`(QWidget + QListWidget/QTreeWidget)。 +- 数据:`repo.loadAnomalies(id)`;core `Anomaly` 已有 name/typeName/markType/localPts/lineColor。 + 派生**位置(质心距离)**、**深度(质心 y)**、**尺寸(bbox 对角/跨度)** 自 localPts;颜色用 lineColor。 + (电阻率 Ωm 无源字段,M1 暂不显示或显"—";如需后续用 grid 在异常处采样。) +- 每条目:色块 + 名称 + "位置 Xm·深 Ym·尺寸 Zm" + 眼睛显隐。 +- 联动:单击数据集→填充列表;眼睛 toggle → 该异常 actor `SetVisibility`(buildAnomalies 已每异常一 actor, + 需让 DataDetailPanel 暴露按 index 设可见或重建时按显隐集过滤)。 +- 放右上 dock(与"属性"分上下或 tab)。**人工复核**:单击数据集→右上列出 3 异常;眼睛切换隐藏对应虚线。 +- 提交 `feat(view): 右上异常列表面板 + 与数据详情异常显隐联动`。 + +### 增量 2:左下「数据列表」+ 树结构调整(树到 TM,DS 入数据列表) +- repo.loadStructure 改为树到 **TM** 层(GS→TM),DS 不再进树;新增 `listDatasets(tmId)` 或复用结构。 + (当前样本仅 1 DS grid1 挂 ERT1;多 TM/DS 为 mock 结构,先支持单条,预留多条。) +- `src/view/panels/DatasetListPanel`:tab 数据/文件;列选中 TM 的采集批次;单击 → 数据详情 + 属性。 +- 树单击 TM → 填充数据列表;移除"单击树DS"旧路径。 +- **人工复核**:点 ERT1 → 左下列出采集批次 → 单击 → 数据详情出图。 +- 提交 `feat(view): 左下数据列表 + 对象树到测线层(DS 移出树)`。 + +### 增量 3:3D「视图详情」图层浮层(体素的正确归宿) +- 中央三维视图左上浮层(QWidget overlay on QVTK,或工具区):勾选显示 测线/帘面/**体素**/(地形)。 +- 勾"体素" → 调 `buildVoxelFromScatters`(已验证)加入 3D 场景;勾选驱动 rebuildCentral 的图层集。 +- main() 设 PROJ_DATA(体素配准需 proj.db;按候选路径,部署随包附带)。 +- **人工复核**:三维视图勾"体素"→ 出十字片体素,与帘面同系;取消则移除。 +- 提交 `feat(view): 3D 视图详情图层浮层(测线/帘面/体素) + 体素正经接入`。 + +### 增量 4:数据详情富工具条 + 电极 + 数值标签(对齐原型中下) +- 工具条补 色阶配置/显示电极/显示等值线 等(按需,部分 M1 可占位)。 +- 剖面顶部电极▼标记(grid.lat/lon 或 x 轴电极位);可选数值标签。 +- 提交 `feat(view): 数据详情电极标记 + 工具条对齐原型`。 + +### (增量 5,可选)右下属性面板规范化 + 整体 dock 透视持久化 + +--- + +## Self-Review +- 严格按原型六面板;体素归 3D 图层浮层(修正"工具条开关"错误)。 +- 面板抽到 view/(spec §3),守 800 行铁律;每增量可构建+复核+提交。 +- 渲染积木复用既有(已 PNG 验证);异常显隐/体素接入复用 buildAnomalies/buildVoxelFromScatters。 +- 回写 STATUS §6.10 / 本计划完成度。 diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 571de88..4929d5c 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -14,7 +14,8 @@ find_package(nlohmann_json CONFIG REQUIRED) add_executable(geopro_desktop WIN32 main.cpp - login/LoginWindow.cpp) + login/LoginWindow.cpp + panels/AnomalyListPanel.cpp) target_include_directories(geopro_desktop PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/src/app/main.cpp b/src/app/main.cpp index 560fe2f..bc18d11 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -25,6 +25,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -43,6 +46,7 @@ #include "ApiClient.hpp" #include "AuthService.hpp" #include "login/LoginWindow.hpp" +#include "panels/AnomalyListPanel.hpp" #include "CameraPreset.hpp" #include "Scene.hpp" @@ -56,6 +60,7 @@ #include #include +#include #include #include @@ -228,14 +233,21 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re leftDock->setWidget(tree); dockManager->addDockWidget(ads::LeftDockWidgetArea, leftDock); - // 右 dock:属性。 + // 右上 dock:异常列表(对齐原型;颜色块 + 名称 + 位置/深/尺寸 + 勾选显隐,与数据详情异常联动)。 + auto* anomalyList = new QListWidget(); + anomalyList->setAlternatingRowColors(true); + auto* anomalyDock = new ads::CDockWidget(QStringLiteral("异常列表")); + anomalyDock->setWidget(anomalyList); + auto* rightArea = dockManager->addDockWidget(ads::RightDockWidgetArea, anomalyDock); + + // 右下 dock:属性。 auto* propLabel = new QLabel(QStringLiteral("(单击左侧数据集查看属性与平面剖面)")); propLabel->setWordWrap(true); propLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft); propLabel->setMargin(8); - auto* rightDock = new ads::CDockWidget(QStringLiteral("数据集 / 属性")); - rightDock->setWidget(propLabel); - dockManager->addDockWidget(ads::RightDockWidgetArea, rightDock); + auto* propDock = new ads::CDockWidget(QStringLiteral("属性")); + propDock->setWidget(propLabel); + dockManager->addDockWidget(ads::BottomDockWidgetArea, propDock, rightArea); // ── 中央视图重建(核心)───────────────────────────────────────────── // 两个互斥视图按当前勾选集整体重建:scene.clear() → 对每个勾选 dd_section 加对应 actor。 @@ -295,12 +307,13 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // 当前选中数据集 id(空=未选)与详情显示模式(反演剖面/原数据);切模式或换选中都重建。 auto currentDsId = std::make_shared(); auto detailMode = std::make_shared(DetailMode::Section18); - auto showAnomalies = std::make_shared(true); // 默认显示异常(对齐原型) + auto showAnomalies = std::make_shared(true); // 默认显示异常(对齐原型) + auto hiddenAnoms = std::make_shared>(); // 异常列表中被取消勾选(隐藏)的异常下标 // 按当前选中 DS + 详情模式重建下方数据详情(平躺俯视正交,纵向夸张填面板)。 // 勾选「显示异常」时在 #18/#17 上叠加异常 dashed 折线(同纵向夸张对齐)。 auto rebuildDetail = [&repo, detailRendererPtr, detailRenderWindowPtr, currentDsId, detailMode, - showAnomalies]() { + showAnomalies, hiddenAnoms]() { detailRendererPtr->RemoveAllViewProps(); if (currentDsId->isEmpty()) { // 未选数据集:清空即可 detailRenderWindowPtr->Render(); @@ -330,12 +343,15 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re detailRendererPtr->AddViewProp(a); } } - // 异常叠加(与剖面同坐标系/同纵向夸张)。 + // 异常叠加(与剖面同坐标系/同纵向夸张)。逐异常构建以按列表显隐(下标=原 vector 序)过滤。 if (*showAnomalies) { const auto anomalies = repo.loadAnomalies(id); - for (auto& act : geopro::render::buildAnomalies(anomalies)) { - act->SetScale(1.0, kDetailYScale, 1.0); - detailRendererPtr->AddViewProp(act); + for (int i = 0; i < static_cast(anomalies.size()); ++i) { + if (hiddenAnoms->count(i)) continue; // 列表中取消勾选→隐藏 + for (auto& act : geopro::render::buildAnomalies({anomalies[i]})) { + act->SetScale(1.0, kDetailYScale, 1.0); + detailRendererPtr->AddViewProp(act); + } } } geopro::render::applyTop2D(detailRendererPtr); @@ -346,7 +362,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // ── 单击 DS → 记选中 + 重建数据详情 + 右侧属性(与勾选区分;不改帘面可见性)── QObject::connect( tree, &QTreeWidget::itemClicked, tree, - [&repo, propLabel, currentDsId, rebuildDetail](QTreeWidgetItem* item, int) { + [&repo, propLabel, currentDsId, rebuildDetail, anomalyList, hiddenAnoms]( + QTreeWidgetItem* item, int) { const QString dsId = item->data(0, kRoleDsId).toString(); if (dsId.isEmpty()) return; // GS/TM 节点无详情 const QString ddType = item->data(0, kRoleDdType).toString(); @@ -354,16 +371,37 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re const QString name = item->text(0); *currentDsId = dsId; + + // 右上异常列表:按该数据集异常重填(默认全显);先清隐藏集再填,避免重建时阻塞信号回灌。 + const auto anomalies = repo.loadAnomalies(dsId.toStdString()); + hiddenAnoms->clear(); + { + const QSignalBlocker block(anomalyList); // 重填触发 itemChanged,先屏蔽 + geopro::app::populateAnomalyList(anomalyList, anomalies); + } + rebuildDetail(); - // 右侧属性(数据集级,与详情模式无关)。 + // 右下属性(数据集级,与详情模式无关)。 const auto g = repo.loadGrid(dsId.toStdString()); propLabel->setText( QStringLiteral("数据集: %1\n类型: 剖面网格 (dd_section)\n网格: %2 x %3\n" - "vmin / vmax: %4 / %5") - .arg(name).arg(g.nx()).arg(g.ny()).arg(g.vmin).arg(g.vmax)); + "vmin / vmax: %4 / %5\n异常: %6 个") + .arg(name).arg(g.nx()).arg(g.ny()).arg(g.vmin).arg(g.vmax) + .arg(anomalies.size())); }); + // ── 异常列表勾选(显隐) → 更新隐藏集 → 重建数据详情 ── + QObject::connect(anomalyList, &QListWidget::itemChanged, anomalyList, + [hiddenAnoms, rebuildDetail](QListWidgetItem* item) { + const int idx = item->data(geopro::app::kAnomalyIndexRole).toInt(); + if (item->checkState() == Qt::Checked) + hiddenAnoms->erase(idx); + else + hiddenAnoms->insert(idx); + rebuildDetail(); + }); + // ── 数据详情工具条「反演剖面/原数据」:切模式 → 重建数据详情 ── QObject::connect(actSection, &QAction::triggered, detailWidget, [detailMode, rebuildDetail]() { diff --git a/src/app/panels/AnomalyListPanel.cpp b/src/app/panels/AnomalyListPanel.cpp new file mode 100644 index 0000000..6f5fe2d --- /dev/null +++ b/src/app/panels/AnomalyListPanel.cpp @@ -0,0 +1,76 @@ +#include "panels/AnomalyListPanel.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "model/ColorScale.hpp" + +namespace geopro::app { + +namespace { + +// 颜色块图标边长(像素)。 +constexpr int kSwatch = 12; + +// 由 localPts 算「位置(质心x)·深(质心y)·尺寸(包络对角)」摘要文本。 +// 异常坐标在剖面距离/深度空间(x=距离米, y=深度米)。 +QString summarize(const geopro::core::Anomaly& a) +{ + if (a.localPts.empty()) return QStringLiteral("(无几何)"); + double cx = 0.0, cy = 0.0; + double minx = a.localPts[0].x, maxx = a.localPts[0].x; + double miny = a.localPts[0].y, maxy = a.localPts[0].y; + for (const auto& p : a.localPts) { + cx += p.x; cy += p.y; + if (p.x < minx) minx = p.x; + if (p.x > maxx) maxx = p.x; + if (p.y < miny) miny = p.y; + if (p.y > maxy) maxy = p.y; + } + const auto n = static_cast(a.localPts.size()); + cx /= n; cy /= n; + const double span = std::hypot(maxx - minx, maxy - miny); + return QStringLiteral("位置 %1m · 深 %2m · 尺寸 %3m") + .arg(cx, 0, 'f', 0) + .arg(cy, 0, 'f', 0) + .arg(span, 0, 'f', 0); +} + +// lineColor 字符串("#RRGGBB"/"rgba(...)") → 颜色块 QPixmap。 +QPixmap swatch(const std::string& colorStr) +{ + const auto c = geopro::core::parseColor(colorStr, geopro::core::AlphaScale::Bit255); + QPixmap pm(kSwatch, kSwatch); + pm.fill(QColor(c.r, c.g, c.b)); + return pm; +} + +} // namespace + +void populateAnomalyList(QListWidget* list, const std::vector& anomalies) +{ + if (!list) return; + list->clear(); + for (std::size_t i = 0; i < anomalies.size(); ++i) { + const auto& a = anomalies[i]; + const QString name = QString::fromStdString(a.name.empty() ? "异常" : a.name); + const QString type = QString::fromStdString(a.typeName); + QString text = name; + if (!type.isEmpty()) text += QStringLiteral("(%1)").arg(type); + text += QStringLiteral("\n%1").arg(summarize(a)); + + auto* item = new QListWidgetItem(QIcon(swatch(a.lineColor)), text, list); + item->setData(kAnomalyIndexRole, static_cast(i)); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Checked); // 默认显示 + } +} + +} // namespace geopro::app diff --git a/src/app/panels/AnomalyListPanel.hpp b/src/app/panels/AnomalyListPanel.hpp new file mode 100644 index 0000000..e5566ed --- /dev/null +++ b/src/app/panels/AnomalyListPanel.hpp @@ -0,0 +1,19 @@ +#pragma once +#include + +#include "model/Anomaly.hpp" + +class QListWidget; + +namespace geopro::app { + +// 异常索引存于条目的 Qt::UserRole(= 在原异常 vector 中的下标,用于显隐映射)。 +constexpr int kAnomalyIndexRole = 0x0100; // Qt::UserRole + +// 用异常填充 QListWidget(对齐原型右上「异常列表」):每条目 = 颜色块图标 + 名称 + +// 派生「位置 Xm · 深 Ym · 尺寸 Zm」(由 location.coordinate 质心/包络算)。 +// 条目可勾选:勾=显示(默认全勾);勾选状态变化由调用方连接驱动该异常 actor 显隐。 +// 清空旧条目后重填。 +void populateAnomalyList(QListWidget* list, const std::vector& anomalies); + +} // namespace geopro::app