feat(render): 散点#17(ScatterActor) — 数据详情「反演剖面/原数据」切换

- ScatterActor(buildScatter): ScatterField+ColorScale → vtkPolyData 彩色方块散点
  (x=距离/y=深度取负, 与#18同坐标系; 点标量+LUT, 色阶范围优先colorBar真实分段值)
- 离屏 verify_scatter.png 核对吻合 Python 真值 ref_17(三角拟断面/顶部深蓝/右侧紫)
- 接入 app 数据详情: 工具条「反演剖面(#18)/原数据(#17)」互斥切换, rebuildDetail 统一重建
- LocalSampleRepository.loadScatterColorScale: 散点自带色阶(范围/分段与网格色阶不同)
- 修复陈旧测试 test_curtain(断言改为 vtkPolyDataMapper+banded 非空 polydata)
- 新增 test_scatter 2 例(点数/verts/上色/y取负/退化安全); 全 31 测试绿
This commit is contained in:
gaozheng 2026-06-08 07:43:36 +08:00
parent 95bc521f49
commit f51fe44533
12 changed files with 292 additions and 38 deletions

View File

@ -17,9 +17,9 @@
- **中央 二维地图 / 三维视图**(两个**真内容**,非相机切换): - **中央 二维地图 / 三维视图**(两个**真内容**,非相机切换):
- 二维地图 = `MapLineActor`:测线 `lat/lon` 轨迹**红线**俯视(浅底),像地图。 - 二维地图 = `MapLineActor`:测线 `lat/lon` 轨迹**红线**俯视(浅底),像地图。
- 三维视图 = `CurtainActor`:沿测线的**竖直断面墙**(分段色带,z 纵向夸张×3,沿弯曲测线弯)。 - 三维视图 = `CurtainActor`:沿测线的**竖直断面墙**(分段色带,z 纵向夸张×3,沿弯曲测线弯)。
- **下方 数据详情**:单击数据集 → `GridContourActor` 平面反演剖面(#18,colorBar 真实非均匀分段值上色,纵向夸张×1.5)。 - **下方 数据详情**:工具条「反演剖面/原数据」切换。单击数据集 → 反演剖面=`GridContourActor` 平面剖面(#18,colorBar 真实非均匀分段值上色,纵向夸张×1.5);原数据=`ScatterActor` 彩色方块散点(#17,x=距离/y=深度取负,用散点自带色阶)。
- **右 属性**:名称/网格 nx×ny/vmin·vmax。 - **右 属性**:名称/网格 nx×ny/vmin·vmax。
- 单元测试累计 ~28 个全绿(core/data/net/render);离屏 `verify_section/map/curtain_3d.png` 均核对正确 - 单元测试累计 **31 个全绿**(core/data/net/render;含 Scatter 2 个、修复了陈旧的 Curtain mapper 类型断言);离屏 `verify_section/map/curtain_3d/scatter.png` 均核对正确(scatter 与 Python 真值 ref_17 吻合)
## 2. 各 Phase 完成度 ## 2. 各 Phase 完成度
@ -29,7 +29,7 @@
| P1 | core(LocalFrame/模型/ColorScale/IDW/CrsTransform/GeoLocalFrame) | ✅ | | P1 | core(LocalFrame/模型/ColorScale/IDW/CrsTransform/GeoLocalFrame) | ✅ |
| P2 | data(解析器/LocalSampleRepository)+ 对象树 | ✅ | | P2 | data(解析器/LocalSampleRepository)+ 对象树 | ✅ |
| P3 | 登录(RsaEncryptor/ApiClient/AuthService/LoginWindow) | ✅(**Credential 记住免登录未做**) | | P3 | 登录(RsaEncryptor/ApiClient/AuthService/LoginWindow) | ✅(**Credential 记住免登录未做**) |
| P4 | 渲染:render 层 + 二维地图(线)+ 三维视图(帘面)+ 数据详情(#18) | 🔶 **核心三视图已对**;**散点#17 / 异常叠加 / DEM地形 / dd_voxel回归 / 底图瓦片 未做** | | P4 | 渲染:render 层 + 二维地图(线)+ 三维视图(帘面)+ 数据详情(#18/#17) | 🔶 **核心三视图 + 散点#17 已对**;**异常叠加 / DEM地形 / dd_voxel回归 / 底图瓦片 未做** |
## 3. 构建约定(**机器本地** ## 3. 构建约定(**机器本地**
@ -60,8 +60,8 @@
## 6. 已知问题 / 待办P4 下次会话) ## 6. 已知问题 / 待办P4 下次会话)
1. **散点 #17**:`ScatterActor`(剖面原数据 2597 点彩色散点),可作数据详情"原数据"视图。 1. ~~**散点 #17**:`ScatterActor`(剖面原数据 2597 点彩色散点),数据详情"原数据"视图~~ ✅ **已完成**(2026-06-08,离屏 PNG 核对吻合 Python 真值,接入数据详情「反演剖面/原数据」切换;app 待人工登录肉眼复核交互)
2. **异常叠加**:`AnomalyActor`(markType 点/线/面),叠加在剖面/帘面上(异常数据已能解析)。 2. **异常叠加**:`AnomalyActor`(markType 点/线/面),叠加在剖面/帘面上(异常数据已能解析)。**下一项**。
3. **DEM/影像地形**:加 vcpkg `gdal`;GDAL 读 dem.tif/image.tif;**影像 EPSG:3857 必须 PROJ 重投影到世界系**;`vtkWarpScalar` 地形面 + 纹理。 3. **DEM/影像地形**:加 vcpkg `gdal`;GDAL 读 dem.tif/image.tif;**影像 EPSG:3857 必须 PROJ 重投影到世界系**;`vtkWarpScalar` 地形面 + 纹理。
4. **dd_voxel 回归**:需先确认项目 CRS,使散点 projX/Y 能转到 lat/lon 世界系,与帘面配准;VoxelActor 已就绪。 4. **dd_voxel 回归**:需先确认项目 CRS,使散点 projX/Y 能转到 lat/lon 世界系,与帘面配准;VoxelActor 已就绪。
5. **底图瓦片**(二维地图,天地图/Mapbox):M1.5。 5. **底图瓦片**(二维地图,天地图/Mapbox):M1.5。
@ -84,4 +84,5 @@ cmd /c "...\external\dev.bat cmake --build build/release --target render_verify"
- 计划:`plans/` 下 phase0-4 + `2026-06-07-m1-view-redesign.md`(正确视图模型)+ spike-report - 计划:`plans/` 下 phase0-4 + `2026-06-07-m1-view-redesign.md`(正确视图模型)+ spike-report
- 环境:`../ENV_SETUP_Windows.md` - 环境:`../ENV_SETUP_Windows.md`
- 验证脚本:`tools/validate_samples.py`(#17/#18 真值)、`tools/validate_voxel.py`(voxel)、`tests/spike/render_verify.cpp`(app 渲染积木离屏核对) - 验证脚本:`tools/validate_samples.py`(#17/#18 真值)、`tools/validate_voxel.py`(voxel)、`tests/spike/render_verify.cpp`(app 渲染积木离屏核对)
- 渲染积木(render 层):`MapLineActor`(测线线)、`CurtainActor`(断面墙)、`GridContourActor`(#18 平面)、`VoxelActor`(体,未接 UI)、`ColorLutBuilder`、`CameraPreset`、`Scene`;`core::GeoLocalFrame`(经纬→局部米)。 - 渲染积木(render 层):`MapLineActor`(测线线)、`CurtainActor`(断面墙)、`GridContourActor`(#18 平面)、`ScatterActor`(#17 散点)、`VoxelActor`(体,未接 UI)、`ColorLutBuilder`、`CameraPreset`、`Scene`;`core::GeoLocalFrame`(经纬→局部米)。
- 散点 #17 专属色阶经 `LocalSampleRepository::loadScatterColorScale`(剖面原数据自带色阶,范围/分段与网格色阶不同)。

View File

@ -5,7 +5,7 @@
> **⚠️ 状态(2026-06-07,务必先读 `2026-06-07-m1-view-redesign.md` + STATUS.md):** > **⚠️ 状态(2026-06-07,务必先读 `2026-06-07-m1-view-redesign.md` + STATUS.md):**
> - **Task 1(render 层 + 相机预设)**:✅ 已完成。但中央"单场景平躺剖面 + 2D/3D 仅换相机"的做法**已被 view-redesign 取代** —— 现在中央是 **二维地图(测线线)/ 三维视图(竖直帘面)两种内容**(详见 view-redesign 的"实施结果")。 > - **Task 1(render 层 + 相机预设)**:✅ 已完成。但中央"单场景平躺剖面 + 2D/3D 仅换相机"的做法**已被 view-redesign 取代** —— 现在中央是 **二维地图(测线线)/ 三维视图(竖直帘面)两种内容**(详见 view-redesign 的"实施结果")。
> - **Task 2(dd_voxel 体绘制+切片)**:🔶 VoxelActor 代码完成,但**已从 UI 移除/搁置**(散点 projX/Y 真实 CRS 未确认,无法与 lat/lon 帘面配准)。**CRS 确认后再回归**。 > - **Task 2(dd_voxel 体绘制+切片)**:🔶 VoxelActor 代码完成,但**已从 UI 移除/搁置**(散点 projX/Y 真实 CRS 未确认,无法与 lat/lon 帘面配准)。**CRS 确认后再回归**。
> - **Task 3(散点#17 + 异常叠加)**:⬜ 未做(P4 下次) > - **Task 3(散点#17 + 异常叠加)**:🔶 **散点#17 ✅ 已完成**(2026-06-08,`ScatterActor` 离屏 PNG 核对吻合 Python 真值 ref_17,接入数据详情「反演剖面/原数据」切换,+2 单测,全 31 测试绿;app 待人工登录复核)。**异常叠加 ⬜ 未做(下一项)**
> - **Task 4(DEM/影像地形)**:⬜ 未做(P4 下次,需先确认 CRS + 加 gdal)。 > - **Task 4(DEM/影像地形)**:⬜ 未做(P4 下次,需先确认 CRS + 加 gdal)。
> 渲染必须用 `tests/spike/render_verify.cpp` 离屏 PNG 核对(本会话教训)。 > 渲染必须用 `tests/spike/render_verify.cpp` 离屏 PNG 核对(本会话教训)。
@ -49,7 +49,7 @@
**Files:** `src/render/actors/ScatterActor.{hpp,cpp}`、`src/render/actors/AnomalyActor.{hpp,cpp}`、`main.cpp` **Files:** `src/render/actors/ScatterActor.{hpp,cpp}`、`src/render/actors/AnomalyActor.{hpp,cpp}`、`main.cpp`
- [ ] `ScatterActor::build(const core::ScatterField&, const core::ColorScale&)`→`vtkActor`:`vtkPolyData`(verts,点用 x/y/(z))+ 标量 v + LUT;点大小适中。(#17 倒三角彩色散点) - [x] `render::buildScatter(const core::ScatterField&, const core::ColorScale&, float pointSize)`→`vtkActor`:`vtkPolyData`(verts,点 x/-y/0,与 #18 同坐标系)+ 点标量 v + LUT(色阶范围优先 colorBar 真实分段值);方块点 SetPointSize。色阶用散点自带文件(`loadScatterColorScale`)。离屏 `verify_scatter.png` 核对吻合 ref_17。
- [ ] `AnomalyActor::build(const std::vector<core::Anomaly>&)`→`vtkActor`(或多 actor):按 markType——点(`vtkGlyph3D`)/线(polydata,dashed,lineColor/width)/面(`vtkPolygon`填充+边);坐标用 localPts。叠加在剖面上。 - [ ] `AnomalyActor::build(const std::vector<core::Anomaly>&)`→`vtkActor`(或多 actor):按 markType——点(`vtkGlyph3D`)/线(polydata,dashed,lineColor/width)/面(`vtkPolygon`填充+边);坐标用 localPts。叠加在剖面上。
- [ ] main.cpp:数据集详情可切换"网格/散点"显示;异常按需叠加。**人工验证**:散点图 ~#17;异常虚线叠在剖面上。提交 `feat(render): 散点(#17) + 异常叠加` - [ ] main.cpp:数据集详情可切换"网格/散点"显示;异常按需叠加。**人工验证**:散点图 ~#17;异常虚线叠在剖面上。提交 `feat(render): 散点(#17) + 异常叠加`

View File

@ -4,8 +4,10 @@
// 二维地图 = 对每个勾选数据集 buildSurveyLinelat/lon 红线俯视z=0+ applyTop2D浅底背景 // 二维地图 = 对每个勾选数据集 buildSurveyLinelat/lon 红线俯视z=0+ applyTop2D浅底背景
// 三维视图 = 对每个勾选数据集 buildCurtain竖直断面墙actor SetScale(1,1,3) 纵向夸张 + applyFree3D白底 // 三维视图 = 对每个勾选数据集 buildCurtain竖直断面墙actor SetScale(1,1,3) 纵向夸张 + applyFree3D白底
// 切视图 / 勾选变化 → 按当前勾选集重建对应内容。 // 切视图 / 勾选变化 → 按当前勾选集重建对应内容。
// - 下方「数据详情」:独立 QVTK 小视图。单击某 DS → 显示该数据集平面反演剖面(#18 banded 等值面+等值线, // - 下方「数据详情」:独立 QVTK 小视图 + 工具条「反演剖面 / 原数据」切换。单击某 DS → 显示该数据集:
// 两 actor SetScale(1,1.5,1) 纵向夸张,平躺俯视正交)+ 属性。 // 反演剖面 = #18 banded 等值面+等值线(两 actor SetScale(1,1.5,1) 纵向夸张)。
// 原数据 = #17 彩色散点buildScatterx=距离/y=深度,按散点自带色阶上色)。
// 两者皆平躺俯视正交 + 属性。
// - 右 属性:选中数据集属性文本。 // - 右 属性:选中数据集属性文本。
// 世界系:启动 loadGrid("grid1") 取一次,用其 lat/lon 中位/均值作 GeoLocalFrame全项目共享保证多视图配准 // 世界系:启动 loadGrid("grid1") 取一次,用其 lat/lon 中位/均值作 GeoLocalFrame全项目共享保证多视图配准
@ -43,6 +45,7 @@
#include "actors/CurtainActor.hpp" #include "actors/CurtainActor.hpp"
#include "actors/GridContourActor.hpp" #include "actors/GridContourActor.hpp"
#include "actors/MapLineActor.hpp" #include "actors/MapLineActor.hpp"
#include "actors/ScatterActor.hpp"
#include "geo/GeoLocalFrame.hpp" #include "geo/GeoLocalFrame.hpp"
@ -108,6 +111,12 @@ double median(std::vector<double> v)
// 当前中央视图(默认二维地图)。二维地图=测线红线俯视;三维视图=断面墙。 // 当前中央视图(默认二维地图)。二维地图=测线红线俯视;三维视图=断面墙。
enum class ViewMode { Map2D, View3D }; enum class ViewMode { Map2D, View3D };
// 数据详情显示内容(默认反演剖面)。反演剖面=#18 banded原数据=#17 散点。
enum class DetailMode { Section18, Scatter17 };
// #17 散点屏幕像素方块边长。
constexpr float kScatterPointSize = 4.0F;
// 纵向夸张倍数:三维断面墙沿 z 拉伸成墙;数据详情 #18 沿 y 拉伸填面板。 // 纵向夸张倍数:三维断面墙沿 z 拉伸成墙;数据详情 #18 沿 y 拉伸填面板。
constexpr double kCurtainZScale = 3.0; constexpr double kCurtainZScale = 3.0;
constexpr double kDetailYScale = 1.5; constexpr double kDetailYScale = 1.5;
@ -176,8 +185,27 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
vtkRenderer* detailRendererPtr = detailRenderer.Get(); vtkRenderer* detailRendererPtr = detailRenderer.Get();
vtkGenericOpenGLRenderWindow* detailRenderWindowPtr = detailRenderWindow.Get(); vtkGenericOpenGLRenderWindow* detailRenderWindowPtr = detailRenderWindow.Get();
// 数据详情容器:顶部「反演剖面/原数据」工具条 + 下方 QVTK 小视图。
auto* detailContainer = new QWidget();
auto* detailLayout = new QVBoxLayout(detailContainer);
detailLayout->setContentsMargins(0, 0, 0, 0);
detailLayout->setSpacing(0);
auto* detailToolBar = new QToolBar();
auto* detailGroup = new QActionGroup(detailToolBar);
detailGroup->setExclusive(true);
auto* actSection = detailToolBar->addAction(QStringLiteral("反演剖面"));
auto* actScatter = detailToolBar->addAction(QStringLiteral("原数据"));
actSection->setCheckable(true);
actScatter->setCheckable(true);
detailGroup->addAction(actSection);
detailGroup->addAction(actScatter);
actSection->setChecked(true); // 默认反演剖面 (#18)
detailLayout->addWidget(detailToolBar);
detailLayout->addWidget(detailWidget, 1);
auto* detailDock = new ads::CDockWidget(QStringLiteral("数据详情")); auto* detailDock = new ads::CDockWidget(QStringLiteral("数据详情"));
detailDock->setWidget(detailWidget); detailDock->setWidget(detailContainer);
// 放在中央视图下方。 // 放在中央视图下方。
dockManager->addDockWidget(ads::BottomDockWidgetArea, detailDock, centerDockArea); dockManager->addDockWidget(ads::BottomDockWidgetArea, detailDock, centerDockArea);
@ -252,24 +280,25 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
rebuildCentral(); rebuildCentral();
}); });
// ── 单击 DS → 下方数据详情显示平面反演剖面 + 右侧属性(与勾选区分;不改帘面可见性)── // ── 数据详情共享状态 + 重建 ──────────────────────────────────────────
QObject::connect( // 当前选中数据集 id空=未选)与详情显示模式(反演剖面/原数据);切模式或换选中都重建。
tree, &QTreeWidget::itemClicked, tree, auto currentDsId = std::make_shared<QString>();
[&repo, propLabel, detailRendererPtr, detailRenderWindowPtr](QTreeWidgetItem* item, int) { auto detailMode = std::make_shared<DetailMode>(DetailMode::Section18);
const QString dsId = item->data(0, kRoleDsId).toString();
if (dsId.isEmpty()) return; // GS/TM 节点无详情
const QString ddType = item->data(0, kRoleDdType).toString();
const QString name = item->text(0);
if (ddType != "dd_section") return;
const std::string id = dsId.toStdString(); // 按当前选中 DS + 详情模式重建下方数据详情(平躺俯视正交,纵向夸张填面板)。
auto rebuildDetail = [&repo, detailRendererPtr, detailRenderWindowPtr, currentDsId,
detailMode]() {
detailRendererPtr->RemoveAllViewProps();
if (currentDsId->isEmpty()) { // 未选数据集:清空即可
detailRenderWindowPtr->Render();
return;
}
const std::string id = currentDsId->toStdString();
if (*detailMode == DetailMode::Section18) {
// 反演剖面:#18 banded 等值面 + 等值线,两 actor 纵向夸张 1.5x(沿 y
const auto g = repo.loadGrid(id); const auto g = repo.loadGrid(id);
const auto cs = repo.loadColorScale(id); const auto cs = repo.loadColorScale(id);
// 下方数据详情:平面反演剖面(#18 banded 等值面 + 等值线),平躺俯视正交。
// 两个 actor 都纵向夸张 1.5x(沿 y填满面板。
const auto actors = geopro::render::buildGridContour(g, cs); const auto actors = geopro::render::buildGridContour(g, cs);
detailRendererPtr->RemoveAllViewProps();
if (actors.bands) { if (actors.bands) {
actors.bands->SetScale(1.0, kDetailYScale, 1.0); actors.bands->SetScale(1.0, kDetailYScale, 1.0);
detailRendererPtr->AddViewProp(actors.bands); detailRendererPtr->AddViewProp(actors.bands);
@ -278,17 +307,54 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
actors.edges->SetScale(1.0, kDetailYScale, 1.0); actors.edges->SetScale(1.0, kDetailYScale, 1.0);
detailRendererPtr->AddViewProp(actors.edges); detailRendererPtr->AddViewProp(actors.edges);
} }
} else {
// 原数据:#17 彩色散点,用散点自带色阶;纵向夸张同剖面以对齐观感。
const auto s = repo.loadScatter(id);
const auto scs = repo.loadScatterColorScale(id);
auto a = geopro::render::buildScatter(s, scs, kScatterPointSize);
if (a) {
a->SetScale(1.0, kDetailYScale, 1.0);
detailRendererPtr->AddViewProp(a);
}
}
geopro::render::applyTop2D(detailRendererPtr); geopro::render::applyTop2D(detailRendererPtr);
detailRendererPtr->ResetCamera(); detailRendererPtr->ResetCamera();
detailRenderWindowPtr->Render(); detailRenderWindowPtr->Render();
};
// 右侧属性。 // ── 单击 DS → 记选中 + 重建数据详情 + 右侧属性(与勾选区分;不改帘面可见性)──
QObject::connect(
tree, &QTreeWidget::itemClicked, tree,
[&repo, propLabel, currentDsId, rebuildDetail](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();
if (ddType != "dd_section") return;
const QString name = item->text(0);
*currentDsId = dsId;
rebuildDetail();
// 右侧属性(数据集级,与详情模式无关)。
const auto g = repo.loadGrid(dsId.toStdString());
propLabel->setText( propLabel->setText(
QStringLiteral("数据集: %1\n类型: 剖面网格 (dd_section)\n网格: %2 x %3\n" QStringLiteral("数据集: %1\n类型: 剖面网格 (dd_section)\n网格: %2 x %3\n"
"vmin / vmax: %4 / %5") "vmin / vmax: %4 / %5")
.arg(name).arg(g.nx()).arg(g.ny()).arg(g.vmin).arg(g.vmax)); .arg(name).arg(g.nx()).arg(g.ny()).arg(g.vmin).arg(g.vmax));
}); });
// ── 数据详情工具条「反演剖面/原数据」:切模式 → 重建数据详情 ──
QObject::connect(actSection, &QAction::triggered, detailWidget,
[detailMode, rebuildDetail]() {
*detailMode = DetailMode::Section18;
rebuildDetail();
});
QObject::connect(actScatter, &QAction::triggered, detailWidget,
[detailMode, rebuildDetail]() {
*detailMode = DetailMode::Scatter17;
rebuildDetail();
});
// ── 工具条「二维地图/三维视图」:切换互斥视图 → 按当前勾选集重建对应内容 ── // ── 工具条「二维地图/三维视图」:切换互斥视图 → 按当前勾选集重建对应内容 ──
QObject::connect(act2D, &QAction::triggered, vtkWidget, [viewMode, rebuildCentral]() { QObject::connect(act2D, &QAction::triggered, vtkWidget, [viewMode, rebuildCentral]() {
*viewMode = ViewMode::Map2D; *viewMode = ViewMode::Map2D;

View File

@ -20,6 +20,7 @@ constexpr const char* kDsId = "grid1";
constexpr const char* kGridFile = u8"剖面网格数据1.txt"; constexpr const char* kGridFile = u8"剖面网格数据1.txt";
constexpr const char* kColorScaleFile = u8"剖面网格数据的色阶数据1.txt"; constexpr const char* kColorScaleFile = u8"剖面网格数据的色阶数据1.txt";
constexpr const char* kScatterFile = u8"剖面原数据1.txt"; constexpr const char* kScatterFile = u8"剖面原数据1.txt";
constexpr const char* kScatterColorScaleFile = u8"剖面原数据的色阶数据1.txt";
// dd_voxel两条交叉剖面散点约 78° 相交),合并为体素插值输入。 // dd_voxel两条交叉剖面散点约 78° 相交),合并为体素插值输入。
constexpr const char* kVoxelScatterFile1 = u8"剖面原数据1.txt"; constexpr const char* kVoxelScatterFile1 = u8"剖面原数据1.txt";
constexpr const char* kVoxelScatterFile2 = u8"剖面原数据2.txt"; constexpr const char* kVoxelScatterFile2 = u8"剖面原数据2.txt";
@ -93,6 +94,11 @@ std::vector<Anomaly> LocalSampleRepository::loadAnomalies(const std::string& dsI
return parseAnomalies(readFile(kAnomalyFile)); return parseAnomalies(readFile(kAnomalyFile));
} }
ColorScale LocalSampleRepository::loadScatterColorScale(const std::string& dsId) {
requireKnownDs(dsId);
return parseColorScale(readFile(kScatterColorScaleFile));
}
std::vector<ScatterField> LocalSampleRepository::loadVoxelScatters() { std::vector<ScatterField> LocalSampleRepository::loadVoxelScatters() {
std::vector<ScatterField> out; std::vector<ScatterField> out;
out.push_back(parseScatter(readFile(kVoxelScatterFile1))); out.push_back(parseScatter(readFile(kVoxelScatterFile1)));

View File

@ -17,6 +17,10 @@ public:
geopro::core::ColorScale loadColorScale(const std::string& dsId) override; geopro::core::ColorScale loadColorScale(const std::string& dsId) override;
std::vector<geopro::core::Anomaly> loadAnomalies(const std::string& dsId) override; std::vector<geopro::core::Anomaly> loadAnomalies(const std::string& dsId) override;
// 散点原数据(#17)专属色阶:剖面原数据自带的色阶文件(与网格色阶不同:分段值范围更大)。
// 具体类专有方法(不进 IDatasetRepository 接口)散点点为不透明alpha 量纲差异无影响。
geopro::core::ColorScale loadScatterColorScale(const std::string& dsId);
// dd_voxel 输入读两条交叉剖面散点剖面原数据1.txt + 剖面原数据2.txt // dd_voxel 输入读两条交叉剖面散点剖面原数据1.txt + 剖面原数据2.txt
// 返回两者,供体素插值合并。具体类专有方法(不进 IDatasetRepository 接口)。 // 返回两者,供体素插值合并。具体类专有方法(不进 IDatasetRepository 接口)。
std::vector<geopro::core::ScatterField> loadVoxelScatters(); std::vector<geopro::core::ScatterField> loadVoxelScatters();

View File

@ -1,6 +1,6 @@
find_package(VTK REQUIRED COMPONENTS CommonCore CommonDataModel FiltersGeometry FiltersModeling RenderingCore RenderingOpenGL2 RenderingVolumeOpenGL2 InteractionStyle InteractionWidgets) find_package(VTK REQUIRED COMPONENTS CommonCore CommonDataModel FiltersGeometry FiltersModeling RenderingCore RenderingOpenGL2 RenderingVolumeOpenGL2 InteractionStyle InteractionWidgets)
add_library(geopro_render STATIC add_library(geopro_render STATIC
Scene.cpp ColorLutBuilder.cpp CameraPreset.cpp actors/GridContourActor.cpp actors/VoxelActor.cpp actors/CurtainActor.cpp actors/MapLineActor.cpp) Scene.cpp ColorLutBuilder.cpp CameraPreset.cpp actors/GridContourActor.cpp actors/VoxelActor.cpp actors/CurtainActor.cpp actors/MapLineActor.cpp actors/ScatterActor.cpp)
target_include_directories(geopro_render PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(geopro_render PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(geopro_render PUBLIC geopro_core ${VTK_LIBRARIES}) target_link_libraries(geopro_render PUBLIC geopro_core ${VTK_LIBRARIES})
target_compile_features(geopro_render PUBLIC cxx_std_17) target_compile_features(geopro_render PUBLIC cxx_std_17)

View File

@ -0,0 +1,87 @@
#include "actors/ScatterActor.hpp"
#include <cstddef>
#include <vector>
#include <vtkCellArray.h>
#include <vtkDoubleArray.h>
#include <vtkNew.h>
#include <vtkPointData.h>
#include <vtkPoints.h>
#include <vtkPolyData.h>
#include <vtkPolyDataMapper.h>
#include <vtkProperty.h>
#include "ColorLutBuilder.hpp"
namespace geopro::render {
namespace {
// LUT 级数。
constexpr int kLutLevels = 256;
} // namespace
vtkSmartPointer<vtkActor> buildScatter(const geopro::core::ScatterField& s,
const geopro::core::ColorScale& cs,
float pointSize)
{
const std::size_t n = s.v.size();
// 退化输入:无值或 x/y 长度不足 → 空 actor(调用方仍可安全 addmapper 无输入则不绘制)。
if (n == 0 || s.x.size() < n || s.y.size() < n) {
return vtkSmartPointer<vtkActor>::New();
}
vtkNew<vtkPoints> points;
points->SetNumberOfPoints(static_cast<vtkIdType>(n));
vtkNew<vtkCellArray> verts; // 每点一个 vtkVertex(GL_POINTS),渲染为方块点
vtkNew<vtkDoubleArray> sc;
sc->SetName("v");
sc->SetNumberOfTuples(static_cast<vtkIdType>(n));
for (std::size_t i = 0; i < n; ++i) {
const auto id = static_cast<vtkIdType>(i);
// y=深度(越大越深)→取负,使深部在下、剖面不倒置(与 GridContourActor 一致)。
points->SetPoint(id, s.x[i], -s.y[i], 0.0);
verts->InsertNextCell(1, &id);
sc->SetValue(id, s.v[i]);
}
vtkNew<vtkPolyData> poly;
poly->SetPoints(points);
poly->SetVerts(verts);
poly->GetPointData()->SetScalars(sc);
// 色阶范围:优先 colorBar 真实分段值(非均匀),否则回退到 v 实测范围。
const std::vector<double> stops = cs.stopValues();
double vmin, vmax;
if (stops.size() >= 2) {
vmin = stops.front();
vmax = stops.back();
} else {
vmin = s.v.front();
vmax = vmin;
for (double v : s.v) {
if (v < vmin) vmin = v;
if (v > vmax) vmax = v;
}
if (vmin >= vmax) vmax = vmin + 1.0;
}
auto lut = buildLut(cs, vmin, vmax, kLutLevels);
vtkNew<vtkPolyDataMapper> mapper;
mapper->SetInputData(poly);
mapper->SetScalarModeToUsePointData();
mapper->SetLookupTable(lut);
mapper->SetScalarRange(vmin, vmax);
auto actor = vtkSmartPointer<vtkActor>::New();
actor->SetMapper(mapper);
actor->GetProperty()->SetPointSize(pointSize);
return actor;
}
} // namespace geopro::render

View File

@ -0,0 +1,18 @@
#pragma once
#include <vtkActor.h>
#include <vtkSmartPointer.h>
#include "model/ColorScale.hpp"
#include "model/Field.hpp"
namespace geopro::render {
// 剖面原数据散点(#17):把 ScatterField 的每个采样点画成按 v 着色的方块点。
// 坐标与数据详情 #18 一致x=xlist(距离)、y=-ylist(深度向下)、z=0(face-on 俯视读)。
// pointSize 为屏幕像素方块边长。色阶范围优先取 colorBar 真实分段值(非均匀)
// 否则回退到 v 的实际 min/max。退化输入(无点)返回空 actor调用方可安全 add。
vtkSmartPointer<vtkActor> buildScatter(const geopro::core::ScatterField& s,
const geopro::core::ColorScale& cs,
float pointSize = 4.0F);
} // namespace geopro::render

View File

@ -64,6 +64,8 @@ target_sources(geopro_tests PRIVATE render/test_color_lut.cpp)
target_sources(geopro_tests PRIVATE render/test_voxel_build.cpp) target_sources(geopro_tests PRIVATE render/test_voxel_build.cpp)
# CurtainbuildCurtain(Grid+GeoLocalFrame->vtkStructuredGrid 帘面) actor + =nx*ny # CurtainbuildCurtain(Grid+GeoLocalFrame->vtkStructuredGrid 帘面) actor + =nx*ny
target_sources(geopro_tests PRIVATE render/test_curtain.cpp) target_sources(geopro_tests PRIVATE render/test_curtain.cpp)
# Scatter(#17)buildScatter(ScatterField+ColorScale->vtkPolyData 彩色散点) /verts//y
target_sources(geopro_tests PRIVATE render/test_scatter.cpp)
target_link_libraries(geopro_tests PRIVATE geopro_render ${VTK_LIBRARIES}) target_link_libraries(geopro_tests PRIVATE geopro_render ${VTK_LIBRARIES})
vtk_module_autoinit(TARGETS geopro_tests MODULES ${VTK_LIBRARIES}) vtk_module_autoinit(TARGETS geopro_tests MODULES ${VTK_LIBRARIES})

View File

@ -1,8 +1,7 @@
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <vtkDataSet.h> #include <vtkPolyData.h>
#include <vtkDataSetMapper.h> #include <vtkPolyDataMapper.h>
#include <vtkMapper.h>
#include "actors/CurtainActor.hpp" #include "actors/CurtainActor.hpp"
#include "geo/GeoLocalFrame.hpp" #include "geo/GeoLocalFrame.hpp"
@ -11,7 +10,8 @@
using namespace geopro::core; using namespace geopro::core;
// buildCurtain: 小 Grid(3,2) + lat/lon/y/values -> 非空 actormapper 输入点数=6。 // buildCurtain: 小 Grid(3,2) + lat/lon/y/values -> 非空 actor经 banded contour 管线
// (vtkDataSetSurfaceFilter→vtkBandedPolyDataContourFilter→vtkPolyDataMapper) 产出非空 polydata。
TEST(Curtain, BuildsStructuredGridFromSmallGrid) { TEST(Curtain, BuildsStructuredGridFromSmallGrid) {
Grid g(3, 2); // nx=3 (lat/lon 列), ny=2 (深度行) Grid g(3, 2); // nx=3 (lat/lon 列), ny=2 (深度行)
g.lat = {22.50, 22.50, 22.50}; g.lat = {22.50, 22.50, 22.50};
@ -32,9 +32,14 @@ TEST(Curtain, BuildsStructuredGridFromSmallGrid) {
auto actor = geopro::render::buildCurtain(g, cs, frame); auto actor = geopro::render::buildCurtain(g, cs, frame);
ASSERT_NE(actor.GetPointer(), nullptr); ASSERT_NE(actor.GetPointer(), nullptr);
auto* mapper = vtkDataSetMapper::SafeDownCast(actor->GetMapper()); auto* mapper = vtkPolyDataMapper::SafeDownCast(actor->GetMapper());
ASSERT_NE(mapper, nullptr); ASSERT_NE(mapper, nullptr);
auto* input = mapper->GetInput(); ASSERT_NE(mapper->GetLookupTable(), nullptr);
ASSERT_NE(input, nullptr);
EXPECT_EQ(input->GetNumberOfPoints(), 6); // 运行管线,断言 banded contour 产出非空 polydata色带面
mapper->Update();
auto* out = mapper->GetInput();
ASSERT_NE(out, nullptr);
EXPECT_GT(out->GetNumberOfPoints(), 0);
EXPECT_GT(out->GetNumberOfCells(), 0);
} }

View File

@ -0,0 +1,52 @@
#include <gtest/gtest.h>
#include <vtkPointData.h>
#include <vtkPolyData.h>
#include <vtkPolyDataMapper.h>
#include "actors/ScatterActor.hpp"
#include "model/ColorScale.hpp"
#include "model/Field.hpp"
using namespace geopro::core;
// buildScatter: 小散点场 -> 非空 actorpolydata 点数=点数、含 verts、按 v 上色(LUT)。
TEST(Scatter, BuildsColoredPointsFromScatterField) {
ScatterField s;
s.x = {0.0, 10.0, 20.0};
s.y = {1.0, 2.0, 3.0}; // 深度,渲染时取负
s.v = {0.0, 2.5, 5.0};
ColorScale cs;
cs.addStop(0.0, Rgba{0, 0, 255, 255});
cs.addStop(5.0, Rgba{255, 0, 0, 255});
auto actor = geopro::render::buildScatter(s, cs);
ASSERT_NE(actor.GetPointer(), nullptr);
auto* mapper = vtkPolyDataMapper::SafeDownCast(actor->GetMapper());
ASSERT_NE(mapper, nullptr);
ASSERT_NE(mapper->GetLookupTable(), nullptr);
auto* poly = vtkPolyData::SafeDownCast(mapper->GetInput());
ASSERT_NE(poly, nullptr);
EXPECT_EQ(poly->GetNumberOfPoints(), 3);
EXPECT_EQ(poly->GetNumberOfVerts(), 3); // 每点一个 vtkVertex
ASSERT_NE(poly->GetPointData()->GetScalars(), nullptr);
// y 取负:深度向下(与剖面 #18 一致)。
double p1[3];
poly->GetPoint(1, p1);
EXPECT_DOUBLE_EQ(p1[0], 10.0);
EXPECT_DOUBLE_EQ(p1[1], -2.0);
}
// 退化输入(无值)返回非空 actor 但无 polydata 输入(调用方可安全 add
TEST(Scatter, EmptyFieldYieldsSafeActor) {
ScatterField s; // 空
ColorScale cs;
cs.addStop(0.0, Rgba{0, 0, 255, 255});
auto actor = geopro::render::buildScatter(s, cs);
ASSERT_NE(actor.GetPointer(), nullptr);
EXPECT_EQ(actor->GetMapper(), nullptr); // 未设 mapper
}

View File

@ -20,6 +20,7 @@
#include "actors/CurtainActor.hpp" #include "actors/CurtainActor.hpp"
#include "actors/GridContourActor.hpp" #include "actors/GridContourActor.hpp"
#include "actors/MapLineActor.hpp" #include "actors/MapLineActor.hpp"
#include "actors/ScatterActor.hpp"
#include "geo/GeoLocalFrame.hpp" #include "geo/GeoLocalFrame.hpp"
#include "parse/SampleParsers.hpp" #include "parse/SampleParsers.hpp"
@ -78,6 +79,18 @@ int main() {
render::applyFree3D(ren); render::applyFree3D(ren);
renderToPng(ren, (dir + "verify_curtain_3d.png").c_str(), 700, 500); renderToPng(ren, (dir + "verify_curtain_3d.png").c_str(), 700, 500);
} }
// 4) 散点 (#17) — 剖面原数据彩色方块散点; x=距离, y=深度(取负向下), face-on 俯视
{
core::ScatterField s = data::parseScatter(slurp((dir + "scatter.json").c_str()));
core::ColorScale scs = data::parseColorScale(slurp((dir + "scatter_colorbar.json").c_str()));
auto a = render::buildScatter(s, scs, 4.0F);
vtkNew<vtkRenderer> ren; ren->SetBackground(1, 1, 1);
ren->AddActor(a);
render::applyTop2D(ren);
renderToPng(ren, (dir + "verify_scatter.png").c_str(), 1100, 360);
std::printf("SCATTER pts=%zu\n", s.v.size());
}
std::printf("RENDER_VERIFY_DONE grid=%dx%d lat0=%.5f lon0=%.5f\n", g.nx(), g.ny(), lat0, lon0); std::printf("RENDER_VERIFY_DONE grid=%dx%d lat0=%.5f lon0=%.5f\n", g.nx(), g.ny(), lat0, lon0);
return 0; return 0;
} }