feat(3d): 色阶跨视图同步真源 + 三维体/切片白化与不透明度重做
- 跨视图色阶单一真源 DatasetViewState:2D详情/3D帘面体共用按dsId的色阶,编辑→真源 →各视图实时联动且无信号回环;散点(type1) load-then-save 回写,避免覆盖网格的 lineConfig/层级方案(共享同一条 businessCode="" 后端记录)。 - 色阶两级透明度:ColorScale.globalOpacity 独立存储不烘焙、渲染时与每色 alpha 相乘; 对话框回显真实值、单色独立;properties 补全 lvlSchemeType/logLinesCount/ equalAreaLayerCount,避免整条覆盖写清空;"整体透明度"改名"不透明度"、显示 0~100。 - 切片白化:SetWindowLevel 钉死 [vmin,vmax] + LUT 0号白化槽,哨兵真透明 (tests/spike/slice_alpha_probe.cpp 真 widget 离屏实测);同时纠正切片颜色映射。 - 切片不透明度:与三维体解耦的独立模型(100%/三维体+50%/跟随),默认100%;保存切片建 自己的色阶对象(颜色快照+不透明度),已保存切片走列表右键"色阶"编辑自身。 - 三维体白化:二值 mask 真白化(NoData 排除出插值,符合 ESRI/GDAL/Surfer 标准);改体 色阶改为原地更新传函(不重建image),未保存切片不再被刷掉且跟随改色;GPU 探测+CPU 回退;体不透明度归一为色阶"不透明度"单一控制(去 kMaxOpacity、移除工具条"透"滑块)。 - 持久化:网格视图补 saveColorGradation;DatasetChartDto.parseColorBar 回读 opacity。 详见 docs/superpowers/specs/2026-06-27-inversion-3d-volume-surfer-method-and-gaps.md §7。
This commit is contained in:
parent
c653a659b2
commit
eef8188bcb
|
|
@ -0,0 +1,144 @@
|
|||
# 反演剖面三维体:客户目标方法(Surfer)对照与客户端差距 — 2026-06-27
|
||||
|
||||
> 来源:客户提供的演示视频 `ScreenShot/9f67e80cb823170bb9374d779ec4c0cb.mp4`(Golden Software Surfer,13min)
|
||||
> 与参考成果图 `ScreenShot/Weixin Image_20260627080012_429_2117.png`。
|
||||
> 结论一句话:**客户用的就是"合并点云 + 3D 反距离加权(IDW) + 边界裁剪(Blanking) + 体绘制/等值面"。**
|
||||
> 客户端插值内核已与之一致,差距只在「搜索半径 / 边界裁剪 / 等值面」三点。
|
||||
|
||||
---
|
||||
|
||||
## 0. 背景与一次认知纠偏
|
||||
|
||||
用户反馈:"选多个 dd_inversion_data(江西理工四条井字相交测线)做三维体,得到的是四个剖面各自左右
|
||||
拉伸的薄板,不连成完整体。"
|
||||
|
||||
排查中曾推荐"逐深度层各向异性插值"——看了客户演示视频后**确认是过度设计**。客户没有逐层做,
|
||||
就是**朴素的三维 IDW**(对合并后的整团散点云一次性 3D 网格化)。本文以视频实证为准。
|
||||
|
||||
---
|
||||
|
||||
## 1. 客户目标方法(Surfer 实测,逐帧为证)
|
||||
|
||||
| 步骤 | 操作 | 视频帧(时间戳)证据 |
|
||||
|---|---|---|
|
||||
| 1. 合并点云 | 所有测线/剖面反演单元合并成**一个 XYZC 文件** `combine-e_m_m.xyz`,列= **X, Y, Elevation, Resistivity, Conductivity, Sensitivity** | t48.7:Grid Data 导入对话框,Data Type=**XYZC** |
|
||||
| 2. 三维网格化 | Grid Data → 方法 **Inverse Distance to a Power(IDW,幂次)** → XYZC 直接生成 **3D 网格体**;注脚明示"does not extrapolate beyond the range of data" | t48.7:Gridding Method 选中 IDW;t97.5:Gridding 进度;t195:`.grd has been created` |
|
||||
| 3. 边界裁剪 | 数字化**测区边界多边形**(Base vector / Polyline)→ **Blanking** 把体裁到测区真实足迹 | t292.5:Polygon 工具数字化边界 |
|
||||
| 4. 三维渲染 | 3D View → 3D Grid Volume(`out-BAIHUA.vtk`):**Volume render**(Sliced / Slice count 500 / Tri-linear / Alpha blending / Opacity 80%) + **Isosurface**(阈值 isovalue) + **Image slice**(YZ/Z) | t585:Volume render 属性;t682:Isosurface(isovalue=1794.39);t633:Image slice(YZ) |
|
||||
|
||||
要点:
|
||||
- **真三维 IDW**,对合并点云一次成体(非逐层 2D)。
|
||||
- IDW **不外推到数据范围外**;测区足迹靠 **Blanking 多边形**裁出(参考图那个不规则边界即来源于此)。
|
||||
- 红色异常体 = **等值面抽取**(Isosurface 按阈值)。
|
||||
- 坐标为真实投影坐标 + 高程,可叠地形/影像底图。
|
||||
|
||||
---
|
||||
|
||||
## 2. 客户端现状(已实现部分)
|
||||
|
||||
生产路径:`Api3dRepository::createVolume`(`src/data/api/Api3dRepository.cpp`)
|
||||
→ 把所有选中 ds 的反演单元按测线真实几何配准合并成 `PointSet`
|
||||
→ `buildVolume(pts, cellXY, cellZ, power, maxDist)`(`src/core/algo/VolumeBuilder.cpp`)
|
||||
→ **三维 IDW**(`src/core/algo/IdwInterpolator.cpp`):`maxDist` 外置 NaN 留空。
|
||||
|
||||
即:**「合并点云 + 3D IDW」内核与 Surfer 一致**。参数见 `src/data/repo/VolumeBuildParams.hpp`:
|
||||
`cellXY=1.0, cellZ=0.5, power=2.0, maxDist=4.0`。
|
||||
|
||||
渲染:`VoxelActor`(`src/render/actors/VoxelActor.hpp`)仅 GPU 体绘制(NaN→透明),**无等值面**
|
||||
(`GridContourActor`/`ContourBands` 是 2D 网格等值线,非 3D 等值面)。地形/影像/坐标轴/电极点已有
|
||||
(`TerrainActor`/`TileBasemap`/`AxesActor`/`ElectrodeActor`)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 差距与修复(共 3 点)
|
||||
|
||||
> 不需改插值算法(内核已对);改的是搜索域、裁剪、与等值面。
|
||||
|
||||
### G1. 搜索半径 maxDist 太小 → "四块板"
|
||||
`maxDist=4m` 远小于井字测线间距 → IDW 只填测线 ±4m 管套,线间留空 → 四块薄板。
|
||||
**修复**:把搜索半径放大到覆盖测区(或提供"覆盖全域"选项),对齐 Surfer 默认搜索域行为。
|
||||
|
||||
### G2. 缺边界裁剪(Blanking) → 单纯放大半径只会"变粗"
|
||||
**这是用户观察到"调大 maxDist 只是让体看起来更粗"的真正原因**:没有足迹裁剪,放大半径会把体
|
||||
鼓满整个外接盒 → 粗大臃肿。Surfer 不粗,是因为 Blanking 把体裁到了测区真实多边形足迹。
|
||||
**修复**:加**足迹掩膜**——
|
||||
- 自动:散点平面**凸包 / alpha-shape**(或沿测线 buffer 并集);
|
||||
- 或手动:支持用户数字化/导入边界多边形(对齐 Surfer Blanking)。
|
||||
掩膜外体素整列置空(NaN/透明)。
|
||||
|
||||
### G3. 缺 3D 等值面 → 出不来红色异常体
|
||||
`VoxelActor` 只有体绘制。
|
||||
**修复**:在体上加 **`vtkFlyingEdges3D` / `vtkContourFilter`** 抽等值面,阈值可调(对齐 Surfer Isosurface)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 修复落地顺序
|
||||
|
||||
1. **G1+G2 一起做**(插值搜索域放大 + 足迹掩膜)→ 出满铺、裁到测区足迹的体。这是核心,先做。
|
||||
2. **G3 等值面**(阈值可调)→ 出红色异常体。参考图第二主角,紧接着做。
|
||||
3. 影像底图/坐标轴/电极点复用现有。
|
||||
|
||||
---
|
||||
|
||||
## 5. 必须先和客户对齐的预期(避免"又看起来不对")
|
||||
|
||||
参考图是**密集测网**(顶面可见很多条测线点阵);江西项目只有**四条井字线**。
|
||||
- **形态可复刻**(满铺体 + 等值面 + 影像底图);
|
||||
- 但**框内细节出不来**——参考图的细碎红异常源于密集采样,四条线之间只能给出平滑趋势,
|
||||
等值面会是几个光滑大团。要那种精度需加密测线。
|
||||
|
||||
---
|
||||
|
||||
## 6. 非目标 / 说明
|
||||
|
||||
- 不改 IDW 内核算法本身(已与 Surfer 一致),不引入逐层各向异性(客户未用)。
|
||||
- 各向异性搜索椭球可作为后续可选增强(Surfer 亦为可选项,非默认),本期不做。
|
||||
- Kriging 仍为占位(`VolumeBuildParams::Model::Kriging`,core 未实现),本期不依赖。
|
||||
|
||||
---
|
||||
|
||||
## 7. 续:白化方式之争 + 体绘制边界「梯田」(2026-06-28,branch `fix/3d-volume-blanking-mask`)
|
||||
|
||||
> 本节记录 §1–6 之后这一轮的来龙去脉、技术取舍与权威佐证,供后续决策不再反复。
|
||||
|
||||
### 7.1 前因后果(时间线)
|
||||
|
||||
客户原话:"**我们这个白化确实有问题,填色了,填了蓝色**"——无数据区被填成蓝色,而非 Surfer 那种透明白化。排查分三层、逐个修:
|
||||
|
||||
1. **切片填蓝**:`vtkImagePlaneWidget` 会按【输入标量范围】(含哨兵)自动 window/level,把哨兵顶到 LUT 最低色格(蓝)且不透明。
|
||||
修复:`SliceTool` 钉死 `SetWindowLevel([vmin,vmax])` + `ColorLutBuilder` 预留 0 号"白化槽"(全透明),哨兵(<vmin)钳到该槽即透明。实测证据:`tests/spike/slice_alpha_probe.cpp`(真 widget 离屏渲染 + 回读像素:背板透出=透明成功)。**这条同时纠正了切片颜色映射**(之前 colorbar 被错误拉伸到切片局部范围)。
|
||||
2. **早期"满屏蓝"是误判**:实为 `maxDist=0`(自动覆盖测区,`kDefMaxDist=0.0`)把整个凸包**填实**=真实低值数据(蓝),**不是空值**(见 §3 G1)。日志实证请求体 `"maxDist":0`。
|
||||
3. **三维体渗蓝**:无数据格设哨兵 `vmin-1`、不透明度 0;三线性插值在"哨兵↔真值"交界处插出低值(蓝)且不透明度非零 → 渗一圈蓝。为消除,给体绘制加**二值 mask**(`VoxelActor::makeMaskLike` + `assembleVolume` 用 `vtkGPUVolumeRayCastMapper`+`SetMaskInput`+`SetMaskTypeToBinary`,mask=0 体素被光线投射**硬跳过**)。
|
||||
|
||||
**mask 的副作用 = 用户 2026-06-28 截图的「竖条/梳齿/底边锯齿」**:硬跳过=边界不再三线性插值/羽化。`maxDist=0` 下体填满**凸包足迹**(带斜边多边形),斜边在 1m 规则网格上离散成**体素阶梯(staircase)**。以前(SmartVolumeMapper+哨兵→不透明度0 软消隐)斜边被羽化抹圆、看不出;加 mask 后变成逐体素硬台阶。**即使色阶不透明度=100% 也可见**——因 mask 在不透明度传函【之前】就把体素从光线上删了(两个并行子代理:渲染侧 + 数据/建体侧共同确诊)。
|
||||
|
||||
### 7.2 取舍的本质
|
||||
|
||||
| 方案 | 边界观感 | 数据诚实度 | 误判风险 |
|
||||
|---|---|---|---|
|
||||
| **二值 mask(当前)** | 体素梯田(难看) | ✅ 只画真数据、零假值 | 低(梯田明显是网格对齐的足迹边界,不像地质体) |
|
||||
| 软消隐(哨兵→不透明度0,无 mask) | 平滑 | ❌ 边界是插值出的假值 | **高** |
|
||||
|
||||
### 7.3 「细蓝边」是什么 + 为何【不能】回退软消隐(权威佐证)
|
||||
|
||||
软消隐回退后的"细蓝边" = 真数据格与哨兵格(`vmin-1`)三线性插值出的、**数据里根本不存在的假低阻值**。低阻在物探通常指含水/导电体,这圈蓝出现在测区最外缘会被**误读成"边界存在真实低阻(导电)异常带"**——真有数据分析歧义。
|
||||
|
||||
多个权威来源一致认定"插值进 NoData 产生边缘伪影"是错误做法、应 mask 排除:
|
||||
|
||||
- **ESRI 官方**:双线性/三次插值 "**interpolate incorrectly into the NoData** and background areas... producing **artifacts or black ridges**" —— 我们的"蓝脊"与之同源(哨兵钳到最低色=蓝)。<https://support.esri.com/en-us/knowledge-base/faq-why-do-bilinear-interpolation-and-cubic-convolution-000003271>
|
||||
- **GDAL 官方**:正确做法是把 masked NoData 的 "**weights of contributing source pixels are set to zero to ignore them**" / "will not be used in interpolation"。<https://gdal.org/en/stable/programs/gdalwarp.html>
|
||||
- **rasterio**:bilinear 重采样在 NoData 边界产生 invalid 值(已知 issue #1721)。<https://github.com/rasterio/rasterio/issues/1721>
|
||||
- **Golden Software Surfer**(客户参照工具):NoData "**removed from the neighborhood**",不跨它插值。<https://surferhelp.goldensoftware.com/gridmisc/Blanked_Nodes_Grid_Filter.htm>(定义 <https://surferhelp.goldensoftware.com/glossary/def_blanking.htm>)
|
||||
- **凸包外 = 外推**:"**Extrapolated data is usually meaningless and misleading.**" <https://github.com/fatiando/fatiando/pull/44>、<https://www.spatialanalysisonline.com/HTML/gridding_and_interpolation_met.htm>
|
||||
|
||||
**结论:二值 mask = 业界标准的"把 NoData 排除出插值"做法,是对的**;梯田只是 mask 在斜足迹边界上的网格离散观感(诚实、不误导)。**不应回退软消隐**(=让哨兵参与插值=以上权威明确反对的造假值做法)。
|
||||
|
||||
### 7.4 决策与待办(截至 2026-06-28,本分支未提交)
|
||||
|
||||
- ✅ **保留二值 mask**(数据诚实/合规,符合 ESRI/GDAL/Surfer 标准)。
|
||||
- 梯田若要压平,走**不造假值**的路(二选一,**待用户/客户拍板**):
|
||||
- (a) **细化 XY 网格**:`cellXY` 1m→0.5m,阶梯缩到亚像素;代价:体素×4、耗时 ~3.5s→~14s、内存×4。
|
||||
- (b) **接受梯田**:它诚实、且明显是足迹边界,不会被当成地质体。
|
||||
- 渲染侧本轮其它已落地修复(排查"分层/稠密"时做、确认非主因但保留):GPU 探测+CPU 回退(`setVolumeGpuSupported`)、细采样距离+`UseJittering`、`ScalarOpacityUnitDistance=对角/10`、去 `kMaxOpacity`(改由色阶「不透明度」单一控制、100%=实心)、移除工具条「透」滑块。
|
||||
- **附带缺陷(待修)**:`VoxelGenerateRequest::maxDist` 结构体默认 `4.0`(`src/data/dto/Vtk3dRequests.hpp:18`)与对话框 `kDefMaxDist=0.0`(`VolumeParamsDialog.cpp:34`)不一致——绕过对话框直建会拿到 4m → 退回"四块板/线间空隙"老问题,应统一为 0。
|
||||
- 抽稀空间哈希 `(ix*p1)^(iy*p2)^(iz*p3)`(`VolumeBuilder.cpp` ~146-151)为 XOR 非单射、有碰撞风险(与本症无关,但宜换 `(iz*ny+iy)*nx+ix` 线性键)。
|
||||
|
|
@ -41,6 +41,7 @@ add_executable(geopro_desktop WIN32
|
|||
panels/DescriptionPanel.cpp
|
||||
panels/QuillDelta.cpp
|
||||
panels/chart/RawDataChartView.cpp
|
||||
panels/chart/ColorScaleProperties.cpp
|
||||
panels/chart/InversionFormDialog.cpp
|
||||
panels/chart/ScatterDataOps.cpp
|
||||
panels/chart/SaveAsDialog.cpp
|
||||
|
|
|
|||
|
|
@ -205,11 +205,11 @@ ColorGradientDialog::ColorGradientDialog(const std::vector<Stop>& init, double m
|
|||
// ── 整体透明度滑块(0~1, step 0.01) ───────────────────────────────────
|
||||
{
|
||||
auto* opRow = new QHBoxLayout();
|
||||
opRow->addWidget(new QLabel(QStringLiteral("整体透明度:")));
|
||||
opRow->addWidget(new QLabel(QStringLiteral("不透明度:")));
|
||||
opacitySlider_ = new QSlider(Qt::Horizontal, this);
|
||||
opacitySlider_->setRange(0, 100);
|
||||
opacitySlider_->setValue(static_cast<int>(opacity_ * 100 + 0.5));
|
||||
opacityLabel_ = new QLabel(QString::number(opacity_, 'f', 2), this);
|
||||
opacityLabel_ = new QLabel(QString::number(opacity_ * 100, 'f', 0), this); // 0~100 显示
|
||||
opRow->addWidget(opacitySlider_, 1);
|
||||
opRow->addWidget(opacityLabel_);
|
||||
root->addLayout(opRow);
|
||||
|
|
@ -247,7 +247,7 @@ ColorGradientDialog::ColorGradientDialog(const std::vector<Stop>& init, double m
|
|||
[this](double) { onMinMaxChanged(); });
|
||||
connect(opacitySlider_, &QSlider::valueChanged, this, [this](int v) {
|
||||
opacity_ = v / 100.0;
|
||||
opacityLabel_->setText(QString::number(opacity_, 'f', 2));
|
||||
opacityLabel_->setText(QString::number(v)); // 0~100 显示(内部仍存 0~1)
|
||||
});
|
||||
|
||||
// 配色方案:内置预设打底,再异步拉取后端 .clr 列表(若有 api)。
|
||||
|
|
|
|||
|
|
@ -112,6 +112,7 @@ ColorScaleConfigDialog::ColorScaleConfigDialog(const geopro::core::ColorScale& i
|
|||
resize(560, 420);
|
||||
|
||||
// 用初始色阶的升序断点填模型;空色阶兜底成 vmin/vmax 两端蓝红。
|
||||
globalOpacity_ = init.globalOpacity(); // 回显真实整体透明度(两级第二级),不再硬编码 1
|
||||
for (const auto& [value, color] : init.stops()) rows_.push_back({value, color});
|
||||
if (rows_.empty()) {
|
||||
rows_.push_back({vmin_, geopro::core::Rgba{0, 0, 255, 255}});
|
||||
|
|
@ -354,15 +355,15 @@ void ColorScaleConfigDialog::onColorScheme() {
|
|||
std::vector<GradientEditWidget::Stop> seed;
|
||||
for (const auto& r : rows_) seed.push_back({(r.value - lo) / span, r.color});
|
||||
|
||||
ColorGradientDialog dlg(seed, lo, hi, vmin_, vmax_, samples_, 1.0, tplRepo_, projectId_, this);
|
||||
ColorGradientDialog dlg(seed, lo, hi, vmin_, vmax_, samples_, globalOpacity_, tplRepo_, projectId_,
|
||||
this);
|
||||
if (dlg.exec() != QDialog::Accepted) return;
|
||||
|
||||
const auto grad = dlg.stops();
|
||||
if (grad.size() < 2) return;
|
||||
const double opacity = dlg.opacity();
|
||||
const unsigned char alpha = static_cast<unsigned char>(opacity * 255.0 + 0.5);
|
||||
globalOpacity_ = dlg.opacity(); // 两级第二级:整体透明度单独存,不烘焙进每色 alpha
|
||||
|
||||
// 在新渐变上按各层级位置连续采样回填颜色(复刻 mapColors + addAlphaToColor 整体透明度)。
|
||||
// 在新渐变上按各层级位置连续采样回填颜色(复刻 mapColors,含每色自有 alpha)。
|
||||
auto sampleGrad = [&](double pos) -> geopro::core::Rgba {
|
||||
if (pos <= grad.front().pos) return grad.front().color;
|
||||
if (pos >= grad.back().pos) return grad.back().color;
|
||||
|
|
@ -372,11 +373,8 @@ void ColorScaleConfigDialog::onColorScheme() {
|
|||
const double t = (x1 > x0) ? (pos - x0) / (x1 - x0) : 0.0;
|
||||
return lerp(grad[i].color, grad[i + 1].color, t);
|
||||
};
|
||||
for (auto& r : rows_) {
|
||||
geopro::core::Rgba c = sampleGrad((r.value - lo) / span);
|
||||
if (opacity < 1.0) c.a = alpha; // 整体透明度覆盖 alpha
|
||||
r.color = c;
|
||||
}
|
||||
// 只回填颜色(含每色自有 alpha),整体透明度单独存于 globalOpacity_、渲染时才相乘(两级)。
|
||||
for (auto& r : rows_) r.color = sampleGrad((r.value - lo) / span);
|
||||
rebuildTable();
|
||||
}
|
||||
|
||||
|
|
@ -559,6 +557,7 @@ void ColorScaleConfigDialog::onOpen() {
|
|||
geopro::core::ColorScale ColorScaleConfigDialog::colorScale() const {
|
||||
geopro::core::ColorScale cs;
|
||||
for (const auto& row : rows_) cs.addStop(row.value, row.color); // 内部按 value 升序
|
||||
cs.setGlobalOpacity(globalOpacity_); // 整体透明度独立带出(渲染时与每色 alpha 相乘)
|
||||
return cs;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,11 @@ public:
|
|||
// 线形/标注配置(线形⚙ 编辑后;2D 消费,3D 忽略)。
|
||||
ContourLineConfig lineConfig() const { return lineCfg_; }
|
||||
|
||||
// 层级方案透传字段(复刻原版 properties:保存色阶时一并写回,避免覆盖清空 web 设过的值)。
|
||||
QString lvlSchemeType() const { return lvlSchemeType_; }
|
||||
int logLinesCount() const { return logLinesCount_; }
|
||||
int equalAreaLayerCount() const { return equalAreaLayerCount_; }
|
||||
|
||||
private:
|
||||
struct Row {
|
||||
double value;
|
||||
|
|
@ -70,6 +75,7 @@ private:
|
|||
|
||||
QTableWidget* table_ = nullptr;
|
||||
std::vector<Row> rows_; // 始终按 value 升序维护
|
||||
double globalOpacity_ = 1.0; // 整体透明度(两级第二级,独立存储,不烘焙进 rows_ 的 alpha)
|
||||
double vmin_ = 0.0;
|
||||
double vmax_ = 0.0;
|
||||
std::vector<double> samples_; // 数据原始标量(等积分层 + 直方图)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@
|
|||
#include <vtkNew.h>
|
||||
#include <vtkProp.h>
|
||||
#include <vtkPiecewiseFunction.h>
|
||||
#include <vtkColorTransferFunction.h>
|
||||
#include <vtkGPUVolumeRayCastMapper.h>
|
||||
#include <vtkRenderWindow.h>
|
||||
#include <vtkRenderer.h>
|
||||
#include <vtkVolume.h>
|
||||
|
|
@ -35,20 +37,6 @@
|
|||
namespace geopro::app {
|
||||
|
||||
namespace {
|
||||
// 运行时改某体的最大不透明度:把其不透明度传递函数「最高 x 的点」(=vmax/qmax 处的最大不透明度点)
|
||||
// 的不透明度值设为 maxOpacity。不重建体、不动颜色与留空透明点,实时生效。
|
||||
void applyVolumeOpacity(vtkVolume* v, double maxOpacity) {
|
||||
if (!v || !v->GetProperty()) return;
|
||||
vtkPiecewiseFunction* op = v->GetProperty()->GetScalarOpacity();
|
||||
if (!op) return;
|
||||
const int n = op->GetSize();
|
||||
if (n <= 0) return;
|
||||
double node[4]; // {x, y(opacity), midpoint, sharpness}
|
||||
op->GetNodeValue(n - 1, node);
|
||||
node[1] = maxOpacity;
|
||||
op->SetNodeValue(n - 1, node);
|
||||
}
|
||||
|
||||
// 控制器层枚举 → render 层枚举(保持控制器不依赖 render)。
|
||||
geopro::render::AxesMode toRenderMode(geopro::controller::AxesMode m) {
|
||||
switch (m) {
|
||||
|
|
@ -140,11 +128,9 @@ void VtkSceneView::clear() {
|
|||
|
||||
void VtkSceneView::setVerticalExaggeration(double ve) { verticalExaggeration_ = ve; }
|
||||
|
||||
void VtkSceneView::setVolumeOpacity(double maxOpacity) {
|
||||
volumeOpacity_ = std::clamp(maxOpacity, 0.0, 1.0); // 记为后续新体默认
|
||||
for (auto& kv : volumes_) // 实时更新所有已渲染体(不重建)
|
||||
applyVolumeOpacity(kv.second.volume, volumeOpacity_);
|
||||
if (renderWindow_) renderWindow_->Render();
|
||||
void VtkSceneView::setVolumeOpacity(double /*maxOpacity*/) {
|
||||
// 已退役:体不透明度统一由【色阶「不透明度」】控制(每单位 = 单色alpha × 色阶不透明度,100%=实心)。
|
||||
// 旧工具条「透明度」滑块移除;保留空实现仅为满足接口(无调用方)。
|
||||
}
|
||||
|
||||
void VtkSceneView::addSurveyLine(const geopro::core::Grid& grid) {
|
||||
|
|
@ -186,6 +172,29 @@ void VtkSceneView::addCurtain(const std::string& dsId, const geopro::core::Grid&
|
|||
|
||||
void VtkSceneView::addVolume(const std::string& dsId, const geopro::data::VolumeGrid& vol,
|
||||
const geopro::core::ColorScale& cs) {
|
||||
// 首次建体时一次性探测 GPU 体绘制支持(此刻 widget 已显示、GL 上下文就绪):不支持则全局回退
|
||||
// SmartVolumeMapper(CPU),避免无独显/软件 GL/远程桌面上整个体渲不出(空值仍靠传函透明)。
|
||||
static bool gpuProbed = false;
|
||||
if (!gpuProbed && renderWindow_) {
|
||||
gpuProbed = true;
|
||||
// 关键:addVolume 在普通 Qt 槽里跑,GL 上下文未必 current → 先 MakeCurrent,否则 IsRenderSupported
|
||||
// 误判为不支持、把有独显的机器错误回退到 CPU(体变稠密/分层)。再给真实传函属性供其判定。
|
||||
renderWindow_->MakeCurrent();
|
||||
vtkNew<vtkGPUVolumeRayCastMapper> probe;
|
||||
vtkNew<vtkVolumeProperty> prop;
|
||||
vtkNew<vtkColorTransferFunction> ctf;
|
||||
ctf->AddRGBPoint(0.0, 1, 1, 1);
|
||||
ctf->AddRGBPoint(1.0, 1, 1, 1);
|
||||
vtkNew<vtkPiecewiseFunction> otf;
|
||||
otf->AddPoint(0.0, 0.0);
|
||||
otf->AddPoint(1.0, 1.0);
|
||||
prop->SetColor(ctf);
|
||||
prop->SetScalarOpacity(otf);
|
||||
const bool ok = probe->IsRenderSupported(renderWindow_, prop) != 0;
|
||||
geopro::render::setVolumeGpuSupported(ok);
|
||||
qInfo().noquote() << "[volrender] GPU volume ray cast supported=" << ok
|
||||
<< (ok ? "(GPU+mask 干净白化)" : "(回退 CPU SmartVolumeMapper,边缘有细渗色)");
|
||||
}
|
||||
// 纵向夸张烤进 image 的 z 原点/间距(与帘面 SetScale 同倍,保证纵向一致)。
|
||||
// 用暴露 image 的 buildVoxel 重载:保留 currentVolumeImage_ 供 P3 切片附着(几何含 VE)。
|
||||
vtkSmartPointer<vtkImageData> image;
|
||||
|
|
@ -198,7 +207,6 @@ void VtkSceneView::addVolume(const std::string& dsId, const geopro::data::Volume
|
|||
// picker 命中体、worldPoint 落体内 → nearestSlice 按平面距离选错切片(用户 ④ 串选)。
|
||||
volume->PickableOff();
|
||||
volume->SetVisibility(analysisMode2D_ ? 0 : 1); // 体=3D内容:二维分析下隐藏
|
||||
applyVolumeOpacity(volume, volumeOpacity_); // 套用当前透明度(工具条调过则新体跟随)
|
||||
scene_.addViewProp(volume);
|
||||
dsProps_[dsId].push_back(volume);
|
||||
currentVolumeImage_ = image;
|
||||
|
|
@ -221,6 +229,21 @@ void VtkSceneView::addVolume(const std::string& dsId, const geopro::data::Volume
|
|||
}
|
||||
}
|
||||
|
||||
bool VtkSceneView::updateVolumeColorInPlace(const std::string& dsId,
|
||||
const geopro::core::ColorScale& cs) {
|
||||
auto it = volumes_.find(dsId);
|
||||
if (it == volumes_.end() || !it->second.volume) return false; // 未渲染 → 调用方回退 remove+add
|
||||
// 仅换传函(image 不变)→ 切片基底保持有效、不被关闭。等值面随阈值色变化较小,暂不重抽。
|
||||
geopro::render::updateVolumeColors(it->second.volume, cs, it->second.vmin, it->second.vmax);
|
||||
it->second.cs = cs;
|
||||
currentColorScale_ = cs;
|
||||
// onVolumeChanged → InteractionManager.setVolumeImage(同 image, 新 cs):检测 image 未变 → 不关切片,
|
||||
// 仅更新体色阶并让该体下未保存切片跟随改色(见 InteractionManager::setVolumeImage)。
|
||||
if (onVolumeChanged) onVolumeChanged();
|
||||
if (renderWindow_) renderWindow_->Render();
|
||||
return true;
|
||||
}
|
||||
|
||||
void VtkSceneView::addMapLine(const std::string& dsId, const geopro::data::MapLine& line,
|
||||
double worldZ) {
|
||||
// 2D 足迹:经共享 frame 投影到世界 XY、Z=worldZ。按 dsId 跟踪(与帘面同 dsProps_ → removeDataset 复用)。
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ public:
|
|||
const geopro::core::ColorScale& cs) override;
|
||||
void addVolume(const std::string& dsId, const geopro::data::VolumeGrid& vol,
|
||||
const geopro::core::ColorScale& cs) override;
|
||||
bool updateVolumeColorInPlace(const std::string& dsId,
|
||||
const geopro::core::ColorScale& cs) override;
|
||||
void addMapLine(const std::string& dsId, const geopro::data::MapLine& line,
|
||||
double worldZ) override;
|
||||
void addTerrain(const geopro::data::TerrainPaths& paths) override;
|
||||
|
|
@ -129,7 +131,6 @@ private:
|
|||
std::shared_ptr<geopro::core::GeoLocalFrame> frame_;
|
||||
double zRefElev_;
|
||||
double verticalExaggeration_ = 1.0;
|
||||
double volumeOpacity_ = 0.30; // 三维体体绘制最大不透明度(默认 0.30,工具条可调);新体建好即套用
|
||||
// 是否已按真实剖面 lat/lon 重锚 frame 原点(每次 clear 重置):默认原点取自样本、可能离真实数据
|
||||
// 很远→局部坐标巨大、轴刻度无意义;首个带经纬剖面到达时重锚到其中心,同选择内多剖面共用配准。
|
||||
bool frameAnchoredToData_ = false;
|
||||
|
|
|
|||
|
|
@ -70,10 +70,7 @@ VtkViewToolbar::VtkViewToolbar(QWidget* parent) : QWidget(parent) {
|
|||
viewDirButtons_.push_back(b); // 二维分析下禁用(会改朝向、破坏近俯视锁定)
|
||||
}
|
||||
sep();
|
||||
// ── 段3:透明度(缩放段顶部,放大上面)+ 缩放 / 复位 ──
|
||||
opacityBtn_ = textBtn(QStringLiteral("透"));
|
||||
opacityBtn_->setToolTip(QStringLiteral("三维体透明度"));
|
||||
connect(opacityBtn_, &QToolButton::clicked, this, &VtkViewToolbar::showOpacityPopup);
|
||||
// ── 段3:缩放 / 复位 ──(三维体不透明度已移交色阶「不透明度」,旧「透」滑块退役移除)
|
||||
connect(iconBtn(Glyph::Plus, QStringLiteral("放大")), &QToolButton::clicked, this,
|
||||
&VtkViewToolbar::zoomInRequested);
|
||||
connect(iconBtn(Glyph::Minus, QStringLiteral("缩小")), &QToolButton::clicked, this,
|
||||
|
|
@ -90,37 +87,6 @@ VtkViewToolbar::VtkViewToolbar(QWidget* parent) : QWidget(parent) {
|
|||
"QToolButton:hover{background:{{bg/hover}};color:{{accent/primary}};}"));
|
||||
setFixedWidth(44);
|
||||
adjustSize();
|
||||
|
||||
// 透明度弹出面板(Qt::Popup → 点击外部自动关闭):横向滑块 0~100(%),默认 30(=0.30)。
|
||||
opacityPopup_ = new QWidget(this, Qt::Popup);
|
||||
opacityPopup_->setAttribute(Qt::WA_StyledBackground, true);
|
||||
applyTokenizedStyleSheet(
|
||||
opacityPopup_,
|
||||
QStringLiteral("QWidget{background:{{bg/panel-subtle}};border:1px solid {{border/default}};"
|
||||
"border-radius:8px;}QLabel{border:none;color:{{text/primary}};}"));
|
||||
auto* pl = new QVBoxLayout(opacityPopup_);
|
||||
pl->setContentsMargins(space::kMd, space::kSm, space::kMd, space::kSm);
|
||||
pl->setSpacing(space::kSm);
|
||||
opacityLabel_ = new QLabel(QStringLiteral("透明度 30%"), opacityPopup_);
|
||||
opacitySlider_ = new QSlider(Qt::Horizontal, opacityPopup_);
|
||||
opacitySlider_->setRange(0, 100);
|
||||
opacitySlider_->setValue(30); // 默认 0.30,与体绘制默认一致
|
||||
opacitySlider_->setFixedWidth(scaledPx(160));
|
||||
pl->addWidget(opacityLabel_);
|
||||
pl->addWidget(opacitySlider_);
|
||||
connect(opacitySlider_, &QSlider::valueChanged, this, [this](int v) {
|
||||
opacityLabel_->setText(QStringLiteral("透明度 %1%").arg(v));
|
||||
emit opacityChanged(v / 100.0); // 实时下发
|
||||
});
|
||||
opacityPopup_->adjustSize();
|
||||
}
|
||||
|
||||
void VtkViewToolbar::showOpacityPopup() {
|
||||
if (!opacityPopup_ || !opacityBtn_) return;
|
||||
// 弹在「透」按钮右侧,与按钮顶对齐(全局坐标,Qt::Popup 顶层窗口)。
|
||||
opacityPopup_->move(opacityBtn_->mapToGlobal(QPoint(opacityBtn_->width() + 6, 0)));
|
||||
opacityPopup_->show();
|
||||
opacityPopup_->raise();
|
||||
}
|
||||
|
||||
void VtkViewToolbar::setAnalysisMode2D(bool is2D) {
|
||||
|
|
|
|||
|
|
@ -27,16 +27,9 @@ signals:
|
|||
void zoomInRequested();
|
||||
void zoomOutRequested();
|
||||
void fitRequested(); // 复位=适配
|
||||
void opacityChanged(double maxOpacity); // 三维体透明度滑块(0~1,实时)
|
||||
|
||||
private:
|
||||
void showOpacityPopup(); // 在透明度按钮旁弹出滑块面板
|
||||
|
||||
std::vector<QToolButton*> viewDirButtons_; // 6 向快捷视图按钮:二维分析下禁用
|
||||
QToolButton* opacityBtn_ = nullptr; // 「透」透明度按钮(缩放段顶部)
|
||||
QWidget* opacityPopup_ = nullptr; // 弹出滑块面板(Qt::Popup,点外即关)
|
||||
QSlider* opacitySlider_ = nullptr; // 0~100(% → 0~1)
|
||||
QLabel* opacityLabel_ = nullptr; // 「透明度 N%」读数
|
||||
};
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@
|
|||
#include "ImportDatasetDialog.hpp"
|
||||
#include "panels/web/ProjectWebView.hpp"
|
||||
#include "WorkbenchNavController.hpp"
|
||||
#include "DatasetViewState.hpp"
|
||||
#include "VtkSceneController.hpp"
|
||||
#include "VtkSceneView.hpp"
|
||||
#include "api/NavRequest.hpp"
|
||||
|
|
@ -301,6 +302,10 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
|||
auto* sceneCtrl = new geopro::controller::VtkSceneController(repo, *scene3dRepo, *sceneView,
|
||||
vtkWidget);
|
||||
sceneCtrl->setVerticalExaggeration(kVerticalExaggeration);
|
||||
// 跨视图色阶真源(统一同步机制):2D 详情/3D 帘面/体 共用一份按 dsId 的色阶;编辑→真源→各视图跟随。
|
||||
// parent=vtkWidget → 随窗口销毁清理;须早于详情面板创建以便注入。
|
||||
auto* viewState = new geopro::controller::DatasetViewState(vtkWidget);
|
||||
sceneCtrl->setViewState(viewState);
|
||||
|
||||
// ── P3 切片交互编排(InteractionManager)─────────────────────────────────
|
||||
// interactor 由 QVTK 在 setRenderWindow 后提供(renderWindow->GetInteractor())。
|
||||
|
|
@ -525,9 +530,14 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
|||
}
|
||||
for (const std::string& id : *checkedSliceIds) {
|
||||
geopro::data::I3dSceneRepository::SliceSpec sp;
|
||||
if (scene3dRepo->sliceSpec(id, sp) && interactionMgr->isVolumeRendered(sp.volumeDsId))
|
||||
if (scene3dRepo->sliceSpec(id, sp) && interactionMgr->isVolumeRendered(sp.volumeDsId)) {
|
||||
interactionMgr->showSavedSlice(id, sp.axis, sp.origin, sp.point1, sp.point2,
|
||||
sp.volumeDsId);
|
||||
// 已保存切片用自己的色阶(颜色+不透明度);无则跟随三维体(兜底,showSavedSlice 已用体色阶)。
|
||||
geopro::core::ColorScale scs;
|
||||
if (scene3dRepo->sliceColorScale(id, scs))
|
||||
interactionMgr->setSliceColorScaleByDsId(id, scs);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -623,6 +633,15 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
|||
QAction* aImg = expMenu->addAction(QStringLiteral("图片"));
|
||||
QAction* aDat = expMenu->addAction(QStringLiteral("dat"));
|
||||
menu.addSeparator();
|
||||
// 「不透明度」放入视觉分组(正视图上方),仅对未保存切片显示(已保存切片改不透明度走列表右键
|
||||
// 「色阶」)。颜色映射始终跟随三维体;这里只设总不透明度。
|
||||
QAction *aOp100 = nullptr, *aOpPlus50 = nullptr, *aOpFollow = nullptr;
|
||||
if (interactionMgr->selectedSliceDsId().empty()) {
|
||||
QMenu* opMenu = menu.addMenu(QStringLiteral("不透明度"));
|
||||
aOp100 = opMenu->addAction(QStringLiteral("100%"));
|
||||
aOpPlus50 = opMenu->addAction(QStringLiteral("三维体+50%"));
|
||||
aOpFollow = opMenu->addAction(QStringLiteral("跟随三维体"));
|
||||
}
|
||||
QAction* aFace = menu.addAction(QStringLiteral("正视图"));
|
||||
QAction* aFlip = menu.addAction(QStringLiteral("视图翻转"));
|
||||
QAction* aClose = menu.addAction(QStringLiteral("关闭"));
|
||||
|
|
@ -632,6 +651,23 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
|||
if (chosen == aFace) { interactionMgr->faceSelected(); return; }
|
||||
if (chosen == aFlip) { interactionMgr->flipView(); return; }
|
||||
if (chosen == aClose) { interactionMgr->closeSelected(); return; } // →onSliceClosed→取消列表勾选
|
||||
if (aOp100 && chosen == aOp100) {
|
||||
interactionMgr->setSelectedSliceOpacity(geopro::render::interact::SliceOpacityMode::Full);
|
||||
vtkWidget->update();
|
||||
return;
|
||||
}
|
||||
if (aOpPlus50 && chosen == aOpPlus50) {
|
||||
interactionMgr->setSelectedSliceOpacity(
|
||||
geopro::render::interact::SliceOpacityMode::VolumePlus50);
|
||||
vtkWidget->update();
|
||||
return;
|
||||
}
|
||||
if (aOpFollow && chosen == aOpFollow) {
|
||||
interactionMgr->setSelectedSliceOpacity(
|
||||
geopro::render::interact::SliceOpacityMode::FollowVolume);
|
||||
vtkWidget->update();
|
||||
return;
|
||||
}
|
||||
if (chosen == aAnoPoint || chosen == aAnoLine || chosen == aAnoFace) {
|
||||
// 形态(1点/2线/3面):同时决定绘制工具 mode、a.markType、对话框查平台类型的 remarkSourceType。
|
||||
// core::AnomalyMarkType 与 remarkSourceType 同值(Point=1/Polyline=2/Polygon=3),用一个 shape 贯通。
|
||||
|
|
@ -783,10 +819,14 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
|||
QLineEdit::Normal,
|
||||
QStringLiteral("切片"), &ok);
|
||||
if (!ok) return;
|
||||
// 保存时快照切片自己的色阶对象:颜色继承当时三维体 + 不透明度取当前具体值(并入 globalOpacity)。
|
||||
const geopro::core::ColorScale sliceCs =
|
||||
interactionMgr->selectedSliceColorScaleSnapshot();
|
||||
scene3dRepo->createSlice(
|
||||
spec, name.isEmpty() ? std::string("切片") : name.toStdString(),
|
||||
[interactionMgr, refreshAnalysis, drawer](std::string newId) {
|
||||
[interactionMgr, refreshAnalysis, drawer, scene3dRepo, sliceCs](std::string newId) {
|
||||
interactionMgr->tagSelectedSlice(newId); // 链接当前切片 → 新数据集(不重绘)
|
||||
scene3dRepo->setSliceColorScale(newId, sliceCs); // 存切片独立色阶(mock)
|
||||
refreshAnalysis(); // 新行进列表
|
||||
// 新切片自动勾选 → 列表打勾 + 保持渲染(refreshAnalysis 已重建列表,故在其后勾选)。
|
||||
if (auto* sec = drawer->analysisTab()->section("voxel"))
|
||||
|
|
@ -838,13 +878,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
|||
&geopro::controller::VtkSceneController::zoomOut);
|
||||
QObject::connect(viewToolbar, &geopro::app::VtkViewToolbar::fitRequested, sceneCtrl,
|
||||
&geopro::controller::VtkSceneController::fit);
|
||||
// 透明度滑块 → 运行时调三维体不透明度(实时)。vtkWidget->update() 保证离屏渲染呈现到屏
|
||||
// (滑块在弹出面板上,vtkWidget 自身无 paint 事件,需显式请求重绘,同 volumeRendered 修复)。
|
||||
QObject::connect(viewToolbar, &geopro::app::VtkViewToolbar::opacityChanged, vtkWidget,
|
||||
[sceneCtrl, vtkWidget](double op) {
|
||||
sceneCtrl->setVolumeOpacity(op);
|
||||
vtkWidget->update();
|
||||
});
|
||||
// (三维体不透明度已移交色阶「不透明度」;旧工具条「透」滑块退役。)
|
||||
// 设置(⚙)→ 工具条右侧 toggle 抽屉面板(非模态弹窗)。
|
||||
QObject::connect(viewToolbar, &geopro::app::VtkViewToolbar::axesSettingsRequested, &window,
|
||||
[axesPanel, viewToolbar]() {
|
||||
|
|
@ -1102,8 +1136,30 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
|||
// 色阶(三维体/切片):复刻原版「色阶配置」对话框,确定后体素 + 其切片随新色阶重渲染。
|
||||
// 仅对当前已渲染的三维体生效(切片色阶继承体色阶,经 InteractionManager 重建)。
|
||||
QObject::connect(analysisTab, &geopro::app::CategoryAnalysisTab::colorScaleRequested, &window,
|
||||
[&window, &colorTplRepo, &nav, sceneCtrl, sceneView](const QString& qid) {
|
||||
[&window, &colorTplRepo, &nav, sceneCtrl, sceneView, scene3dRepo,
|
||||
interactionMgr](const QString& qid) {
|
||||
const std::string dsId = qid.toStdString();
|
||||
// 已保存切片(dd_slice)→ 编辑切片自己的色阶(颜色+不透明度),不走三维体路径。
|
||||
if (scene3dRepo->isSliceDataset(dsId)) {
|
||||
geopro::core::ColorScale scs;
|
||||
if (!scene3dRepo->sliceColorScale(dsId, scs)) {
|
||||
QMessageBox::information(&window, QStringLiteral("色阶"),
|
||||
QStringLiteral("该切片暂无独立色阶。"));
|
||||
return;
|
||||
}
|
||||
const auto stops = scs.stopValues();
|
||||
const double vmin = stops.empty() ? 0.0 : stops.front();
|
||||
const double vmax = stops.empty() ? 1.0 : stops.back();
|
||||
geopro::app::ColorScaleConfigDialog dlg(scs, vmin, vmax, {}, {},
|
||||
&colorTplRepo,
|
||||
nav.currentProjectId(), QString(),
|
||||
&window);
|
||||
if (dlg.exec() != QDialog::Accepted) return;
|
||||
const auto newCs = dlg.colorScale();
|
||||
scene3dRepo->setSliceColorScale(dsId, newCs); // 存切片独立色阶(mock)
|
||||
interactionMgr->setSliceColorScaleByDsId(dsId, newCs); // 若在渲染则即时改色
|
||||
return;
|
||||
}
|
||||
// 多体并发:编辑"该体"(任一已渲染体,不限当前体)的色阶。
|
||||
const auto* vol = sceneView->volume(dsId);
|
||||
if (!vol) {
|
||||
|
|
@ -1333,6 +1389,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
|||
// 单击数据集 → 聚焦已开页;双击 → 新建/聚焦页(真实反演剖面/散点/异常/色阶)。
|
||||
auto* detailPanel = new geopro::app::DatasetDetailPanel();
|
||||
// 注入色阶模板仓储 + 当前 projectId 取值回调(网格剖面「色阶配置」编辑器的另存/打开/新建色阶)。
|
||||
detailPanel->setViewState(viewState); // 跨视图色阶真源(2D↔3D 同步)
|
||||
detailPanel->setColorTemplateRepo(&colorTplRepo, [&nav] { return nav.currentProjectId(); });
|
||||
// 注入反演命令仓储(measurement 反演运算/生成视电阻率)。projectId 取值仍由页内 projectIdGetter 提供。
|
||||
detailPanel->setCommandRepo(&cmdRepo);
|
||||
|
|
|
|||
|
|
@ -51,7 +51,8 @@ void DatasetDetailPage::build(const QString& dsId, const QString& ddCode, const
|
|||
// dsIdGetter 用本页 dsId_(此处已赋值),随项目/数据集稳定。
|
||||
auto view = makeDetailView(spec.kind, this, colorTplRepo_, projectIdGetter_, cmdRepo_,
|
||||
[this] { return dsId_; },
|
||||
[this] { return tmObjectId_; }); // 抛出由调用栈兜底(GuardedApplication)
|
||||
[this] { return tmObjectId_; },
|
||||
viewState_); // 抛出由调用栈兜底(GuardedApplication)
|
||||
IDetailView* raw = view.release(); // QWidget 由 this/QwtPlot 父子树接管生命周期
|
||||
views_[i] = raw;
|
||||
// lazy 页签:建覆盖该视图的加载遮罩(父为视图 widget,随其尺寸覆盖图区)。
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ class IColorTemplateRepository;
|
|||
class IDatasetCommandRepository;
|
||||
}
|
||||
|
||||
namespace geopro::controller {
|
||||
class DatasetViewState; // 跨视图色阶真源(统一同步)
|
||||
}
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
class IDetailView;
|
||||
|
|
@ -35,6 +39,9 @@ public:
|
|||
// 所属 TM 对象 id(=白化 structParentId)注入(须在 build 前设置 → tmObjectIdGetter 透传给视图)。
|
||||
void setTmObjectId(const QString& tmObjectId) { tmObjectId_ = tmObjectId; }
|
||||
|
||||
// 跨视图色阶真源注入(须在 build 前设置 → 透传给网格视图,实现 2D↔3D 色阶同步)。
|
||||
void setViewState(geopro::controller::DatasetViewState* state) { viewState_ = state; }
|
||||
|
||||
// 按页签集构建页签(首次打开调一次)。dsId/ddCode/dsName 用于 tabNeeded。
|
||||
void build(const QString& dsId, const QString& ddCode, const QString& dsName,
|
||||
const std::vector<geopro::controller::TabSpec>& tabs);
|
||||
|
|
@ -75,6 +82,8 @@ private:
|
|||
|
||||
// 反演命令仓储注入(透传给 makeDetailView → measurement 散点视图反演按钮)。
|
||||
geopro::data::IDatasetCommandRepository* cmdRepo_ = nullptr;
|
||||
|
||||
geopro::controller::DatasetViewState* viewState_ = nullptr; // 跨视图色阶真源(透传给网格视图)
|
||||
};
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ void DatasetDetailPanel::setCommandRepo(geopro::data::IDatasetCommandRepository*
|
|||
cmdRepo_ = repo;
|
||||
}
|
||||
|
||||
void DatasetDetailPanel::setViewState(geopro::controller::DatasetViewState* state) {
|
||||
viewState_ = state;
|
||||
}
|
||||
|
||||
DatasetDetailPanel::DatasetDetailPanel(QWidget* parent) : QTabWidget(parent) {
|
||||
setTabsClosable(true);
|
||||
connect(this, &QTabWidget::tabCloseRequested, this, [this](int i) { widget(i)->deleteLater(); });
|
||||
|
|
@ -40,6 +44,7 @@ void DatasetDetailPanel::onDatasetOpened(const QString& dsId, const QString& ddC
|
|||
// 注入须在 build 前(build 内造视图时即透传给工厂)。
|
||||
p->setColorTemplateRepo(colorTplRepo_, projectIdGetter_);
|
||||
p->setCommandRepo(cmdRepo_);
|
||||
p->setViewState(viewState_); // 跨视图色阶真源(build 前设置 → 透传给网格视图)
|
||||
p->setTmObjectId(tmObjectId); // 白化 structParentId(build 前设置 → 透传给视图)
|
||||
p->build(dsId, ddCode, dsName, tabs); // ddCode 透传 → 页内 tabNeeded 携带
|
||||
const QString title = dsName.isEmpty() ? dsId : dsName; // 页签标题用数据名(空则回退 id)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ namespace geopro::data {
|
|||
class IColorTemplateRepository;
|
||||
class IDatasetCommandRepository;
|
||||
}
|
||||
namespace geopro::controller {
|
||||
class DatasetViewState; // 跨视图色阶真源(统一同步)
|
||||
}
|
||||
namespace geopro::app {
|
||||
class DatasetDetailPage;
|
||||
|
||||
|
|
@ -25,6 +28,9 @@ public:
|
|||
// 反演命令仓储:透传给每个新建的详情页(measurement 反演运算/生成视电阻率用)。
|
||||
void setCommandRepo(geopro::data::IDatasetCommandRepository* repo);
|
||||
|
||||
// 跨视图色阶真源:透传给每个新建的详情页 → 网格视图(2D↔3D 色阶同步)。
|
||||
void setViewState(geopro::controller::DatasetViewState* state);
|
||||
|
||||
// 数据集打开:find-or-create 页 → build(tabs) → 加/抬该面板页签。
|
||||
// tmObjectId:所属 TM 对象 id(白化 structParentId),build 前交给页 → 视图。
|
||||
void onDatasetOpened(const QString& dsId, const QString& ddCode, const QString& dsName,
|
||||
|
|
@ -51,5 +57,7 @@ private:
|
|||
|
||||
// 反演命令仓储注入(新页 build 前 setCommandRepo 透传)。
|
||||
geopro::data::IDatasetCommandRepository* cmdRepo_ = nullptr;
|
||||
|
||||
geopro::controller::DatasetViewState* viewState_ = nullptr; // 跨视图色阶真源(透传给详情页)
|
||||
};
|
||||
} // namespace geopro::app
|
||||
|
|
|
|||
|
|
@ -11,6 +11,11 @@ inline double clamp01(double v) {
|
|||
inline unsigned char lerpByte(unsigned char a, unsigned char b, double t) {
|
||||
return static_cast<unsigned char>(a + (b - a) * t + 0.5);
|
||||
}
|
||||
// 两级透明度:每色 alpha × 整体透明度(渲染时相乘,不烘焙)。
|
||||
inline core::Rgba applyGlobalAlpha(core::Rgba c, double g) {
|
||||
c.a = static_cast<unsigned char>(c.a * g + 0.5);
|
||||
return c;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ColorMapService::ColorMapService(const core::ColorScale& scale)
|
||||
|
|
@ -47,17 +52,18 @@ double ColorMapService::normalized(double v) const {
|
|||
}
|
||||
|
||||
core::Rgba ColorMapService::colorAtContinuous(double v) const {
|
||||
if (normStops_.empty()) return core::Rgba{0, 0, 0, 255};
|
||||
if (normStops_.size() == 1) return normStops_.front().color;
|
||||
const double g = scale_.globalOpacity(); // 两级第二级:整体透明度
|
||||
if (normStops_.empty()) return applyGlobalAlpha(core::Rgba{0, 0, 0, 255}, g);
|
||||
if (normStops_.size() == 1) return applyGlobalAlpha(normStops_.front().color, g);
|
||||
|
||||
double t = normalized(v);
|
||||
// 非有限值(NaN/Inf,可能来自降级后端的脏数据或退化数据范围):回退首断点色,
|
||||
// 避免下方 upper_bound 用 NaN 比较返回 end() 后解引用越界(崩溃)。
|
||||
if (!std::isfinite(t)) return normStops_.front().color;
|
||||
if (!std::isfinite(t)) return applyGlobalAlpha(normStops_.front().color, g);
|
||||
|
||||
// 找到 t 落在哪两个 normStop 之间
|
||||
if (t <= normStops_.front().pos) return normStops_.front().color;
|
||||
if (t >= normStops_.back().pos) return normStops_.back().color;
|
||||
if (t <= normStops_.front().pos) return applyGlobalAlpha(normStops_.front().color, g);
|
||||
if (t >= normStops_.back().pos) return applyGlobalAlpha(normStops_.back().color, g);
|
||||
|
||||
// 二分查找第一个 pos > t
|
||||
auto it = std::upper_bound(normStops_.begin(), normStops_.end(), t,
|
||||
|
|
@ -68,16 +74,16 @@ core::Rgba ColorMapService::colorAtContinuous(double v) const {
|
|||
double segLen = hi.pos - lo.pos;
|
||||
double frac = (segLen > 0.0) ? (t - lo.pos) / segLen : 0.0;
|
||||
|
||||
return core::Rgba{
|
||||
return applyGlobalAlpha(core::Rgba{
|
||||
lerpByte(lo.color.r, hi.color.r, frac),
|
||||
lerpByte(lo.color.g, hi.color.g, frac),
|
||||
lerpByte(lo.color.b, hi.color.b, frac),
|
||||
lerpByte(lo.color.a, hi.color.a, frac)
|
||||
};
|
||||
}, g);
|
||||
}
|
||||
|
||||
core::Rgba ColorMapService::colorAtDiscrete(double v) const {
|
||||
return scale_.colorAt(v);
|
||||
return applyGlobalAlpha(scale_.colorAt(v), scale_.globalOpacity());
|
||||
}
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
#include "panels/chart/ColorScaleProperties.hpp"
|
||||
|
||||
#include <QJsonArray>
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
QString rgbaToColorBarCss(const geopro::core::Rgba& c) {
|
||||
if (c.a >= 255)
|
||||
return QStringLiteral("#%1%2%3")
|
||||
.arg(c.r, 2, 16, QLatin1Char('0'))
|
||||
.arg(c.g, 2, 16, QLatin1Char('0'))
|
||||
.arg(c.b, 2, 16, QLatin1Char('0'))
|
||||
.toUpper();
|
||||
return QStringLiteral("rgba(%1, %2, %3, %4)")
|
||||
.arg(c.r)
|
||||
.arg(c.g)
|
||||
.arg(c.b)
|
||||
.arg(QString::number(c.a / 255.0, 'g', 3));
|
||||
}
|
||||
|
||||
QJsonObject buildColorScaleProperties(const geopro::core::ColorScale& scale,
|
||||
const ContourLineConfig& lineCfg, const QString& lvlSchemeType,
|
||||
int logLinesCount, int equalAreaLayerCount,
|
||||
bool includeLvlScheme) {
|
||||
QJsonArray colorBar;
|
||||
for (const auto& [value, color] : scale.stops())
|
||||
colorBar.append(QJsonArray{QString::number(value, 'f', 2), rgbaToColorBarCss(color)});
|
||||
QJsonObject lineConfig{
|
||||
{QStringLiteral("showLines"), lineCfg.lineShow},
|
||||
{QStringLiteral("color"), rgbaToColorBarCss(lineCfg.lineColor)},
|
||||
{QStringLiteral("lineType"),
|
||||
lineCfg.dashed ? QStringLiteral("dashed") : QStringLiteral("solid")}};
|
||||
QJsonObject labelConfig{{QStringLiteral("showLabels"), lineCfg.labelShow},
|
||||
{QStringLiteral("color"), rgbaToColorBarCss(lineCfg.labelColor)}};
|
||||
QJsonObject props{{QStringLiteral("colorBar"), colorBar},
|
||||
{QStringLiteral("opacity"), scale.globalOpacity()}, // 两级第二级:整体透明度
|
||||
{QStringLiteral("lineConfig"), lineConfig},
|
||||
{QStringLiteral("labelConfig"), labelConfig}};
|
||||
if (includeLvlScheme) { // 等值面(网格/反演)路径:层级方案透传字段(复刻原版,整条覆盖写须带)
|
||||
props[QStringLiteral("lvlSchemeType")] = lvlSchemeType;
|
||||
props[QStringLiteral("logLinesCount")] = logLinesCount;
|
||||
props[QStringLiteral("equalAreaLayerCount")] = equalAreaLayerCount;
|
||||
}
|
||||
return props;
|
||||
}
|
||||
|
||||
QJsonObject withColorBarAndOpacity(const QJsonObject& base, const geopro::core::ColorScale& scale) {
|
||||
QJsonObject props = base; // 保留 lineConfig/labelConfig/层级方案 等加载到的原值
|
||||
QJsonArray colorBar;
|
||||
for (const auto& [value, color] : scale.stops())
|
||||
colorBar.append(QJsonArray{QString::number(value, 'f', 2), rgbaToColorBarCss(color)});
|
||||
props[QStringLiteral("colorBar")] = colorBar; // 仅覆盖本次编辑的颜色
|
||||
props[QStringLiteral("opacity")] = scale.globalOpacity(); // 与整体不透明度
|
||||
return props;
|
||||
}
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
#pragma once
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
#include "ContourLineDialog.hpp" // ContourLineConfig
|
||||
#include "model/ColorScale.hpp"
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
// core::Rgba → colorBar 颜色串(不透明 #RRGGBB,半透明 rgba(r,g,b,a∈0..1)),与后端 colorBar 互通。
|
||||
QString rgbaToColorBarCss(const geopro::core::Rgba& c);
|
||||
|
||||
// 组装色阶持久化 properties(colorBar[每色含 alpha] + opacity[整体透明度] + lineConfig + labelConfig
|
||||
// + lvlSchemeType/logLinesCount/equalAreaLayerCount[层级方案,复刻原版透传字段])。
|
||||
// 散点/网格共用同一格式(同一条后端记录 businessCode="")。整条 properties 覆盖写,故层级字段必须带,
|
||||
// 否则会清空 web 设过的值。opacity 为两级透明度的第二级。
|
||||
// includeLvlScheme=false:measurement 散点(type3) 路径,不写等值面专属的层级方案字段(对齐原版
|
||||
// scatters「仅发 colorBar/lineConfig/labelConfig」+ 桌面两级 opacity),避免向 R0 记录注入无关字段。
|
||||
QJsonObject buildColorScaleProperties(const geopro::core::ColorScale& scale,
|
||||
const ContourLineConfig& lineCfg,
|
||||
const QString& lvlSchemeType = QStringLiteral("normal"),
|
||||
int logLinesCount = 8, int equalAreaLayerCount = 10,
|
||||
bool includeLvlScheme = true);
|
||||
|
||||
// load-then-save 回写(对齐原版 originPage):在加载到的原始 properties 上【只覆盖 colorBar+opacity】,
|
||||
// 其余字段(lineConfig/labelConfig/层级方案)原样保留,避免散点保存清掉网格(共用同一条记录)的值。
|
||||
QJsonObject withColorBarAndOpacity(const QJsonObject& base, const geopro::core::ColorScale& scale);
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
@ -195,7 +195,7 @@ void ContourPlotItem::buildFillImage(const core::Grid& g, ColorMapService* svc)
|
|||
double v = (v00 * (1 - ti) + v10 * ti) * (1 - tj) +
|
||||
(v01 * (1 - ti) + v11 * ti) * tj;
|
||||
auto c = svc->colorAtDiscrete(v); // 离散色带 → 平滑填充带边界
|
||||
scan[px] = qRgba(c.r, c.g, c.b, c.a ? c.a : 255);
|
||||
scan[px] = qRgba(c.r, c.g, c.b, c.a); // 听色阶 alpha:alpha=0 真透明(无 alpha 色阶默认 255 不受影响)
|
||||
}
|
||||
}
|
||||
fillImage_ = std::move(img);
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ std::unique_ptr<IDetailView> makeDetailView(controller::ViewKind kind, QWidget*
|
|||
std::function<QString()> projectIdGetter,
|
||||
geopro::data::IDatasetCommandRepository* cmdRepo,
|
||||
std::function<QString()> dsIdGetter,
|
||||
std::function<QString()> tmObjectIdGetter) {
|
||||
std::function<QString()> tmObjectIdGetter,
|
||||
geopro::controller::DatasetViewState* viewState) {
|
||||
switch (kind) {
|
||||
case controller::ViewKind::Scatter: {
|
||||
auto* raw = new RawDataChartView(parent);
|
||||
|
|
@ -25,6 +26,8 @@ std::unique_ptr<IDetailView> makeDetailView(controller::ViewKind kind, QWidget*
|
|||
raw->setCommandRepo(cmdRepo, dsIdGetter, projectIdGetter);
|
||||
// 注入色阶模板仓储(散点「色阶配置」编辑器另存为/打开/覆盖用;projectId 复用上面的 getter)。
|
||||
raw->setColorTemplateRepo(colorTplRepo);
|
||||
// 注入跨视图色阶真源(反演原数据 type1 与网格/3D 共用色阶 → 实时联动;measurement 不路由)。
|
||||
raw->setViewState(viewState);
|
||||
return std::unique_ptr<IDetailView>(raw);
|
||||
}
|
||||
case controller::ViewKind::FilledContour: {
|
||||
|
|
@ -35,6 +38,8 @@ std::unique_ptr<IDetailView> makeDetailView(controller::ViewKind kind, QWidget*
|
|||
grid->setCommandRepo(cmdRepo, std::move(dsIdGetter), std::move(projectIdGetter));
|
||||
// 注入 tmObjectId 取值回调(白化对话框模板列表用,= 数据集 structParentId)。
|
||||
grid->setTmObjectIdGetter(std::move(tmObjectIdGetter));
|
||||
// 注入跨视图色阶真源(编辑→真源→3D 帘面/体等跟随;本视图也跟随他视图改色)。
|
||||
grid->setViewState(viewState);
|
||||
return std::unique_ptr<IDetailView>(grid);
|
||||
}
|
||||
case controller::ViewKind::Table: {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ class IColorTemplateRepository;
|
|||
class IDatasetCommandRepository;
|
||||
}
|
||||
|
||||
namespace geopro::controller {
|
||||
class DatasetViewState; // 跨视图色阶真源(统一同步)
|
||||
}
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
class IDetailView;
|
||||
|
|
@ -29,6 +33,7 @@ std::unique_ptr<IDetailView> makeDetailView(
|
|||
std::function<QString()> projectIdGetter = {},
|
||||
geopro::data::IDatasetCommandRepository* cmdRepo = nullptr,
|
||||
std::function<QString()> dsIdGetter = {},
|
||||
std::function<QString()> tmObjectIdGetter = {});
|
||||
std::function<QString()> tmObjectIdGetter = {},
|
||||
geopro::controller::DatasetViewState* viewState = nullptr);
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@
|
|||
#include "panels/chart/AutoAnnotationDialog.hpp"
|
||||
#include "panels/chart/ColorBarWidget.hpp"
|
||||
#include "panels/chart/ColorMapService.hpp"
|
||||
#include "panels/chart/ColorScaleProperties.hpp"
|
||||
#include "DatasetViewState.hpp"
|
||||
#include "panels/chart/ContourDrawTool.hpp"
|
||||
#include "panels/chart/ContourHoverTip.hpp"
|
||||
#include "panels/chart/ContourPlotItem.hpp"
|
||||
|
|
@ -324,7 +326,7 @@ void GridDataChartView::openColorScaleEditor() {
|
|||
tplRepo_, projectId, lvlTemplateId_, this);
|
||||
if (dlg.exec() != QDialog::Accepted) return;
|
||||
|
||||
gridScale_ = dlg.colorScale();
|
||||
const auto cs = dlg.colorScale();
|
||||
lineCfg_ = dlg.lineConfig();
|
||||
showLabels_ = lineCfg_.labelShow; // 标注显隐同步 + 回写工具条复选框(避免 UI 与状态脱钩)
|
||||
if (chkShowLabels_) {
|
||||
|
|
@ -332,10 +334,63 @@ void GridDataChartView::openColorScaleEditor() {
|
|||
chkShowLabels_->setChecked(showLabels_);
|
||||
}
|
||||
|
||||
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
|
||||
// 统一同步:写入色阶真源 → 经 colorScaleChanged 触发本视图(及 3D 帘面/体等)重渲染。
|
||||
// 无状态层(理论不至)才本地兜底重绘。
|
||||
if (state_ && !dsId.isEmpty()) {
|
||||
state_->setColorScale(dsId, cs);
|
||||
} else {
|
||||
gridScale_ = cs;
|
||||
applyColorScaleRender();
|
||||
}
|
||||
persistColorScale(dsId, cs, dlg.lvlSchemeType(), dlg.logLinesCount(),
|
||||
dlg.equalAreaLayerCount()); // 持久化到后端(businessCode="",与散点同一条记录)
|
||||
}
|
||||
|
||||
void GridDataChartView::applyColorScaleRender() {
|
||||
delete colorSvc_;
|
||||
colorSvc_ = new ColorMapService(gridScale_);
|
||||
rebuildContour();
|
||||
colorBar_->setColorScale(gridScale_);
|
||||
if (colorBar_) colorBar_->setColorScale(gridScale_);
|
||||
}
|
||||
|
||||
void GridDataChartView::setViewState(geopro::controller::DatasetViewState* state) {
|
||||
state_ = state;
|
||||
if (!state_) return;
|
||||
connect(state_, &geopro::controller::DatasetViewState::colorScaleChanged, this,
|
||||
&GridDataChartView::onColorScaleChanged);
|
||||
}
|
||||
|
||||
void GridDataChartView::onColorScaleChanged(const QString& dsId) {
|
||||
if (!state_ || !hasGrid_) return;
|
||||
if (!dsIdGetter_ || dsIdGetter_() != dsId) return; // 只跟随本视图所示数据集
|
||||
const auto* cs = state_->colorScale(dsId);
|
||||
if (!cs) return;
|
||||
gridScale_ = *cs;
|
||||
applyColorScaleRender();
|
||||
}
|
||||
|
||||
void GridDataChartView::persistColorScale(const QString& dsId, const geopro::core::ColorScale& cs,
|
||||
const QString& lvlSchemeType, int logLinesCount,
|
||||
int equalAreaLayerCount) {
|
||||
if (!cmdRepo_ || dsId.isEmpty()) return; // 无仓储/无 dsId → 仅本地生效(不阻塞)
|
||||
const QString projectId = projectIdGetter_ ? projectIdGetter_() : QString();
|
||||
// 网格(getDetail type2) 与 反演散点(type1) 共用 businessCode=""(后端按 (dsObjectId,businessCode)
|
||||
// 存唯一记录,save 无 type 字段)。properties 含每色 alpha + 整体透明度 + 层级方案透传字段。
|
||||
QJsonObject body{
|
||||
{QStringLiteral("dsObjectId"), dsId},
|
||||
{QStringLiteral("templateId"), lvlTemplateId_},
|
||||
{QStringLiteral("businessCode"), QString()},
|
||||
{QStringLiteral("projectId"), projectId},
|
||||
{QStringLiteral("properties"),
|
||||
buildColorScaleProperties(cs, lineCfg_, lvlSchemeType, logLinesCount, equalAreaLayerCount)},
|
||||
};
|
||||
QPointer<GridDataChartView> self(this);
|
||||
cmdRepo_->saveColorGradation(body, [self](bool ok, QString msg) {
|
||||
if (!self || ok) return;
|
||||
QMessageBox::warning(self, QStringLiteral("色阶配置"),
|
||||
msg.isEmpty() ? QStringLiteral("色阶保存失败") : msg);
|
||||
});
|
||||
}
|
||||
|
||||
void GridDataChartView::setCommandRepo(geopro::data::IDatasetCommandRepository* repo,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include <vector>
|
||||
|
||||
#include <QJsonObject> // submitDrawnException 默认参数 const QJsonObject& = {} 需完整类型
|
||||
#include <QPointer>
|
||||
#include <QString>
|
||||
#include <QWidget>
|
||||
|
||||
|
|
@ -28,6 +29,10 @@ class IColorTemplateRepository;
|
|||
class IDatasetCommandRepository;
|
||||
}
|
||||
|
||||
namespace geopro::controller {
|
||||
class DatasetViewState; // 跨视图色阶真源(统一同步机制)
|
||||
}
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
class AnomalyTablePanel;
|
||||
|
|
@ -71,9 +76,17 @@ public:
|
|||
tmObjectIdGetter_ = std::move(tmObjectIdGetter);
|
||||
}
|
||||
|
||||
// 注入跨视图色阶真源(统一同步):编辑写入它、并连 colorScaleChanged 跟随他视图(如 3D)改色。
|
||||
void setViewState(geopro::controller::DatasetViewState* state);
|
||||
|
||||
private:
|
||||
void rebuildContour(); // 按当前显隐开关重建并重绘 ContourPlotItem
|
||||
void openColorScaleEditor(); // 「色阶配置」→ 共享色阶编辑器(色阶 + 层级⚙ + 线形⚙)
|
||||
void applyColorScaleRender(); // 用当前 gridScale_ 重建色彩服务/等值面/色阶条
|
||||
void onColorScaleChanged(const QString& dsId); // 色阶真源变更(本视图或他视图编辑)→ 跟随重渲染
|
||||
void persistColorScale(const QString& dsId, const geopro::core::ColorScale& cs,
|
||||
const QString& lvlSchemeType, int logLinesCount,
|
||||
int equalAreaLayerCount); // 存后端(含层级方案透传字段)
|
||||
void openGridWizard(); // I1「网格」→ 网格化向导
|
||||
void openWhitening(); // I3「白化」→ 白化弹窗
|
||||
void openFilter(); // I4「滤波处理」→ 滤波弹窗
|
||||
|
|
@ -131,6 +144,8 @@ private:
|
|||
|
||||
// tmObjectId 取值回调(= 数据集 structParentId)。白化对话框模板列表用;空 → 模板列表为空。
|
||||
std::function<QString()> tmObjectIdGetter_;
|
||||
|
||||
QPointer<geopro::controller::DatasetViewState> state_; // 跨视图色阶真源(注入;QPointer 自动判空防悬挂)
|
||||
};
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
#include "ColorScaleConfigDialog.hpp"
|
||||
#include "panels/chart/ChartTheme.hpp"
|
||||
#include "panels/chart/ColorBarWidget.hpp"
|
||||
#include "panels/chart/ColorScaleProperties.hpp"
|
||||
#include "DatasetViewState.hpp"
|
||||
#include "panels/chart/GridWizardDialog.hpp"
|
||||
#include "panels/chart/InversionFormDialog.hpp"
|
||||
#include "panels/chart/SaveAsDialog.hpp"
|
||||
|
|
@ -302,41 +304,6 @@ void styleToolIconButton(QToolButton* btn, const QIcon& icon) {
|
|||
btn->setCursor(Qt::PointingHandCursor);
|
||||
}
|
||||
|
||||
// core::Rgba → colorBar 颜色串(与 ColorScaleConfigDialog::rgbaToCss 同格式:不透明 #RRGGBB,
|
||||
// 半透明 rgba(r,g,b,a∈0..1)),与后端 colorBar 互通。
|
||||
QString rgbaToColorBarCss(const geopro::core::Rgba& c) {
|
||||
if (c.a >= 255)
|
||||
return QStringLiteral("#%1%2%3")
|
||||
.arg(c.r, 2, 16, QLatin1Char('0'))
|
||||
.arg(c.g, 2, 16, QLatin1Char('0'))
|
||||
.arg(c.b, 2, 16, QLatin1Char('0'))
|
||||
.toUpper();
|
||||
return QStringLiteral("rgba(%1, %2, %3, %4)")
|
||||
.arg(c.r)
|
||||
.arg(c.g)
|
||||
.arg(c.b)
|
||||
.arg(QString::number(c.a / 255.0, 'g', 3));
|
||||
}
|
||||
|
||||
// 组装色阶 properties(colorBar + lineConfig + labelConfig),与原版散点路径
|
||||
// newLvlColorLevel 一致(battery/scatters 仅发这三块,不含 lvlSchemeType 等等值面专属字段)。
|
||||
QJsonObject buildColorScaleProperties(const geopro::core::ColorScale& scale,
|
||||
const ContourLineConfig& lineCfg) {
|
||||
QJsonArray colorBar;
|
||||
for (const auto& [value, color] : scale.stops())
|
||||
colorBar.append(QJsonArray{QString::number(value, 'f', 2), rgbaToColorBarCss(color)});
|
||||
QJsonObject lineConfig{
|
||||
{QStringLiteral("showLines"), lineCfg.lineShow},
|
||||
{QStringLiteral("color"), rgbaToColorBarCss(lineCfg.lineColor)},
|
||||
{QStringLiteral("lineType"),
|
||||
lineCfg.dashed ? QStringLiteral("dashed") : QStringLiteral("solid")}};
|
||||
QJsonObject labelConfig{{QStringLiteral("showLabels"), lineCfg.labelShow},
|
||||
{QStringLiteral("color"), rgbaToColorBarCss(lineCfg.labelColor)}};
|
||||
return QJsonObject{{QStringLiteral("colorBar"), colorBar},
|
||||
{QStringLiteral("lineConfig"), lineConfig},
|
||||
{QStringLiteral("labelConfig"), labelConfig}};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void RawDataChartView::showNotImplemented(QWidget* anchor) {
|
||||
|
|
@ -400,16 +367,21 @@ void RawDataChartView::openInversionColorScale(QWidget* anchor) {
|
|||
this);
|
||||
if (dlg.exec() != QDialog::Accepted) return;
|
||||
|
||||
// 本地重建上色重绘。
|
||||
data_.scale = dlg.colorScale();
|
||||
delete colorSvc_;
|
||||
colorSvc_ = new ColorMapService(data_.scale);
|
||||
redrawScatter();
|
||||
colorBar_->setColorScale(data_.scale);
|
||||
const auto cs = dlg.colorScale();
|
||||
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
|
||||
// 统一同步:反演原数据(type1,"")与网格/3D 共用 → 写真源,经 colorScaleChanged 各视图(含自身)重绘。
|
||||
if (state_ && !dsId.isEmpty()) {
|
||||
state_->setColorScale(dsId, cs);
|
||||
} else {
|
||||
data_.scale = cs;
|
||||
delete colorSvc_;
|
||||
colorSvc_ = new ColorMapService(data_.scale);
|
||||
redrawScatter();
|
||||
colorBar_->setColorScale(data_.scale);
|
||||
}
|
||||
showToast(this, QStringLiteral("色阶应用成功")); // M8 成功提示(对照原版 Message.success)
|
||||
|
||||
// 持久化(businessCode 空,对照原版 originPage newLvlColorLevel businessCode:'')。
|
||||
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
|
||||
const QString projectId = projectIdGetter_ ? projectIdGetter_() : QString();
|
||||
if (!cmdRepo_ || dsId.isEmpty()) return;
|
||||
QJsonObject body{
|
||||
|
|
@ -417,7 +389,13 @@ void RawDataChartView::openInversionColorScale(QWidget* anchor) {
|
|||
{QStringLiteral("templateId"), data_.templateId}, // 读取到的色阶模板 id(对照原版,可空)
|
||||
{QStringLiteral("businessCode"), QString()},
|
||||
{QStringLiteral("projectId"), projectId},
|
||||
{QStringLiteral("properties"), buildColorScaleProperties(data_.scale, dlg.lineConfig())},
|
||||
// load-then-save:在加载到的原始 properties 上只覆盖 colorBar+opacity,保留网格设过的
|
||||
// lineConfig/层级(共用同一条 businessCode="" 记录);无原始记录(首次)才整条新建。
|
||||
{QStringLiteral("properties"),
|
||||
data_.properties.isEmpty()
|
||||
? buildColorScaleProperties(cs, dlg.lineConfig(), dlg.lvlSchemeType(),
|
||||
dlg.logLinesCount(), dlg.equalAreaLayerCount())
|
||||
: withColorBarAndOpacity(data_.properties, cs)},
|
||||
};
|
||||
QPointer<RawDataChartView> self(this);
|
||||
cmdRepo_->saveColorGradation(body, [self](bool ok, QString msg) {
|
||||
|
|
@ -427,6 +405,26 @@ void RawDataChartView::openInversionColorScale(QWidget* anchor) {
|
|||
});
|
||||
}
|
||||
|
||||
void RawDataChartView::setViewState(geopro::controller::DatasetViewState* state) {
|
||||
state_ = state;
|
||||
if (!state_) return;
|
||||
connect(state_, &geopro::controller::DatasetViewState::colorScaleChanged, this,
|
||||
&RawDataChartView::onColorScaleChanged);
|
||||
}
|
||||
|
||||
void RawDataChartView::onColorScaleChanged(const QString& dsId) {
|
||||
if (!state_) return;
|
||||
if (!dsIdGetter_ || dsIdGetter_() != dsId) return; // 只跟随本视图所示数据集
|
||||
const auto* cs = state_->colorScale(dsId);
|
||||
if (!cs) return;
|
||||
data_.scale = *cs;
|
||||
delete colorSvc_;
|
||||
colorSvc_ = new ColorMapService(data_.scale);
|
||||
redrawScatter();
|
||||
if (data_.verticalLegend) colorBarV_->setColorScale(data_.scale);
|
||||
else colorBar_->setColorScale(data_.scale);
|
||||
}
|
||||
|
||||
void RawDataChartView::openInversionSaveAs(QWidget* anchor) {
|
||||
// O3:另存为(复用 SaveAsDialog::Inversion → saveInversionAsData)。
|
||||
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
|
||||
|
|
@ -599,7 +597,10 @@ void RawDataChartView::openScatterColorScale(QWidget* anchor) {
|
|||
{QStringLiteral("templateId"), data_.templateId}, // 读取到的色阶模板 id(对照原版,可空)
|
||||
{QStringLiteral("businessCode"), currentVFieldCode()},
|
||||
{QStringLiteral("projectId"), projectId},
|
||||
{QStringLiteral("properties"), buildColorScaleProperties(data_.scale, dlg.lineConfig())},
|
||||
// measurement(type3,"R0") 独立记录:不写等值面层级方案字段(includeLvlScheme=false)。
|
||||
{QStringLiteral("properties"),
|
||||
buildColorScaleProperties(data_.scale, dlg.lineConfig(), QStringLiteral("normal"), 8, 10,
|
||||
/*includeLvlScheme=*/false)},
|
||||
};
|
||||
QPointer<RawDataChartView> self(this);
|
||||
cmdRepo_->saveColorGradation(body, [self](bool ok, QString msg) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#pragma once
|
||||
#include <functional>
|
||||
|
||||
#include <QPointer>
|
||||
#include <QString>
|
||||
#include <QWidget>
|
||||
#include "model/detail/DetailPayloads.hpp"
|
||||
|
|
@ -18,6 +19,10 @@ class IDatasetCommandRepository;
|
|||
class IColorTemplateRepository;
|
||||
}
|
||||
|
||||
namespace geopro::controller {
|
||||
class DatasetViewState; // 跨视图色阶真源(统一同步)
|
||||
}
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
class ColorBarWidget;
|
||||
|
|
@ -52,6 +57,10 @@ public:
|
|||
// setCommandRepo 注入的 projectIdGetter_)。可传空 → 编辑器后端按钮禁用。
|
||||
void setColorTemplateRepo(geopro::data::IColorTemplateRepository* repo);
|
||||
|
||||
// 注入跨视图色阶真源:反演原数据散点(type1, businessCode="")与网格/3D 共用色阶 → 实时联动。
|
||||
// measurement(type3,"R0") 单视图、不路由真源(openScatterColorScale 不经此)。
|
||||
void setViewState(geopro::controller::DatasetViewState* state);
|
||||
|
||||
protected:
|
||||
// 信息模式(M13)下捕获画布点击:找最近散点显示属性。其余事件不消费。
|
||||
bool eventFilter(QObject* obj, QEvent* ev) override;
|
||||
|
|
@ -70,6 +79,7 @@ private:
|
|||
// 反演原数据默认工具条交互(O1/O2/O3):
|
||||
void openGridWizard(QWidget* anchor); // O1 网格化向导(复用 GridWizardDialog)
|
||||
void openInversionColorScale(QWidget* anchor); // O2 原数据散点色阶(type1,businessCode='')
|
||||
void onColorScaleChanged(const QString& dsId); // 色阶真源变更(本视图或网格/3D)→ type1 散点跟随重绘
|
||||
void openInversionSaveAs(QWidget* anchor); // O3 另存为(复用 SaveAsDialog::Inversion)
|
||||
|
||||
// measurement 交互:
|
||||
|
|
@ -130,6 +140,8 @@ private:
|
|||
std::function<QString()> projectIdGetter_;
|
||||
// 色阶模板仓储(注入;空则编辑器「另存为/打开」禁用)。
|
||||
geopro::data::IColorTemplateRepository* colorTplRepo_ = nullptr;
|
||||
|
||||
QPointer<geopro::controller::DatasetViewState> state_; // 跨视图色阶真源(注入;仅 type1 路由;QPointer 防悬挂)
|
||||
};
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ find_package(Qt6 COMPONENTS Core REQUIRED)
|
|||
add_library(geopro_controller STATIC
|
||||
WorkbenchNavController.cpp
|
||||
DatasetDetailController.cpp
|
||||
DatasetViewState.cpp
|
||||
VtkSceneController.cpp)
|
||||
target_include_directories(geopro_controller PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(geopro_controller PUBLIC geopro_data Qt6::Core)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
#include "DatasetViewState.hpp"
|
||||
// 实现全部内联于头;此 .cpp 仅为让 AUTOMOC 为带 Q_OBJECT 的头生成并链接 moc。
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
#pragma once
|
||||
#include <QHash>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include "model/ColorScale.hpp"
|
||||
|
||||
namespace geopro::controller {
|
||||
|
||||
// 跨视图共享的「单一真源」会话状态,按 dsId 维护。统一所有视图间同步,取代两两接线:
|
||||
// - 改色阶:任何编辑入口只调 setColorScale(dsId, cs),不再各改各的拷贝;
|
||||
// - 观察:各视图连一次 colorScaleChanged(dsId),槽里【只重渲染】、【绝不回写】→ 无信号回环;
|
||||
// - 加载:视图取色阶时先问 hub(colorScale 非空则用之),否则把后端值 seed 进来当真源。
|
||||
// 新增同步项(可见性/选中/值域…)= 加一个字段 + 一个 xxxChanged(dsId) 信号,沿用同一套机制。
|
||||
//
|
||||
// 作用域:数据集的「默认/共享色阶」(后端 businessCode=""),被 反演散点/网格/帘面/体 共用(同一条后端
|
||||
// 记录)。measurement(businessCode="R0") 为单视图、无跨视图伙伴,暂不入此层(清晰边界,非欠债)。
|
||||
class DatasetViewState : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit DatasetViewState(QObject* parent = nullptr) : QObject(parent) {}
|
||||
|
||||
bool hasColorScale(const QString& dsId) const { return scales_.contains(dsId); }
|
||||
|
||||
// 无记录返回 nullptr(调用方据此兜底为自带值)。
|
||||
const geopro::core::ColorScale* colorScale(const QString& dsId) const {
|
||||
auto it = scales_.constFind(dsId);
|
||||
return it == scales_.constEnd() ? nullptr : &it.value();
|
||||
}
|
||||
|
||||
// 用户编辑应用:写入真源并广播。观察者据此重渲染。
|
||||
void setColorScale(const QString& dsId, const geopro::core::ColorScale& cs) {
|
||||
scales_.insert(dsId, cs);
|
||||
emit colorScaleChanged(dsId);
|
||||
}
|
||||
|
||||
// 首次从后端加载得到色阶时播种:已有则不覆盖、不广播(避免加载即触发重建/存盘)。
|
||||
void seedColorScale(const QString& dsId, const geopro::core::ColorScale& cs) {
|
||||
if (!scales_.contains(dsId)) scales_.insert(dsId, cs);
|
||||
}
|
||||
|
||||
signals:
|
||||
void colorScaleChanged(const QString& dsId);
|
||||
|
||||
private:
|
||||
QHash<QString, geopro::core::ColorScale> scales_;
|
||||
};
|
||||
|
||||
} // namespace geopro::controller
|
||||
|
|
@ -44,6 +44,10 @@ public:
|
|||
// 3D:体绘制(IDW 体素 + colorScale);按 dsId 跟踪。
|
||||
virtual void addVolume(const std::string& dsId, const geopro::data::VolumeGrid& vol,
|
||||
const geopro::core::ColorScale& cs) = 0;
|
||||
// 原地更新已渲染体颜色/不透明度(仅换传函、不重建 image):色阶改动用,避免换 image 连带关闭未保存切片。
|
||||
// 返回 true=已原地更新;false=该体未渲染/不支持 → 调用方回退 remove+add。默认 false。
|
||||
virtual bool updateVolumeColorInPlace(const std::string& /*dsId*/,
|
||||
const geopro::core::ColorScale& /*cs*/) { return false; }
|
||||
// 2D 足迹:把测线/轨迹经纬折线平铺进 3D 地图(worldZ=摆放高程);按 dsId 跟踪以支持增量移除。
|
||||
virtual void addMapLine(const std::string& dsId, const geopro::data::MapLine& line,
|
||||
double worldZ) = 0;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include <QDebug>
|
||||
#include <QPointer>
|
||||
|
||||
#include "DatasetViewState.hpp"
|
||||
#include "I3dSceneView.hpp"
|
||||
#include "repo/IDatasetRepository.hpp"
|
||||
|
||||
|
|
@ -24,6 +25,39 @@ VtkSceneController::VtkSceneController(data::IDatasetRepository& dsRepo,
|
|||
QObject* parent)
|
||||
: QObject(parent), dsRepo_(dsRepo), sceneRepo_(sceneRepo), view_(view) {}
|
||||
|
||||
void VtkSceneController::setViewState(DatasetViewState* state) {
|
||||
state_ = state;
|
||||
if (state_)
|
||||
connect(state_, &DatasetViewState::colorScaleChanged, this,
|
||||
&VtkSceneController::recolorDataset);
|
||||
}
|
||||
|
||||
void VtkSceneController::recolorDataset(const QString& qid) {
|
||||
if (!state_) return;
|
||||
const geopro::core::ColorScale* cs = state_->colorScale(qid);
|
||||
if (!cs) return;
|
||||
const std::string dsId = qid.toStdString();
|
||||
volumeScaleCache_[dsId] = *cs; // 体色阶随真源更新(未渲染时下次勾选命中)
|
||||
if (!isChecked(dsId)) return; // 未渲染 → 仅更缓存
|
||||
// 就地重建:体 → 用新色阶重 addVolume(addVolume 内部触发体下切片随新色阶重建);
|
||||
// 帘面 → 用缓存源网格重 addCurtain。一个 dsId 只会是其一。
|
||||
bool changed = false;
|
||||
if (auto vit = volumeCache_.find(dsId); vit != volumeCache_.end()) {
|
||||
// 优先原地改色(仅换传函、不重建 image)→ 该体下未保存切片不被关闭、跟随改色。
|
||||
// 原地失败(理论不至)才回退 remove+add(会关未保存切片)。
|
||||
if (!view_.updateVolumeColorInPlace(dsId, *cs)) {
|
||||
view_.removeDataset(dsId);
|
||||
view_.addVolume(dsId, vit->second, *cs);
|
||||
}
|
||||
changed = true;
|
||||
} else if (auto sit = sectionGridCache_.find(dsId); sit != sectionGridCache_.end()) {
|
||||
view_.removeDataset(dsId);
|
||||
view_.addCurtain(dsId, sit->second, *cs);
|
||||
changed = true;
|
||||
}
|
||||
if (changed) view_.renderIncremental();
|
||||
}
|
||||
|
||||
void VtkSceneController::setCheckedDatasets(const QStringList& dsIds) {
|
||||
std::vector<std::string> newDs;
|
||||
newDs.reserve(static_cast<std::size_t>(dsIds.size()));
|
||||
|
|
@ -149,7 +183,10 @@ void VtkSceneController::addDatasetAsync(const std::string& dsId, unsigned long
|
|||
auto cachedGrid = volumeCache_.find(dsId);
|
||||
auto cachedScale = volumeScaleCache_.find(dsId);
|
||||
if (cachedGrid != volumeCache_.end() && cachedScale != volumeScaleCache_.end()) {
|
||||
view_.addVolume(dsId, cachedGrid->second, cachedScale->second); // 缓存命中(色阶随体缓存)
|
||||
const QString qid = QString::fromStdString(dsId); // 优先用色阶真源(含已编辑值)
|
||||
const geopro::core::ColorScale& useCs =
|
||||
(state_ && state_->colorScale(qid)) ? *state_->colorScale(qid) : cachedScale->second;
|
||||
view_.addVolume(dsId, cachedGrid->second, useCs); // 缓存命中(色阶随体缓存)
|
||||
onDatasetArrived();
|
||||
emit volumeRendered(QString::fromStdString(dsId)); // 缓存命中即时完成 → 撤 spinner
|
||||
emit datasetRendered(QString::fromStdString(dsId));
|
||||
|
|
@ -164,11 +201,16 @@ void VtkSceneController::addDatasetAsync(const std::string& dsId, unsigned long
|
|||
self->loadingDs_.erase(dsId);
|
||||
if (gen != self->rebuildGeneration_ || !self->isChecked(dsId)) return;
|
||||
self->volumeScaleCache_[dsId] = cs; // 色阶随体一起缓存(mock 体在 dsRepo_ 无条目)
|
||||
const QString qid = QString::fromStdString(dsId);
|
||||
if (self->state_) self->state_->seedColorScale(qid, cs); // 播种真源
|
||||
auto it = self->volumeCache_.emplace(dsId, std::move(g)).first;
|
||||
qInfo().noquote() << "[volrender] addVolume dsId=" << QString::fromStdString(dsId)
|
||||
qInfo().noquote() << "[volrender] addVolume dsId=" << qid
|
||||
<< "nx=" << it->second.vol.nx() << "ny=" << it->second.vol.ny()
|
||||
<< "nz=" << it->second.vol.nz();
|
||||
self->view_.addVolume(dsId, it->second, self->volumeScaleCache_[dsId]);
|
||||
const geopro::core::ColorScale& useCs =
|
||||
(self->state_ && self->state_->colorScale(qid)) ? *self->state_->colorScale(qid)
|
||||
: self->volumeScaleCache_[dsId];
|
||||
self->view_.addVolume(dsId, it->second, useCs);
|
||||
self->onDatasetArrived();
|
||||
emit self->volumeRendered(QString::fromStdString(dsId)); // 落地完成 → 撤 spinner
|
||||
emit self->datasetRendered(QString::fromStdString(dsId));
|
||||
|
|
@ -191,7 +233,13 @@ void VtkSceneController::addDatasetAsync(const std::string& dsId, unsigned long
|
|||
if (!self) return;
|
||||
self->loadingDs_.erase(dsId);
|
||||
if (gen != self->rebuildGeneration_ || !self->isChecked(dsId)) return; // 作废/已取消
|
||||
self->view_.addCurtain(dsId, s.grid, s.scale);
|
||||
self->sectionGridCache_.insert_or_assign(dsId, s.grid); // 留存源网格供帘面重着色(Grid 无默认构造)
|
||||
const QString qid = QString::fromStdString(dsId);
|
||||
if (self->state_) self->state_->seedColorScale(qid, s.scale); // 播种真源
|
||||
const geopro::core::ColorScale& useCs =
|
||||
(self->state_ && self->state_->colorScale(qid)) ? *self->state_->colorScale(qid)
|
||||
: s.scale;
|
||||
self->view_.addCurtain(dsId, s.grid, useCs);
|
||||
self->onDatasetArrived();
|
||||
emit self->datasetRendered(QString::fromStdString(dsId)); // 帘面落地 → 复原复选框
|
||||
},
|
||||
|
|
@ -260,12 +308,16 @@ void VtkSceneController::rebuild() { rebuildInternal(); }
|
|||
|
||||
void VtkSceneController::setVolumeColorScale(const std::string& dsId,
|
||||
const geopro::core::ColorScale& cs) {
|
||||
volumeScaleCache_[dsId] = cs; // 会话级 mock 持久(再勾选命中缓存,见 addDatasetAsync)
|
||||
if (!isChecked(dsId)) return; // 未渲染 → 仅更缓存,下次勾选生效
|
||||
// 统一走色阶真源:写入即广播 colorScaleChanged → recolorDataset 就地重着色(体/帘面+切片),
|
||||
// 同时 2D 详情等其它视图一并跟随。无 state_(理论不至)才退化为直连重建。
|
||||
if (state_) {
|
||||
state_->setColorScale(QString::fromStdString(dsId), cs);
|
||||
return;
|
||||
}
|
||||
volumeScaleCache_[dsId] = cs;
|
||||
if (!isChecked(dsId)) return;
|
||||
auto git = volumeCache_.find(dsId);
|
||||
if (git == volumeCache_.end()) return; // 体网格尚未到场 → 同上
|
||||
// 移除旧体素 → 以新色阶重建:addVolume 内部置 currentColorScale_ 并触发 onVolumeChanged,
|
||||
// InteractionManager 据此以新色阶重建该体下已勾选切片。
|
||||
if (git == volumeCache_.end()) return;
|
||||
view_.removeDataset(dsId);
|
||||
view_.addVolume(dsId, git->second, cs);
|
||||
view_.renderIncremental();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#pragma once
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <map>
|
||||
|
|
@ -18,6 +19,8 @@ class IDatasetRepository;
|
|||
|
||||
namespace geopro::controller {
|
||||
|
||||
class DatasetViewState; // 跨视图共享色阶真源(统一同步机制)
|
||||
|
||||
// 中央视图模式:二维地图(俯视测线)/ 三维视图(帘面/体素/地形)。
|
||||
enum class ViewMode { Map2D, View3D };
|
||||
|
||||
|
|
@ -35,6 +38,10 @@ public:
|
|||
VtkSceneController(data::IDatasetRepository& dsRepo, data::I3dSceneRepository& sceneRepo,
|
||||
I3dSceneView& view, QObject* parent = nullptr);
|
||||
|
||||
// 注入跨视图色阶真源(统一同步):连 colorScaleChanged → 就地按 dsId 重着色帘面/体。
|
||||
// 构造后由 main.cpp 注入一次。
|
||||
void setViewState(DatasetViewState* state);
|
||||
|
||||
public slots:
|
||||
void setCheckedDatasets(const QStringList& dsIds);
|
||||
// 二维数据集栏勾选(足迹型测线/轨迹)→ 平铺进 View3D 地图。与 3D 勾选集独立,按 dsId 增量。
|
||||
|
|
@ -51,8 +58,8 @@ public slots:
|
|||
void setVolumeOpacity(double maxOpacity);
|
||||
void rebuild(); // 主题切换等外部触发的重渲染
|
||||
|
||||
// 色阶编辑器「确定」:更新某三维体色阶并就地重渲染(体素 + 其切片随新色阶重建)。
|
||||
// 后端 3D 色阶保存未就绪 → 缓存即会话级 mock 持久(再勾选命中 volumeScaleCache_)。
|
||||
// 色阶编辑器「确定」:写入色阶真源(state_),经 colorScaleChanged 统一就地重着色(体/帘面 + 切片)。
|
||||
// 兼容旧调用点;真正的重着色在 recolorDataset()。无 state_ 时退化为直连重建。
|
||||
void setVolumeColorScale(const std::string& dsId, const geopro::core::ColorScale& cs);
|
||||
|
||||
// ── P2 三维数据集栏 ──
|
||||
|
|
@ -77,6 +84,8 @@ signals:
|
|||
|
||||
private:
|
||||
void rebuildInternal();
|
||||
// colorScaleChanged(dsId) 槽:从 state_ 取新色阶,就地重建该 dsId 的帘面/体(及体下切片)。只渲染,不回写。
|
||||
void recolorDataset(const QString& dsId);
|
||||
// 增量加入单个 ds(帘面/体素,按图层开关);回调按 gen + 仍勾选 守护,落地后增量渲染。
|
||||
void addDatasetAsync(const std::string& dsId, unsigned long long gen);
|
||||
// 增量加入单个 2D 足迹(异步 loadMapLine → addMapLine at 当前摆放 Z);回调按 gen + 仍勾选 守护。
|
||||
|
|
@ -113,9 +122,13 @@ private:
|
|||
AxisRangeCfg axisX_, axisY_, axisZ_; // 坐标轴设置面板的 per-axis 可见性 + 自定义范围
|
||||
static constexpr int kAxesFontSize = 12;
|
||||
|
||||
QPointer<DatasetViewState> state_; // 跨视图色阶真源(注入;编辑/加载/重着色都经它;QPointer 防悬挂)
|
||||
|
||||
// 缓存(按 dsId):避免重复读盘/插值。
|
||||
std::map<std::string, geopro::core::Grid> gridCache_;
|
||||
std::map<std::string, geopro::core::ColorScale> colorScaleCache_;
|
||||
// 帘面源网格缓存:帘面重着色需 grid 重建 addCurtain(loadSection 的 s.grid 不在 gridCache_)。
|
||||
std::map<std::string, geopro::core::Grid> sectionGridCache_;
|
||||
std::map<std::string, data::VolumeGrid> volumeCache_;
|
||||
// 三维体色阶缓存:mock 体在 dsRepo_ 无条目,色阶随 loadVolume 一起交付并缓存于此。
|
||||
std::map<std::string, geopro::core::ColorScale> volumeScaleCache_;
|
||||
|
|
|
|||
|
|
@ -23,10 +23,16 @@ public:
|
|||
void setOver(Rgba c) { over_ = c; }
|
||||
void setNan(Rgba c) { nan_ = c; }
|
||||
bool empty() const { return stops_.empty(); }
|
||||
|
||||
// 整体透明度(两级透明度的第二级):与每色 alpha 相乘,渲染时才叠加,绝不烘焙进 stop。
|
||||
// [0,1],默认 1(不透明)。独立存储 → 色阶编辑可回显真实值、单色 alpha 保持独立。
|
||||
void setGlobalOpacity(double o) { globalOpacity_ = o; }
|
||||
double globalOpacity() const { return globalOpacity_; }
|
||||
private:
|
||||
struct Stop { double value; Rgba color; };
|
||||
std::vector<Stop> stops_;
|
||||
std::optional<Rgba> under_, over_, nan_;
|
||||
double globalOpacity_ = 1.0;
|
||||
};
|
||||
|
||||
} // namespace geopro::core
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#pragma once
|
||||
#include <vector>
|
||||
#include <QJsonObject>
|
||||
#include <QMetaType>
|
||||
#include <QString>
|
||||
#include "model/Field.hpp"
|
||||
|
|
@ -41,6 +42,9 @@ struct ScatterPayload {
|
|||
// 色阶模板 id(来自 lvl/colorGradation/getDetail 的 templateId):保存色阶时回带
|
||||
// (对照原版 newLvlColorLevel 带读取到的 templateId;可空)。
|
||||
QString templateId;
|
||||
// type1 原始 properties(lineConfig/labelConfig/层级方案):保存色阶时只覆盖 colorBar+opacity、
|
||||
// 其余原样回写,避免清掉网格(共用同一条 businessCode="" 记录)设过的等值线/层级值(对齐原版 load-then-save)。
|
||||
QJsonObject properties;
|
||||
};
|
||||
|
||||
// 等值面载荷:grid(rows) + 色阶 + 异常(≈ data::GridParts)。
|
||||
|
|
|
|||
|
|
@ -477,6 +477,21 @@ void Api3dRepository::deleteSlice(const std::string& dsId, std::function<void()>
|
|||
onOk();
|
||||
}
|
||||
|
||||
void Api3dRepository::setSliceColorScale(const std::string& dsId,
|
||||
const geopro::core::ColorScale& cs) {
|
||||
auto it = slices_.find(dsId);
|
||||
if (it == slices_.end()) return;
|
||||
it->second.colorScale = cs; // 切片独立色阶(mock;真实后端走该切片 dsId 的 colorGradation)
|
||||
it->second.hasColorScale = true;
|
||||
}
|
||||
|
||||
bool Api3dRepository::sliceColorScale(const std::string& dsId, geopro::core::ColorScale& out) const {
|
||||
auto it = slices_.find(dsId);
|
||||
if (it == slices_.end() || !it->second.hasColorScale) return false;
|
||||
out = it->second.colorScale;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── 异常 / 异常体(后端真实端点存在,但异常挂三维体、三维体仍 mock → 异常暂内存 mock;
|
||||
// 挂载结构按"异常→三维体",整链端点就绪后切真实,见记忆 vtk-3d-persistence-structure)──
|
||||
|
||||
|
|
|
|||
|
|
@ -98,6 +98,8 @@ public:
|
|||
std::function<void()> onOk, OnError onErr) override;
|
||||
void deleteSlice(const std::string& dsId,
|
||||
std::function<void()> onOk, OnError onErr) override;
|
||||
void setSliceColorScale(const std::string& dsId, const geopro::core::ColorScale& cs) override;
|
||||
bool sliceColorScale(const std::string& dsId, geopro::core::ColorScale& out) const override;
|
||||
|
||||
// 异常 / 异常体(后端未就绪 → load 回空树,变更走 onErr)
|
||||
void loadAnomalyTree(const std::string& objectId,
|
||||
|
|
@ -151,6 +153,8 @@ private:
|
|||
SliceSpec spec;
|
||||
std::string name;
|
||||
std::string createTime; // 创建时刻
|
||||
geopro::core::ColorScale colorScale; // 切片自己的色阶(颜色快照 + 不透明度并入 globalOpacity)
|
||||
bool hasColorScale = false; // 是否已设过独立色阶(否则还原时跟随三维体)
|
||||
};
|
||||
std::map<std::string, StoredSlice> slices_; // dsId → 切片
|
||||
int sliceCounter_ = 0;
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ struct ChartParts {
|
|||
geopro::core::ScatterField scatter;
|
||||
geopro::core::ColorScale scatterScale;
|
||||
QString templateId; // 散点色阶模板 id(保存色阶回带,对照原版 lvlTemplateId)
|
||||
QJsonObject scatterProperties; // type1 记录原始 properties(保存时回写 lineConfig/层级,不覆盖网格的值)
|
||||
};
|
||||
// 网格数据加载结果:grid(rows) + 网格色阶(type2) + 异常。
|
||||
struct GridParts {
|
||||
|
|
@ -61,6 +62,8 @@ ChartParts parseScatterParts(const QList<net::ApiResponse>& r) {
|
|||
p.scatter = dto::parseScatterGraph(r[0].data);
|
||||
p.scatterScale = dto::parseColorBar(r[1].data);
|
||||
p.templateId = r[1].data.value(QStringLiteral("templateId")).toVariant().toString();
|
||||
// 原始 properties(含 lineConfig/labelConfig/层级方案):保存色阶时原样回写,避免清掉网格(共用同条记录)的值。
|
||||
p.scatterProperties = r[1].data.value(QStringLiteral("properties")).toObject();
|
||||
return p;
|
||||
}
|
||||
|
||||
|
|
@ -175,6 +178,7 @@ DetailLoad* ApiDatasetRepository::makeInversionScatter(const std::string& dsId)
|
|||
ChartParts p = parseScatterParts(r);
|
||||
core::ScatterPayload payload{p.scatter, p.scatterScale};
|
||||
payload.templateId = p.templateId; // 色阶保存回带(对照原版 lvlTemplateId)
|
||||
payload.properties = p.scatterProperties; // 原始 properties → 保存时回写非 colorBar 字段
|
||||
return QVariant::fromValue(payload);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,10 @@ ScatterField parseScatterGraph(const QJsonObject& data) {
|
|||
|
||||
ColorScale parseColorBar(const QJsonObject& data) {
|
||||
ColorScale cs;
|
||||
const QJsonArray bar = data.value("properties").toObject().value("colorBar").toArray();
|
||||
const QJsonObject props = data.value("properties").toObject();
|
||||
// 整体透明度(两级第二级):properties.opacity,缺省 1(不透明)。
|
||||
if (props.contains("opacity")) cs.setGlobalOpacity(props.value("opacity").toDouble(1.0));
|
||||
const QJsonArray bar = props.value("colorBar").toArray();
|
||||
for (auto e : bar) {
|
||||
const QJsonArray pair = e.toArray();
|
||||
if (pair.size() < 2) continue;
|
||||
|
|
|
|||
|
|
@ -113,6 +113,13 @@ public:
|
|||
virtual void deleteSlice(const std::string& dsId,
|
||||
std::function<void()> onOk, OnError onErr) = 0;
|
||||
|
||||
// 已保存切片的独立色阶(颜色快照 + 不透明度并入 globalOpacity)。mock 仓储内存存;
|
||||
// 默认空实现(无色阶存储的仓储)。set 在保存切片后调用,get 在还原/编辑切片色阶时用。
|
||||
virtual void setSliceColorScale(const std::string& /*dsId*/,
|
||||
const geopro::core::ColorScale& /*cs*/) {}
|
||||
virtual bool sliceColorScale(const std::string& /*dsId*/,
|
||||
geopro::core::ColorScale& /*out*/) const { return false; }
|
||||
|
||||
// ── 异常 / 异常体(spec §6.4)────────────────────────────────────────────
|
||||
// 异常体(树中间层):含该体下的多个 Anomaly。
|
||||
struct AnomalyBody {
|
||||
|
|
|
|||
|
|
@ -2,19 +2,34 @@
|
|||
|
||||
namespace geopro::render {
|
||||
|
||||
vtkSmartPointer<vtkLookupTable> buildLut(const geopro::core::ColorScale& cs, double vmin, double vmax, int n)
|
||||
vtkSmartPointer<vtkLookupTable> buildLut(const geopro::core::ColorScale& cs, double vmin, double vmax,
|
||||
int n, bool transparentBelowRange)
|
||||
{
|
||||
if (n < 2) n = 2; // 至少两级,避免 (n-1) 退化
|
||||
auto lut = vtkSmartPointer<vtkLookupTable>::New();
|
||||
lut->SetNumberOfTableValues(n);
|
||||
lut->SetTableRange(vmin, vmax);
|
||||
|
||||
// 白化(无数据)真透明:体把留空格设为哨兵 vmin-1.0(< vmin),切片 reslice 后据此识别。
|
||||
// ⚠ 实测(tests/spike/slice_alpha_probe):vtkImagePlaneWidget 纹理【认】区间内 texel alpha
|
||||
// (alpha=0 的格→透明,背后透出),但【不认】UseBelowRangeColor(下溢被钳到 0 号最低色格、
|
||||
// 填蓝,根本不走 below-range 色)。故不能用 UseBelowRangeColor,改为:把下限下移一格、
|
||||
// 预留 0 号格为全透明"白化槽"——下溢哨兵被钳到 0 号格即透明;真实 [vmin,vmax] 数据落
|
||||
// 1..n-1 格(不透明),不受影响。
|
||||
const double lo = transparentBelowRange ? vmin - (vmax - vmin) / (n - 1) : vmin;
|
||||
lut->SetTableRange(lo, vmax);
|
||||
for (int t = 0; t < n; ++t) {
|
||||
const double val = vmin + (vmax - vmin) * t / (n - 1);
|
||||
if (transparentBelowRange && t == 0) {
|
||||
lut->SetTableValue(0, 0.0, 0.0, 0.0, 0.0); // 白化槽:全透明(下溢钳到此)
|
||||
continue;
|
||||
}
|
||||
const double val = lo + (vmax - lo) * t / (n - 1);
|
||||
const auto c = cs.colorAt(val);
|
||||
// 复刻原版 three 渲染(parseColor 只取 rgb、MeshBasicMaterial opacity=1):
|
||||
// 忽略 colorBar 的 alpha,画满不透明 RGB。
|
||||
lut->SetTableValue(t, c.r / 255.0, c.g / 255.0, c.b / 255.0, 1.0);
|
||||
// 两级透明度(渲染时相乘,不烘焙):有效 alpha = 每色 alpha × 整体透明度。
|
||||
// #RRGGBB 无 alpha 默认 a=255、整体默认 1 → 旧纯色阶不受影响;alpha=0 真透明。
|
||||
lut->SetTableValue(t, c.r / 255.0, c.g / 255.0, c.b / 255.0,
|
||||
c.a / 255.0 * cs.globalOpacity());
|
||||
}
|
||||
if (transparentBelowRange) lut->SetNanColor(0.0, 0.0, 0.0, 0.0); // NaN 也透明(双保险)
|
||||
lut->Build();
|
||||
return lut;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
namespace geopro::render {
|
||||
|
||||
// 由 core 阶梯色阶构建 N 级 vtkLookupTable,区间 [vmin, vmax]。
|
||||
vtkSmartPointer<vtkLookupTable> buildLut(const geopro::core::ColorScale& cs, double vmin, double vmax, int n = 256);
|
||||
// transparentBelowRange=true:把 < vmin 的标量(=三维体无数据格的留空哨兵 vmin-1.0)映射为
|
||||
// 全透明(而非默认钳到最低档色不透明)。切片复用体的标量 image,据此让白化区真透明、不填蓝。
|
||||
// 仅切片需要;散点/等值线传 false(保留"低于值域钳最低色"的原行为,避免误隐真实欠量数据)。
|
||||
vtkSmartPointer<vtkLookupTable> buildLut(const geopro::core::ColorScale& cs, double vmin, double vmax,
|
||||
int n = 256, bool transparentBelowRange = false);
|
||||
|
||||
} // namespace geopro::render
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "actors/VoxelActor.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
|
|
@ -8,6 +9,7 @@
|
|||
#include <vtkDoubleArray.h>
|
||||
#include <vtkFloatArray.h>
|
||||
#include <vtkFlyingEdges3D.h>
|
||||
#include <vtkGPUVolumeRayCastMapper.h>
|
||||
#include <vtkNew.h>
|
||||
#include <vtkPolyData.h>
|
||||
#include <vtkPolyDataMapper.h>
|
||||
|
|
@ -16,6 +18,8 @@
|
|||
#include <vtkSmartVolumeMapper.h>
|
||||
#include <vtkPiecewiseFunction.h>
|
||||
#include <vtkPointData.h>
|
||||
#include <vtkUnsignedCharArray.h>
|
||||
#include <vtkVolumeMapper.h>
|
||||
#include <vtkVolumeProperty.h>
|
||||
|
||||
namespace geopro::render {
|
||||
|
|
@ -24,30 +28,88 @@ namespace {
|
|||
|
||||
// 颜色/不透明度传递函数采样级数。
|
||||
constexpr int kTransferSamples = 64;
|
||||
// 体绘制最大不透明度([vmin,vmax] 线性 0→kMaxOpacity)。值越大体越实(越不透明);
|
||||
// 0.15 偏淡 → 0.30 更实仍可看穿内部。再大(0.4~0.6)会更像实心块、遮挡内部结构。
|
||||
constexpr double kMaxOpacity = 0.30;
|
||||
|
||||
// 是否支持 GPU 体绘制(光线投射)。默认 true(有独显的常态);无 GPU 机器由 setVolumeGpuSupported(false)
|
||||
// 设回退。影响:mask 真白化只有 GPU mapper 支持 → 无 GPU 时不建 mask、改用 SmartVolumeMapper(自动 CPU 回退),
|
||||
// 空值仍靠不透明度传函(哨兵→0)透明,仅交界处少了 mask 的干净边(重现一圈细渗色)。
|
||||
bool g_gpuVolumeSupported = true;
|
||||
|
||||
// NaN/留空格的哨兵:落在 [vmin,vmax] 之外,传递函数把它映射为完全透明。
|
||||
double sentinel(double vmin) { return vmin - 1.0; }
|
||||
|
||||
// double/int16 两版公用的 mapper+property+volume 组装(行为与原 double 版一致)。
|
||||
// 二值 mask 体(UCHAR,255=有效、0=空值)。与标量同维同 origin/spacing、同点序
|
||||
// (id=(k*ny+j)*nx+i)。空值格 mask=0 → 喂给 GPU ray cast 后被完全跳过:不着色、不参与
|
||||
// 三线性插值,对齐 Surfer Blanking 的真白化语义(消除"空白处沿数据边界渗蓝")。
|
||||
// 调用方在填标量的同一循环里写 m->SetValue(id, valid?255:0)。
|
||||
vtkSmartPointer<vtkImageData> makeMaskLike(int nx, int ny, int nz,
|
||||
double ox, double oy, double oz,
|
||||
double dx, double dy, double dz,
|
||||
vtkUnsignedCharArray*& outArr)
|
||||
{
|
||||
auto mask = vtkSmartPointer<vtkImageData>::New();
|
||||
mask->SetDimensions(nx, ny, nz);
|
||||
mask->SetOrigin(ox, oy, oz);
|
||||
mask->SetSpacing(dx, dy, dz);
|
||||
vtkNew<vtkUnsignedCharArray> m;
|
||||
m->SetName("mask");
|
||||
m->SetNumberOfTuples(static_cast<vtkIdType>(nx) * ny * nz);
|
||||
mask->GetPointData()->SetScalars(m);
|
||||
outArr = m; // image 持有引用,循环结束前有效
|
||||
return mask;
|
||||
}
|
||||
|
||||
// double/int16 两版公用的 mapper+property+volume 组装。mask 非空 → 用 GPU ray cast + 二值 mask
|
||||
// 做真白化(SmartVolumeMapper 不转发 mask,故走 GPU mapper;桌面端恒有 GL 上下文);
|
||||
// mask 为空 → 保留 SmartVolumeMapper(GPU/CPU 自适应)。
|
||||
vtkSmartPointer<vtkVolume> assembleVolume(vtkImageData* img,
|
||||
vtkColorTransferFunction* color,
|
||||
vtkPiecewiseFunction* opacity)
|
||||
vtkPiecewiseFunction* opacity,
|
||||
vtkImageData* mask)
|
||||
{
|
||||
// SmartVolumeMapper:有 GPU 走 GPU ray cast,否则自动回退 CPU,避免无 GPU 时卡死/失败。
|
||||
vtkNew<vtkSmartVolumeMapper> mapper;
|
||||
mapper->SetInputData(img);
|
||||
// 全程统一全质量(GPU 足够快, 实测 ~7ms/帧):关掉交互降采样, 避免"停手补高清"那一帧突跳停顿。
|
||||
mapper->SetAutoAdjustSampleDistances(0);
|
||||
mapper->SetInteractiveAdjustSampleDistances(0);
|
||||
// 采样距离 + 不透明度单位距离用到几何尺度。
|
||||
double sp[3];
|
||||
img->GetSpacing(sp);
|
||||
const double minSp =
|
||||
std::min({std::abs(sp[0]), std::abs(sp[1]), std::abs(sp[2])}); // 最细体素维度
|
||||
double bnd[6];
|
||||
img->GetBounds(bnd);
|
||||
const double diag = std::sqrt((bnd[1] - bnd[0]) * (bnd[1] - bnd[0]) +
|
||||
(bnd[3] - bnd[2]) * (bnd[3] - bnd[2]) +
|
||||
(bnd[5] - bnd[4]) * (bnd[5] - bnd[4])); // 包围盒对角(最长穿越路径)
|
||||
|
||||
vtkSmartPointer<vtkVolumeMapper> mapper;
|
||||
if (mask && g_gpuVolumeSupported) {
|
||||
// 真白化:mask=0 体素被光线投射完全跳过,杜绝空值格沿边界渗蓝。需 GPU 光线投射支持。
|
||||
vtkNew<vtkGPUVolumeRayCastMapper> gpu;
|
||||
gpu->SetInputData(img);
|
||||
gpu->SetMaskInput(mask);
|
||||
gpu->SetMaskTypeToBinary();
|
||||
gpu->SetAutoAdjustSampleDistances(0); // 全程全质量(GPU 直接 mapper 无交互降采样开关)
|
||||
// 关了自适应必须显式给【细】采样距离,否则用粗默认值 → 看到一层层体素(分层伪影)。
|
||||
if (minSp > 0) gpu->SetSampleDistance(static_cast<float>(0.3 * minSp));
|
||||
// 抖动:用噪声纹理微扰每条光线的采样起点,消除规则采样面造成的「木纹/分层」伪影(VTK 官方此用途)。
|
||||
gpu->SetUseJittering(1);
|
||||
mapper = gpu;
|
||||
} else {
|
||||
// SmartVolumeMapper:有 GPU 走 GPU ray cast,否则自动回退 CPU,避免无 GPU 时卡死/失败。
|
||||
vtkNew<vtkSmartVolumeMapper> sm;
|
||||
sm->SetInputData(img);
|
||||
// 全程统一全质量(GPU 足够快, 实测 ~7ms/帧):关掉交互降采样, 避免"停手补高清"那一帧突跳停顿。
|
||||
sm->SetAutoAdjustSampleDistances(0);
|
||||
sm->SetInteractiveAdjustSampleDistances(0);
|
||||
mapper = sm;
|
||||
}
|
||||
|
||||
vtkNew<vtkVolumeProperty> prop;
|
||||
prop->SetColor(color);
|
||||
prop->SetScalarOpacity(opacity);
|
||||
prop->SetInterpolationTypeToLinear();
|
||||
prop->ShadeOff();
|
||||
// 不透明度单位距离 = 包围盒对角 × kOpacityUnitFraction:控制沿深度的累积速度,使色阶「不透明度」滑块
|
||||
// 有层次。取对角/10:100%(每单位=1.0)→沿体累积到≈实心、10% 很淡。太大(=整条对角)→100% 也偏透;
|
||||
// 太小(=体素)→ 低不透明度也累积到全不透明。
|
||||
constexpr double kOpacityUnitFraction = 0.1;
|
||||
if (diag > 0) prop->SetScalarOpacityUnitDistance(kOpacityUnitFraction * diag);
|
||||
|
||||
auto volume = vtkSmartPointer<vtkVolume>::New();
|
||||
volume->SetMapper(mapper);
|
||||
|
|
@ -81,14 +143,52 @@ vtkSmartPointer<vtkVolume> assembleVolumeI16(vtkImageData* img,
|
|||
|
||||
vtkNew<vtkPiecewiseFunction> opacity;
|
||||
opacity->AddPoint(static_cast<double>(geopro::core::ScalarVolumeI16::kBlank), 0.0);
|
||||
opacity->AddPoint(qminD, 0.0);
|
||||
opacity->AddPoint(qmaxD, kMaxOpacity);
|
||||
for (int t = 0; t < kTransferSamples; ++t) {
|
||||
const double qd = qminD + (qmaxD - qminD) * t / (kTransferSamples - 1);
|
||||
const auto qvLevel = static_cast<std::int16_t>(std::lround(qd));
|
||||
const double phys = q.toPhys(qvLevel);
|
||||
opacity->AddPoint(qd, cs.colorAt(phys).a / 255.0 * cs.globalOpacity());
|
||||
}
|
||||
|
||||
return assembleVolume(img, color, opacity);
|
||||
// 由预建 short 体扫出二值 mask(kBlank→0 跳过)。稠密体(无 kBlank)→ 全 255,等价无 mask。
|
||||
int dims[3];
|
||||
img->GetDimensions(dims);
|
||||
vtkUnsignedCharArray* mArr = nullptr;
|
||||
auto mask = makeMaskLike(dims[0], dims[1], dims[2], img->GetOrigin()[0], img->GetOrigin()[1],
|
||||
img->GetOrigin()[2], img->GetSpacing()[0], img->GetSpacing()[1],
|
||||
img->GetSpacing()[2], mArr);
|
||||
if (auto* sc = vtkShortArray::SafeDownCast(img->GetPointData()->GetScalars())) {
|
||||
const vtkIdType n = sc->GetNumberOfTuples();
|
||||
for (vtkIdType id = 0; id < n; ++id)
|
||||
mArr->SetValue(id, sc->GetValue(id) == geopro::core::ScalarVolumeI16::kBlank ? 0 : 255);
|
||||
}
|
||||
return assembleVolume(img, color, opacity, mask);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void setVolumeGpuSupported(bool ok) { g_gpuVolumeSupported = ok; }
|
||||
|
||||
void updateVolumeColors(vtkVolume* volume, const geopro::core::ColorScale& cs, double vmin,
|
||||
double vmax) {
|
||||
if (!volume || !volume->GetProperty()) return;
|
||||
if (vmin >= vmax) vmax = vmin + 1.0;
|
||||
const double blank = sentinel(vmin);
|
||||
// 与 buildVoxel(float 路径) 同口径重建颜色/不透明度传函,原地换到已有 actor 上(不重建 image →
|
||||
// 切片基底不变、不被关闭)。
|
||||
vtkNew<vtkColorTransferFunction> color;
|
||||
vtkNew<vtkPiecewiseFunction> opacity;
|
||||
opacity->AddPoint(blank, 0.0);
|
||||
for (int t = 0; t < kTransferSamples; ++t) {
|
||||
const double val = vmin + (vmax - vmin) * t / (kTransferSamples - 1);
|
||||
const auto c = cs.colorAt(val);
|
||||
color->AddRGBPoint(val, c.r / 255.0, c.g / 255.0, c.b / 255.0);
|
||||
opacity->AddPoint(val, c.a / 255.0 * cs.globalOpacity());
|
||||
}
|
||||
volume->GetProperty()->SetColor(color);
|
||||
volume->GetProperty()->SetScalarOpacity(opacity);
|
||||
}
|
||||
|
||||
vtkSmartPointer<vtkVolume> buildVoxel(const geopro::core::ScalarVolume& vol,
|
||||
const geopro::core::ColorScale& cs,
|
||||
double ox, double oy, double oz,
|
||||
|
|
@ -109,6 +209,10 @@ vtkSmartPointer<vtkVolume> buildVoxel(const geopro::core::ScalarVolume& vol,
|
|||
|
||||
// 标量用 float(非 double):OpenGL 无原生 double 体纹理,GPU 体绘制对 double 处理不稳/部分驱动间歇
|
||||
// 出空(偶发不渲染根因之一),且省一半显存。float 精度对可视化足够。
|
||||
// 二值 mask:NaN 空格→0(光线投射跳过,真白化),有值→255。与标量同循环填,免二次扫描。
|
||||
vtkUnsignedCharArray* mArr = nullptr;
|
||||
auto mask = makeMaskLike(nx, ny, nz, ox, oy, oz, dx, dy, dz, mArr);
|
||||
|
||||
vtkNew<vtkFloatArray> sc;
|
||||
sc->SetName("v");
|
||||
sc->SetNumberOfTuples(static_cast<vtkIdType>(nx) * ny * nz);
|
||||
|
|
@ -118,7 +222,9 @@ vtkSmartPointer<vtkVolume> buildVoxel(const geopro::core::ScalarVolume& vol,
|
|||
for (int i = 0; i < nx; ++i) {
|
||||
const double v = vol.at(i, j, k);
|
||||
const vtkIdType id = (static_cast<vtkIdType>(k) * ny + j) * nx + i;
|
||||
sc->SetValue(id, static_cast<float>(std::isnan(v) ? blank : v)); // NaN → 哨兵
|
||||
const bool isBlank = std::isnan(v);
|
||||
sc->SetValue(id, static_cast<float>(isBlank ? blank : v)); // NaN → 哨兵
|
||||
mArr->SetValue(id, isBlank ? 0 : 255);
|
||||
}
|
||||
img->GetPointData()->SetScalars(sc);
|
||||
outImage = img;
|
||||
|
|
@ -131,13 +237,17 @@ vtkSmartPointer<vtkVolume> buildVoxel(const geopro::core::ScalarVolume& vol,
|
|||
color->AddRGBPoint(val, c.r / 255.0, c.g / 255.0, c.b / 255.0);
|
||||
}
|
||||
|
||||
// 不透明度传递函数:哨兵 → 0(透明);[vmin,vmax] 线性递增到 kMaxOpacity。
|
||||
// 不透明度传递函数:哨兵 → 0(透明);区间内由色阶 alpha 驱动,再乘体密度主控 kMaxOpacity。
|
||||
// 体素不透明度 = (色阶 alpha/255) × kMaxOpacity(整体透明度已在配置时乘进 alpha)。
|
||||
// alpha=0 → 真透明;alpha=255(无 alpha 色阶默认)→ 维持 kMaxOpacity 的通透手感,不回归。
|
||||
vtkNew<vtkPiecewiseFunction> opacity;
|
||||
opacity->AddPoint(blank, 0.0);
|
||||
opacity->AddPoint(vmin, 0.0);
|
||||
opacity->AddPoint(vmax, kMaxOpacity);
|
||||
for (int t = 0; t < kTransferSamples; ++t) {
|
||||
const double val = vmin + (vmax - vmin) * t / (kTransferSamples - 1);
|
||||
opacity->AddPoint(val, cs.colorAt(val).a / 255.0 * cs.globalOpacity());
|
||||
}
|
||||
|
||||
return assembleVolume(img, color, opacity);
|
||||
return assembleVolume(img, color, opacity, mask);
|
||||
}
|
||||
|
||||
vtkSmartPointer<vtkVolume> buildVoxelI16(const geopro::core::ScalarVolumeI16& vol,
|
||||
|
|
@ -158,6 +268,10 @@ vtkSmartPointer<vtkVolume> buildVoxelI16(const geopro::core::ScalarVolumeI16& vo
|
|||
img->SetOrigin(ox, oy, oz);
|
||||
img->SetSpacing(dx, dy, dz);
|
||||
|
||||
// 二值 mask:kBlank 空格→0(真白化跳过),有值→255。与标量同循环填。
|
||||
vtkUnsignedCharArray* mArr = nullptr;
|
||||
auto mask = makeMaskLike(nx, ny, nz, ox, oy, oz, dx, dy, dz, mArr);
|
||||
|
||||
vtkNew<vtkShortArray> sc;
|
||||
sc->SetName("v");
|
||||
sc->SetNumberOfTuples(static_cast<vtkIdType>(nx) * ny * nz);
|
||||
|
|
@ -169,6 +283,7 @@ vtkSmartPointer<vtkVolume> buildVoxelI16(const geopro::core::ScalarVolumeI16& vo
|
|||
const std::int16_t qv = vol.at(i, j, k);
|
||||
const vtkIdType id = (static_cast<vtkIdType>(k) * ny + j) * nx + i;
|
||||
sc->SetValue(id, qv);
|
||||
mArr->SetValue(id, qv == geopro::core::ScalarVolumeI16::kBlank ? 0 : 255);
|
||||
}
|
||||
img->GetPointData()->SetScalars(sc);
|
||||
outImage = img;
|
||||
|
|
@ -189,13 +304,17 @@ vtkSmartPointer<vtkVolume> buildVoxelI16(const geopro::core::ScalarVolumeI16& vo
|
|||
color->AddRGBPoint(qd, c.r / 255.0, c.g / 255.0, c.b / 255.0);
|
||||
}
|
||||
|
||||
// 不透明度传递函数(量化域):kBlank → 0(透明);[qmin,qmax] 线性递增到 kMaxOpacity。
|
||||
// 不透明度传递函数(量化域):kBlank → 0(透明);区间内由色阶 alpha 驱动 × 体密度主控 kMaxOpacity。
|
||||
vtkNew<vtkPiecewiseFunction> opacity;
|
||||
opacity->AddPoint(static_cast<double>(geopro::core::ScalarVolumeI16::kBlank), 0.0);
|
||||
opacity->AddPoint(qminD, 0.0);
|
||||
opacity->AddPoint(qmaxD, kMaxOpacity);
|
||||
for (int t = 0; t < kTransferSamples; ++t) {
|
||||
const double qd = qminD + (qmaxD - qminD) * t / (kTransferSamples - 1);
|
||||
const auto qvLevel = static_cast<std::int16_t>(std::lround(qd));
|
||||
const double phys = q.toPhys(qvLevel);
|
||||
opacity->AddPoint(qd, cs.colorAt(phys).a / 255.0 * cs.globalOpacity());
|
||||
}
|
||||
|
||||
return assembleVolume(img, color, opacity);
|
||||
return assembleVolume(img, color, opacity, mask);
|
||||
}
|
||||
|
||||
vtkSmartPointer<vtkVolume> buildVoxel(const geopro::core::ScalarVolume& vol,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,15 @@
|
|||
#include "model/ScalarVolumeI16.hpp"
|
||||
namespace geopro::render {
|
||||
|
||||
// 设置是否支持 GPU 体绘制(启动探测后调一次)。false → 体绘制回退 SmartVolumeMapper(CPU 自适应)、
|
||||
// 不建 mask(空值仍透明,仅边缘少了 mask 的干净度)。默认 true。
|
||||
void setVolumeGpuSupported(bool ok);
|
||||
|
||||
// 原地更新已渲染体的颜色/不透明度(仅换传函,不重建 image):色阶改动时用,避免重建 image 把切片基底
|
||||
// 换掉、连带关闭未保存切片。float 体路径口径(标准 addVolume 产物)。
|
||||
void updateVolumeColors(vtkVolume* volume, const geopro::core::ColorScale& cs, double vmin,
|
||||
double vmax);
|
||||
|
||||
// 体上抽等值面(marching cubes/FlyingEdges)→ 不透明实心 actor,凸显超阈异常体(参考图红块)。
|
||||
// img 为 buildVoxel 暴露的 vtkImageData(标量=物理值,留空=哨兵 vmin-1,低于任意 isoValue 不成面)。
|
||||
// isoValue 在 [vmin,vmax] 内;颜色取 ColorScale 在 isoValue 处的实色、不透明。无超阈区 → 返回 nullptr。
|
||||
|
|
|
|||
|
|
@ -29,6 +29,15 @@ std::array<double, 6> imageBounds(vtkImageData* img) {
|
|||
if (img) img->GetBounds(b.data());
|
||||
return b;
|
||||
}
|
||||
// 据三维体总不透明度(0~1)把切片不透明度模式解析为具体总不透明度(0~1)。
|
||||
double resolveSliceOpacity(SliceOpacityMode mode, double volumeOpacity01) {
|
||||
switch (mode) {
|
||||
case SliceOpacityMode::Full: return 1.0; // 100% 不透明
|
||||
case SliceOpacityMode::VolumePlus50: return std::min(1.0, volumeOpacity01 + 0.5);
|
||||
case SliceOpacityMode::FollowVolume: return volumeOpacity01;
|
||||
}
|
||||
return 1.0;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
InteractionManager::InteractionManager(vtkRenderWindowInteractor* interactor,
|
||||
|
|
@ -110,8 +119,19 @@ void InteractionManager::setVolumeImage(const std::string& volumeDsId, vtkImageD
|
|||
const geopro::core::ColorScale& cs, double vmin, double vmax) {
|
||||
if (volumeDsId.empty()) return;
|
||||
auto it = volumes_.find(volumeDsId);
|
||||
// 同体 image 变更(重建/改色阶):旧 image 即将失效 → 先关该体已显示切片(上层 syncSlices 用新 image 重现)。
|
||||
if (it != volumes_.end() && it->second.image != image) closeSlicesOfVolume(volumeDsId);
|
||||
if (it != volumes_.end() && it->second.image != image) {
|
||||
// image 变(体重建):旧 image 即将失效 → 关该体切片(上层 syncSlices 用新 image 重现已保存切片)。
|
||||
closeSlicesOfVolume(volumeDsId);
|
||||
} else if (it != volumes_.end()) {
|
||||
// image 不变、仅色阶变(原地改色):该体下【未保存】切片跟随改色(颜色 + 按模式重解析总不透明度);
|
||||
// 已保存切片用自己的色阶、不动。切片不被关闭,解决"改体色阶刷掉未保存切片"。
|
||||
for (const auto& s : slices_) {
|
||||
if (s->volumeDsId() != volumeDsId || !s->dsId().empty()) continue;
|
||||
s->setColorScale(cs);
|
||||
s->setOpacity(s->opacityMode(), resolveSliceOpacity(s->opacityMode(), cs.globalOpacity()));
|
||||
}
|
||||
safeRender();
|
||||
}
|
||||
volumes_[volumeDsId] = VolumeImg{image, cs, vmin, vmax};
|
||||
}
|
||||
|
||||
|
|
@ -321,6 +341,53 @@ void InteractionManager::tagSelectedSlice(const std::string& dsId) {
|
|||
slices_[static_cast<std::size_t>(selected_)]->setInteractive(false); // 保存即定稿锁定(不可改)
|
||||
}
|
||||
|
||||
void InteractionManager::setSelectedSliceOpacity(SliceOpacityMode mode) {
|
||||
if (selected_ < 0 || selected_ >= static_cast<int>(slices_.size())) return;
|
||||
SliceTool* s = slices_[static_cast<std::size_t>(selected_)].get();
|
||||
const VolumeImg* v = volumeOf(s->volumeDsId());
|
||||
const double volOp = v ? v->cs.globalOpacity() : 1.0; // 三维体的总不透明度(参照量)
|
||||
s->setOpacity(mode, resolveSliceOpacity(mode, volOp));
|
||||
safeRender();
|
||||
}
|
||||
|
||||
SliceOpacityMode InteractionManager::selectedSliceOpacityMode() const {
|
||||
if (selected_ < 0 || selected_ >= static_cast<int>(slices_.size())) return SliceOpacityMode::Full;
|
||||
return slices_[static_cast<std::size_t>(selected_)]->opacityMode();
|
||||
}
|
||||
|
||||
double InteractionManager::selectedSliceOpacity() const {
|
||||
if (selected_ < 0 || selected_ >= static_cast<int>(slices_.size())) return 1.0;
|
||||
return slices_[static_cast<std::size_t>(selected_)]->opacity();
|
||||
}
|
||||
|
||||
geopro::core::ColorScale InteractionManager::selectedSliceColorScaleSnapshot() const {
|
||||
if (selected_ < 0 || selected_ >= static_cast<int>(slices_.size())) return {};
|
||||
const SliceTool* s = slices_[static_cast<std::size_t>(selected_)].get();
|
||||
geopro::core::ColorScale cs = s->colorScale(); // 颜色快照(来自三维体)
|
||||
cs.setGlobalOpacity(s->opacity()); // 当前总不透明度并入 → 切片自己的色阶对象
|
||||
return cs;
|
||||
}
|
||||
|
||||
void InteractionManager::setSelectedSliceColorScale(const geopro::core::ColorScale& cs) {
|
||||
if (selected_ < 0 || selected_ >= static_cast<int>(slices_.size())) return;
|
||||
SliceTool* s = slices_[static_cast<std::size_t>(selected_)].get();
|
||||
s->setColorScale(cs);
|
||||
s->setOpacity(SliceOpacityMode::Full, cs.globalOpacity()); // 已保存切片:不透明度取自其色阶
|
||||
safeRender();
|
||||
}
|
||||
|
||||
bool InteractionManager::setSliceColorScaleByDsId(const std::string& dsId,
|
||||
const geopro::core::ColorScale& cs) {
|
||||
for (const auto& s : slices_) {
|
||||
if (s->dsId() != dsId) continue;
|
||||
s->setColorScale(cs);
|
||||
s->setOpacity(SliceOpacityMode::Full, cs.globalOpacity()); // 不透明度并入其色阶 globalOpacity
|
||||
safeRender();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
vtkImageData* InteractionManager::selectedSliceImage() const {
|
||||
if (selected_ < 0 || selected_ >= static_cast<int>(slices_.size())) return nullptr;
|
||||
return slices_[static_cast<std::size_t>(selected_)]->reslicedOutput();
|
||||
|
|
|
|||
|
|
@ -92,6 +92,20 @@ public:
|
|||
std::string selectedSliceVolumeDsId() const;
|
||||
// 给当前选中(未保存)切片打 dsId 标签:保存=把当前切片链接到新数据集(不重绘、不重复)。
|
||||
void tagSelectedSlice(const std::string& dsId);
|
||||
|
||||
// ── 切片不透明度(与三维体解耦;总不透明度据所属体不透明度解析)──────────────
|
||||
// 设置选中切片不透明度模式。无选中则忽略。
|
||||
void setSelectedSliceOpacity(SliceOpacityMode mode);
|
||||
// 选中切片当前不透明度模式(菜单勾选当前项用)。无选中返回 Full。
|
||||
SliceOpacityMode selectedSliceOpacityMode() const;
|
||||
// 选中切片当前解析后的总不透明度(0~1)(保存切片取具体值用)。无选中返回 1。
|
||||
double selectedSliceOpacity() const;
|
||||
// 选中切片色阶快照(颜色 + 当前总不透明度并入 globalOpacity):保存切片建自己色阶对象用。
|
||||
geopro::core::ColorScale selectedSliceColorScaleSnapshot() const;
|
||||
// 用给定色阶覆盖选中切片(已保存切片编辑自己色阶时用;不透明度取该色阶 globalOpacity)。
|
||||
void setSelectedSliceColorScale(const geopro::core::ColorScale& cs);
|
||||
// 用给定色阶覆盖指定 dsId 的已显示切片(还原/编辑已保存切片色阶时用,不依赖选中)。找到返回 true。
|
||||
bool setSliceColorScaleByDsId(const std::string& dsId, const geopro::core::ColorScale& cs);
|
||||
// 选中切片的重采样 2D 标量影像(导出 dat 用);无选中返回 nullptr。
|
||||
vtkImageData* selectedSliceImage() const;
|
||||
// 选中切片"上色后"的 2D 影像(导出图片用):重采样标量经切片色阶 LUT → RGB 图;无选中返回 nullptr。
|
||||
|
|
|
|||
|
|
@ -34,9 +34,37 @@ void SliceTool::initWidget(const geopro::core::ColorScale& cs, double vmin, doub
|
|||
widget_->TextureInterpolateOn();
|
||||
widget_->DisplayTextOff();
|
||||
|
||||
// 色阶 LUT 套用:用户自管 LUT(不让 widget 用默认灰度窗位)。
|
||||
auto lut = buildLut(cs, vmin, vmax);
|
||||
// 色阶/区间存为单一真源;总不透明度(opacity_)默认 100%。LUT 由 rebuildLut 统一建。
|
||||
cs_ = cs;
|
||||
vmin_ = vmin;
|
||||
vmax_ = vmax;
|
||||
rebuildLut();
|
||||
}
|
||||
|
||||
void SliceTool::rebuildLut() {
|
||||
if (!widget_) return;
|
||||
double vmin = vmin_, vmax = vmax_;
|
||||
if (vmin >= vmax) vmax = vmin + 1.0;
|
||||
// 切片渲染单一真源:颜色/单色 alpha 取自 cs_,总不透明度由 opacity_ 覆盖(与三维体解耦)。
|
||||
geopro::core::ColorScale c = cs_;
|
||||
c.setGlobalOpacity(opacity_);
|
||||
// transparentBelowRange=true:体的留空哨兵(vmin-1.0)在切片上映射为透明,白化区不再填蓝。
|
||||
auto lut = buildLut(c, vmin, vmax, 256, /*transparentBelowRange=*/true);
|
||||
widget_->SetLookupTable(lut);
|
||||
// 关键:钉死 window/level 到 [vmin,vmax]。否则 vtkImagePlaneWidget 会按【输入标量范围】
|
||||
// (含哨兵)自动拉伸映射,把哨兵顶到最低色格填蓝(实测 tests/spike/slice_alpha_probe 确诊)。
|
||||
widget_->SetWindowLevel(vmax - vmin, 0.5 * (vmin + vmax));
|
||||
}
|
||||
|
||||
void SliceTool::setOpacity(SliceOpacityMode mode, double resolved01) {
|
||||
opacityMode_ = mode;
|
||||
opacity_ = resolved01 < 0.0 ? 0.0 : (resolved01 > 1.0 ? 1.0 : resolved01);
|
||||
rebuildLut();
|
||||
}
|
||||
|
||||
void SliceTool::setColorScale(const geopro::core::ColorScale& cs) {
|
||||
cs_ = cs;
|
||||
rebuildLut();
|
||||
}
|
||||
|
||||
void SliceTool::applyMarginsAndActivate() {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,10 @@ class vtkTrivialProducer;
|
|||
|
||||
namespace geopro::render::interact {
|
||||
|
||||
// 切片不透明度模式(用户右键设置):满(100%)/ 三维体+50% / 跟随三维体。
|
||||
// 实际渲染用的"总不透明度"= 据所属三维体的不透明度解析(见 InteractionManager::resolveSliceOpacity)。
|
||||
enum class SliceOpacityMode { Full, VolumePlus50, FollowVolume };
|
||||
|
||||
// 单个切片工具:封装 vtkImagePlaneWidget。
|
||||
// 内部对体素 vtkImageData 做 reslice + 纹理着色(spec §9.1 钉死 reslice 路线,非 cutter)。
|
||||
// 轴向(UpDown/FrontBack/LeftRight):SetPlaneOrientationToX/Y/Z,角度固定。
|
||||
|
|
@ -88,6 +92,17 @@ public:
|
|||
// 切回零重建。重显时复原锁定态(SetEnabled 可能把交互重置为开)。
|
||||
void setVisible(bool on);
|
||||
|
||||
// ── 不透明度(切片独立,与三维体解耦)──────────────────────────────────
|
||||
// 设置不透明度模式 + 已解析的总不透明度(0~1)。颜色映射/单色 alpha 仍由色阶(cs_)给,
|
||||
// 这里只决定整条切片的"总不透明度"(= cs_.globalOpacity 在切片渲染时被它覆盖)。
|
||||
void setOpacity(SliceOpacityMode mode, double resolved01);
|
||||
SliceOpacityMode opacityMode() const { return opacityMode_; }
|
||||
double opacity() const { return opacity_; } // 已解析的总不透明度(0~1)
|
||||
|
||||
// 切换本切片色阶(颜色):未保存切片跟随三维体改色、已保存切片用自己的色阶。重建 LUT。
|
||||
void setColorScale(const geopro::core::ColorScale& cs);
|
||||
const geopro::core::ColorScale& colorScale() const { return cs_; }
|
||||
|
||||
// 关闭:Off() 并解除 interactor 绑定(幂等)。
|
||||
void close();
|
||||
|
||||
|
|
@ -104,7 +119,15 @@ private:
|
|||
|
||||
void initWidget(const geopro::core::ColorScale& cs, double vmin, double vmax); // 共享 widget 配置
|
||||
void applyMarginsAndActivate(); // 按 axis 设旋转锁 + On() + 装交互观察者
|
||||
void rebuildLut(); // 据 cs_ + opacity_ 重建 LUT 并钉死 window/level
|
||||
std::array<double, 6> imageBounds() const;
|
||||
|
||||
// 色阶(颜色) + 区间 + 总不透明度:切片渲染单一真源(cs_ 提供颜色/单色 alpha,opacity_ 覆盖总不透明度)。
|
||||
geopro::core::ColorScale cs_;
|
||||
double vmin_ = 0.0;
|
||||
double vmax_ = 1.0;
|
||||
double opacity_ = 1.0; // 已解析的总不透明度(0~1),默认 100%
|
||||
SliceOpacityMode opacityMode_ = SliceOpacityMode::Full;
|
||||
};
|
||||
|
||||
} // namespace geopro::render::interact
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ find_package(VTK REQUIRED COMPONENTS
|
|||
CommonColor
|
||||
FiltersGeometry
|
||||
FiltersModeling
|
||||
FiltersSources
|
||||
ImagingCore
|
||||
InteractionWidgets
|
||||
RenderingOpenGL2
|
||||
IOImage
|
||||
)
|
||||
|
|
@ -19,3 +22,9 @@ add_executable(render_verify render_verify.cpp)
|
|||
target_link_libraries(render_verify PRIVATE
|
||||
geopro_render geopro_data geopro_core ${VTK_LIBRARIES})
|
||||
vtk_module_autoinit(TARGETS render_verify MODULES ${VTK_LIBRARIES})
|
||||
|
||||
# 实测:vtkImagePlaneWidget 纹理是否支持 texel alpha 透明(白化切片可行性)。
|
||||
add_executable(slice_alpha_probe slice_alpha_probe.cpp)
|
||||
target_link_libraries(slice_alpha_probe PRIVATE
|
||||
geopro_render geopro_core ${VTK_LIBRARIES})
|
||||
vtk_module_autoinit(TARGETS slice_alpha_probe MODULES ${VTK_LIBRARIES})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,161 @@
|
|||
// 实测:vtkImagePlaneWidget 的纹理平面能否把 LUT 下溢透明(alpha=0)的 texel 渲染成透明。
|
||||
// 构造一张图:左半真值(映射红)、右半哨兵 -1(下溢→透明)。纹理平面背后放一块不透明【黄】板,
|
||||
// 背景【绿】。套用 buildLut(transparentBelowRange=true) 后离屏渲染、回读像素分类:
|
||||
// 右半空白处出现【黄】 → texel alpha 透明【成功】(黄板透过来)
|
||||
// 出现【蓝】 → 值被钳到最低色(透明没生效)
|
||||
// 出现【黑】 → alpha 被丢
|
||||
// 纯实测,不靠文档措辞猜。
|
||||
#include <cstdio>
|
||||
|
||||
#include <vtkActor.h>
|
||||
#include <vtkFloatArray.h>
|
||||
#include <vtkImageData.h>
|
||||
#include <vtkImageMapToColors.h>
|
||||
#include <vtkImagePlaneWidget.h>
|
||||
#include <vtkLookupTable.h>
|
||||
#include <vtkNew.h>
|
||||
#include <vtkPNGWriter.h>
|
||||
#include <vtkPlaneSource.h>
|
||||
#include <vtkPointData.h>
|
||||
#include <vtkPolyDataMapper.h>
|
||||
#include <vtkProperty.h>
|
||||
#include <vtkRenderWindow.h>
|
||||
#include <vtkRenderWindowInteractor.h>
|
||||
#include <vtkRenderer.h>
|
||||
#include <vtkTrivialProducer.h>
|
||||
#include <vtkUnsignedCharArray.h>
|
||||
#include <vtkWindowToImageFilter.h>
|
||||
#include <vtkCamera.h>
|
||||
|
||||
#include "ColorLutBuilder.hpp"
|
||||
#include "model/ColorScale.hpp"
|
||||
|
||||
int main() {
|
||||
using namespace geopro;
|
||||
|
||||
// 1) 构造体: 40x40x3, 左半 i<20 → 200(真值,映射红); 右半 → -1(哨兵,下溢透明)。
|
||||
const int nx = 40, ny = 40, nz = 3;
|
||||
vtkNew<vtkImageData> img;
|
||||
img->SetDimensions(nx, ny, nz);
|
||||
img->SetOrigin(0, 0, 0);
|
||||
img->SetSpacing(1, 1, 1);
|
||||
vtkNew<vtkFloatArray> sc;
|
||||
sc->SetName("v");
|
||||
sc->SetNumberOfTuples(static_cast<vtkIdType>(nx) * ny * nz);
|
||||
// 全值域核查:左侧 [0..nx-9] 列填 0→200 渐变(真数据),右侧 8 列填 -1(哨兵/白化)。
|
||||
// 验证 (a) 颜色映射跨整个 [vmin,vmax] 是否正确;(b) 空值是否透明。
|
||||
const int blankCols = 8;
|
||||
const int gradCols = nx - blankCols;
|
||||
for (int k = 0; k < nz; ++k)
|
||||
for (int j = 0; j < ny; ++j)
|
||||
for (int i = 0; i < nx; ++i) {
|
||||
const vtkIdType id = (static_cast<vtkIdType>(k) * ny + j) * nx + i;
|
||||
const float v = (i < gradCols)
|
||||
? static_cast<float>(200.0 * i / (gradCols - 1)) // 0→200 渐变
|
||||
: -1.0F; // 哨兵
|
||||
sc->SetValue(id, v);
|
||||
}
|
||||
img->GetPointData()->SetScalars(sc);
|
||||
|
||||
// 2) 三色阶(全不透明): 0→蓝, 100→黄, 200→红。渐变应呈 蓝→黄→红;-1 应透明(露品红背板)。
|
||||
core::ColorScale cs;
|
||||
cs.addStop(0.0, core::Rgba{0, 0, 255, 255});
|
||||
cs.addStop(100.0, core::Rgba{255, 255, 0, 255});
|
||||
cs.addStop(200.0, core::Rgba{255, 0, 0, 255});
|
||||
auto lut = render::buildLut(cs, 0.0, 200.0, 256, /*transparentBelowRange=*/true);
|
||||
|
||||
// 3) 渲染器 + 离屏窗口 + 交互器(widget 必需)。背景绿。
|
||||
vtkNew<vtkRenderer> ren;
|
||||
ren->SetBackground(0.15, 0.15, 0.15); // 深灰 = 背景
|
||||
vtkNew<vtkRenderWindow> rw;
|
||||
rw->SetOffScreenRendering(1);
|
||||
rw->AddRenderer(ren);
|
||||
rw->SetSize(400, 400);
|
||||
vtkNew<vtkRenderWindowInteractor> iren;
|
||||
iren->SetRenderWindow(rw);
|
||||
|
||||
// 4) 纹理平面【背后】放一块不透明黄板(z=0.5, 在切片 z=1 之下=远离上方相机)。
|
||||
vtkNew<vtkPlaneSource> yp;
|
||||
yp->SetOrigin(0, 0, 0.5);
|
||||
yp->SetPoint1(nx - 1, 0, 0.5);
|
||||
yp->SetPoint2(0, ny - 1, 0.5);
|
||||
vtkNew<vtkPolyDataMapper> ym;
|
||||
ym->SetInputConnection(yp->GetOutputPort());
|
||||
vtkNew<vtkActor> ya;
|
||||
ya->SetMapper(ym);
|
||||
ya->GetProperty()->SetColor(1.0, 0.0, 1.0); // 品红 = 背板(空值透明处会露出它)
|
||||
ya->GetProperty()->LightingOff();
|
||||
ren->AddActor(ya);
|
||||
|
||||
// 5) 真 vtkImagePlaneWidget: Z 法向切片 1(z=1), 套用户 LUT, 最近邻避免边界混色。
|
||||
vtkNew<vtkTrivialProducer> prod;
|
||||
prod->SetOutput(img);
|
||||
vtkNew<vtkImagePlaneWidget> w;
|
||||
w->SetInteractor(iren);
|
||||
w->SetInputConnection(prod->GetOutputPort());
|
||||
w->SetPlaneOrientationToZAxes();
|
||||
w->SetSliceIndex(1);
|
||||
w->SetResliceInterpolateToNearestNeighbour();
|
||||
w->TextureInterpolateOff();
|
||||
w->SetLookupTable(lut);
|
||||
// 钉死 window/level 到 [vmin,vmax]=[0,200],阻止 widget 按输入标量范围自动拉伸。
|
||||
w->SetWindowLevel(200.0, 100.0);
|
||||
w->On();
|
||||
|
||||
// 6) 顶视相机(沿 -Z 俯视), 平行投影框住平面。
|
||||
ren->GetActiveCamera()->SetPosition(nx / 2.0, ny / 2.0, 100.0);
|
||||
ren->GetActiveCamera()->SetFocalPoint(nx / 2.0, ny / 2.0, 1.0);
|
||||
ren->GetActiveCamera()->SetViewUp(0, 1, 0);
|
||||
ren->GetActiveCamera()->ParallelProjectionOn();
|
||||
ren->ResetCamera();
|
||||
rw->Render();
|
||||
|
||||
// 7) 回读像素, 分类计数。
|
||||
vtkNew<vtkWindowToImageFilter> w2i;
|
||||
w2i->SetInput(rw);
|
||||
w2i->Update();
|
||||
vtkImageData* out = w2i->GetOutput();
|
||||
{
|
||||
vtkNew<vtkPNGWriter> pw;
|
||||
pw->SetFileName("D:/dev/spike_data/slice_alpha_probe.png");
|
||||
pw->SetInputConnection(w2i->GetOutputPort());
|
||||
pw->Write();
|
||||
}
|
||||
int dims[3];
|
||||
out->GetDimensions(dims);
|
||||
long red = 0, yellow = 0, green = 0, blue = 0, black = 0, other = 0;
|
||||
for (int y = 0; y < dims[1]; ++y)
|
||||
for (int x = 0; x < dims[0]; ++x) {
|
||||
const auto* p = static_cast<unsigned char*>(out->GetScalarPointer(x, y, 0));
|
||||
const int r = p[0], g = p[1], b = p[2];
|
||||
if (r > 150 && g < 100 && b < 100) ++red;
|
||||
else if (r > 150 && g > 150 && b < 100) ++yellow;
|
||||
else if (r < 100 && g > 150 && b < 100) ++green;
|
||||
else if (b > 150 && r < 100 && g < 100) ++blue;
|
||||
else if (r < 60 && g < 60 && b < 60) ++black;
|
||||
else ++other;
|
||||
}
|
||||
|
||||
std::printf("PIXELS red(data)=%ld yellow(behind-shows=TRANSPARENT)=%ld "
|
||||
"green(bg)=%ld blue(clamp-lowest)=%ld black(alpha-dropped)=%ld other=%ld\n",
|
||||
red, yellow, green, blue, black, other);
|
||||
|
||||
// 8) 直接判读 colormap 对下溢值输出的 RGBA(隔离 LUT 正确性 vs 纹理渲染)。
|
||||
if (auto* cm = w->GetColorMap()) {
|
||||
cm->Update();
|
||||
if (auto* co = cm->GetOutput()) {
|
||||
int cd[3];
|
||||
co->GetDimensions(cd);
|
||||
const int comps = co->GetNumberOfScalarComponents();
|
||||
// 右半某点(i=30,j=20,值=150) colormap 应产出 alpha=0。
|
||||
const auto* px = static_cast<unsigned char*>(co->GetScalarPointer(30, 20, 0));
|
||||
std::printf("COLORMAP comps=%d rightHalfTexel.RGBA=", comps);
|
||||
for (int c = 0; c < comps; ++c) std::printf("%d ", px[c]);
|
||||
std::printf("(期望 alpha=0 => LUT 正确产出透明)\n");
|
||||
}
|
||||
}
|
||||
|
||||
std::printf("VERDICT: 若 yellow 远多于 blue/black => widget 纹理【支持】texel alpha 透明; "
|
||||
"若 blue 多 => 钳最低色; 若 black 多 => 丢 alpha\n");
|
||||
return 0;
|
||||
}
|
||||
Loading…
Reference in New Issue