From 14a9e8f627ebd7760985cd1fe473745700425af4 Mon Sep 17 00:00:00 2001 From: lh <2334563547@qq.com> Date: Fri, 18 Sep 2026 17:39:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BF=AE=E6=AD=A3=E5=8D=95=E5=80=BC?= =?UTF-8?q?=E5=B1=9E=E6=80=A7=E8=89=B2=E6=A0=87=E6=98=A0=E5=B0=84=E4=B8=8E?= =?UTF-8?q?=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将自动单值属性映射到当前色带的最小端颜色 - 在色带上下端显示真实值,手工范围恢复连续渐变 - 完善 Linear 和 Log 单值内部范围校验 - 在读取色带矩形前刷新 VTK 内部布局 - 修正标题、端点标签及上下越界说明的定位与避让 - 同步标题、标签格式和刻度数量变化后的 Overlay 布局 --- Include/nmNum/nmRender/nmVTKScalarBarLayer.h | 11 +- Src/nmNum/nmRender/nmVTKScalarBarLayer.cpp | 246 +++++++++++++++--- .../nmSubWxs/nmPebiResultSceneController.cpp | 101 ++++--- 3 files changed, 276 insertions(+), 82 deletions(-) diff --git a/Include/nmNum/nmRender/nmVTKScalarBarLayer.h b/Include/nmNum/nmRender/nmVTKScalarBarLayer.h index 3227ded3..1aba50e4 100644 --- a/Include/nmNum/nmRender/nmVTKScalarBarLayer.h +++ b/Include/nmNum/nmRender/nmVTKScalarBarLayer.h @@ -39,7 +39,7 @@ public: bool setLabelFormat(const QString& sFormat); /** @brief 返回调用方最后设置的普通刻度格式。 */ QString getLabelFormat() const; - /** @brief 设置 0~64 个普通刻度;常量色标使用 0。 */ + /** @brief 设置 0~64 个普通刻度;单值色标使用 0。 */ bool setNumberOfLabels(int nLabelCount); /** @brief 返回当前普通刻度数量。 */ int getNumberOfLabels() const; @@ -57,9 +57,9 @@ public: void setAboveRangeSwatch(bool bVisible, const QString& sText); /** - * @brief 显示或隐藏常量属性的唯一业务值。 + * @brief 在色带上下端同时显示或隐藏单值属性的真实业务值。 * - * 该文字由独立 vtkTextActor 绘制,不启用共享 LUT 的 IndexedLookup, + * 两个文字由独立 vtkTextActor 绘制,不启用共享 LUT 的 IndexedLookup, * 因而不会改变网格使用的连续颜色映射语义。 */ void setConstantValueLabel(bool bVisible, const QString& sText); @@ -84,7 +84,8 @@ private: vtkSmartPointer m_pRootAssembly; ///< Scene 挂载的稳定根 Prop。 vtkSmartPointer m_pActor; ///< 图层独占的二维色标 Actor。 vtkSmartPointer m_pTitleActor; ///< 独立左对齐 ASCII 标题。 - vtkSmartPointer m_pConstantValueActor; ///< 常量属性唯一值文字。 + vtkSmartPointer m_pConstantMinimumValueActor; ///< 单值属性下端值文字。 + vtkSmartPointer m_pConstantMaximumValueActor; ///< 单值属性上端值文字。 vtkSmartPointer m_pBelowRangeSwatchPoints; ///< 下越界色块四角坐标。 vtkSmartPointer m_pBelowRangeSwatchActor; ///< 下越界独立色块。 vtkSmartPointer m_pBelowRangeTextActor; ///< 下越界说明文字。 @@ -93,7 +94,7 @@ private: vtkSmartPointer m_pAboveRangeTextActor; ///< 上越界说明文字。 QString m_sTitle; ///< 保留无损 Qt 标题,避免从 VTK 窄字符串反向解析。 QString m_sLabelFormat; ///< 普通刻度的 printf 风格格式字符串。 - QString m_sConstantValueText; ///< 保留常量值文字,避免从 VTK 反向解析。 + QString m_sConstantValueText; ///< 保留单值属性文字,避免从 VTK 反向解析。 QString m_sBelowRangeText; ///< 下越界说明文字状态。 QString m_sAboveRangeText; ///< 上越界说明文字状态。 double m_dPositionX; ///< 色标左下角的归一化视口 X 坐标。 diff --git a/Src/nmNum/nmRender/nmVTKScalarBarLayer.cpp b/Src/nmNum/nmRender/nmVTKScalarBarLayer.cpp index d54d4af5..86607825 100644 --- a/Src/nmNum/nmRender/nmVTKScalarBarLayer.cpp +++ b/Src/nmNum/nmRender/nmVTKScalarBarLayer.cpp @@ -10,17 +10,51 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include namespace { +/** + * @brief 只公开 VTK 色标的按需布局入口,供独立 Overlay 在 Render 前读取新几何。 + */ +class vtkNmScalarBarActor : public vtkScalarBarActor +{ +public: + static vtkNmScalarBarActor* New(); + vtkTypeMacro(vtkNmScalarBarActor, vtkScalarBarActor); + + /** @brief 立即同步内部色带、刻度和文字布局,不触发 Renderer 绘制。 */ + int refreshLayout(vtkViewport* pViewport) + { + return pViewport != nullptr + ? RebuildLayoutIfNeeded(pViewport) : 0; + } + +protected: + vtkNmScalarBarActor() + { + } + + virtual ~vtkNmScalarBarActor() + { + } + +private: + vtkNmScalarBarActor(const vtkNmScalarBarActor&); + void operator=(const vtkNmScalarBarActor&); +}; + +vtkStandardNewMacro(vtkNmScalarBarActor); + const int SCALAR_BAR_LEFT_MARGIN_PIXELS = 20; const int SCALAR_BAR_TOTAL_WIDTH_PIXELS = 108; // VTK 内部留白会压缩色带,目标宽度设为 34px 后可见宽度约为 30px。 @@ -40,6 +74,7 @@ const int SCALAR_BAR_SWATCH_HEIGHT_PIXELS = 12; const int SCALAR_BAR_SWATCH_GAP_PIXELS = 4; const int SCALAR_BAR_TEXT_GAP_PIXELS = 6; const int SCALAR_BAR_LABEL_GAP_PIXELS = 8; +const int SCALAR_BAR_TITLE_CONTENT_GAP_PIXELS = 4; const double SCALAR_BAR_NORMAL_BOTTOM_RATIO = 0.22; const double SCALAR_BAR_NORMAL_HEIGHT_RATIO = 0.67; const double SCALAR_BAR_SMALL_BOTTOM_RATIO = 0.22; @@ -87,7 +122,7 @@ vtkSmartPointer createRangeSwatchActor(vtkPoints* pPoints) return pActor; } -// 独立文本共用字号和颜色;常量值可选择与普通刻度一致的强调样式。 +// 独立文本共用字号和颜色;单值端点可选择与普通刻度一致的强调样式。 void configureOverlayTextActor(vtkTextActor* pActor, bool bUseTickStyle) { @@ -134,6 +169,34 @@ void configureTitleActor(vtkTextActor* pActor) pActor->SetVisibility(0); } +// 优先读取 VTK 按当前 DPI 计算的真实文字高度,尚未挂载时使用稳定回退值。 +double getOverlayTextHeightPixels(vtkTextActor* pActor, + vtkRenderer* pRenderer, + double dFallbackHeight) +{ + if(pActor == nullptr || pRenderer == nullptr) { + return dFallbackHeight; + } + double dSize[2] = { 0.0, 0.0 }; + pActor->GetSize(pRenderer, dSize); + return vtkMath::IsFinite(dSize[1]) && dSize[1] > 0.0 + ? dSize[1] : dFallbackHeight; +} + +// 横向避让仅在极小视口空间不足时使用,正常布局仍保持色块位于色带上方。 +double getOverlayTextWidthPixels(vtkTextActor* pActor, + vtkRenderer* pRenderer, + double dFallbackWidth) +{ + if(pActor == nullptr || pRenderer == nullptr) { + return dFallbackWidth; + } + double dSize[2] = { 0.0, 0.0 }; + pActor->GetSize(pRenderer, dSize); + return vtkMath::IsFinite(dSize[0]) && dSize[0] > 0.0 + ? dSize[0] : dFallbackWidth; +} + // 更新矩形四角并显式标记 Points 已修改,Mapper 会在下一次 Render 使用新位置。 void updateRectangle(vtkPoints* pPoints, double dX, double dY, @@ -152,9 +215,10 @@ nmVTKScalarBarLayer::nmVTKScalarBarLayer(const QString& sLayerId) : nmVTKRenderLayer(sLayerId), m_pLookupTable(nullptr), m_pRootAssembly(vtkSmartPointer::New()), - m_pActor(vtkSmartPointer::New()), + m_pActor(vtkSmartPointer::New()), m_pTitleActor(vtkSmartPointer::New()), - m_pConstantValueActor(vtkSmartPointer::New()), + m_pConstantMinimumValueActor(vtkSmartPointer::New()), + m_pConstantMaximumValueActor(vtkSmartPointer::New()), m_pBelowRangeSwatchPoints(vtkSmartPointer::New()), m_pBelowRangeSwatchActor(nullptr), m_pBelowRangeTextActor(vtkSmartPointer::New()), @@ -204,7 +268,8 @@ nmVTKScalarBarLayer::nmVTKScalarBarLayer(const QString& sLayerId) m_pAboveRangeSwatchActor = createRangeSwatchActor(m_pAboveRangeSwatchPoints); configureTitleActor(m_pTitleActor); - configureOverlayTextActor(m_pConstantValueActor, true); + configureOverlayTextActor(m_pConstantMinimumValueActor, true); + configureOverlayTextActor(m_pConstantMaximumValueActor, true); configureOverlayTextActor(m_pBelowRangeTextActor, false); configureOverlayTextActor(m_pAboveRangeTextActor, false); updateOverlayLayout(); @@ -212,7 +277,8 @@ nmVTKScalarBarLayer::nmVTKScalarBarLayer(const QString& sLayerId) // 根 Assembly 只在构造时注册,运行期仅更新子 Actor 状态。 m_pRootAssembly->AddPart(m_pActor); m_pRootAssembly->AddPart(m_pTitleActor); - m_pRootAssembly->AddPart(m_pConstantValueActor); + m_pRootAssembly->AddPart(m_pConstantMinimumValueActor); + m_pRootAssembly->AddPart(m_pConstantMaximumValueActor); m_pRootAssembly->AddPart(m_pBelowRangeSwatchActor); m_pRootAssembly->AddPart(m_pBelowRangeTextActor); m_pRootAssembly->AddPart(m_pAboveRangeSwatchActor); @@ -232,7 +298,8 @@ nmVTKScalarBarLayer::~nmVTKScalarBarLayer() m_pBelowRangeTextActor = nullptr; m_pBelowRangeSwatchActor = nullptr; m_pBelowRangeSwatchPoints = nullptr; - m_pConstantValueActor = nullptr; + m_pConstantMaximumValueActor = nullptr; + m_pConstantMinimumValueActor = nullptr; m_pTitleActor = nullptr; m_pActor = nullptr; m_pRootAssembly = nullptr; @@ -248,6 +315,7 @@ void nmVTKScalarBarLayer::setLookupTable(vtkLookupTable* pLookupTable) m_pLookupTable = pLookupTable; m_pActor->SetLookupTable(pLookupTable); updateRangeSwatchColors(); + updateOverlayLayout(); } vtkLookupTable* nmVTKScalarBarLayer::getLookupTable() const @@ -267,6 +335,8 @@ void nmVTKScalarBarLayer::setTitle(const QString& sTitle) const QByteArray aTitle = sTitle.toUtf8(); m_pTitleActor->SetInput(aTitle.constData()); m_pTitleActor->SetVisibility(sTitle.isEmpty() ? 0 : 1); + // 标题宽度参与极小视口下 Above max 的横向避让。 + updateOverlayLayout(); } QString nmVTKScalarBarLayer::getTitle() const @@ -283,6 +353,8 @@ bool nmVTKScalarBarLayer::setLabelFormat(const QString& sFormat) m_sLabelFormat = sFormat; const QByteArray aFormat = sFormat.toLatin1(); m_pActor->SetLabelFormat(aFormat.constData()); + // 格式变化可能改变刻度宽度及 VTK 内部实际色带矩形。 + updateOverlayLayout(); return true; } @@ -298,6 +370,8 @@ bool nmVTKScalarBarLayer::setNumberOfLabels(int nLabelCount) return false; } m_pActor->SetNumberOfLabels(nLabelCount); + // 标签数量会影响 VTK 内部色带矩形,独立 Overlay 必须同步其最新位置。 + updateOverlayLayout(); return true; } @@ -387,6 +461,7 @@ void nmVTKScalarBarLayer::setBelowRangeSwatch( m_pBelowRangeSwatchActor->SetVisibility(bVisible ? 1 : 0); m_pBelowRangeTextActor->SetVisibility( bVisible && !m_sBelowRangeText.isEmpty() ? 1 : 0); + updateOverlayLayout(); } void nmVTKScalarBarLayer::setAboveRangeSwatch( @@ -403,6 +478,7 @@ void nmVTKScalarBarLayer::setAboveRangeSwatch( m_pAboveRangeSwatchActor->SetVisibility(bVisible ? 1 : 0); m_pAboveRangeTextActor->SetVisibility( bVisible && !m_sAboveRangeText.isEmpty() ? 1 : 0); + updateOverlayLayout(); } void nmVTKScalarBarLayer::setConstantValueLabel( @@ -416,9 +492,13 @@ void nmVTKScalarBarLayer::setConstantValueLabel( m_sConstantValueText = bVisible ? sText : QString(); const QByteArray aText = m_sConstantValueText.toUtf8(); - m_pConstantValueActor->SetInput(aText.constData()); - m_pConstantValueActor->SetVisibility( - bVisible && !m_sConstantValueText.isEmpty() ? 1 : 0); + m_pConstantMinimumValueActor->SetInput(aText.constData()); + m_pConstantMaximumValueActor->SetInput(aText.constData()); + const int nVisibility = + bVisible && !m_sConstantValueText.isEmpty() ? 1 : 0; + m_pConstantMinimumValueActor->SetVisibility(nVisibility); + m_pConstantMaximumValueActor->SetVisibility(nVisibility); + updateOverlayLayout(); } bool nmVTKScalarBarLayer::setPosition(double dX, double dY) @@ -460,33 +540,49 @@ void nmVTKScalarBarLayer::updateOverlayLayout() ? 1.0 / m_nViewportWidth : 0.01; const double dPixelY = bHasViewport ? 1.0 / m_nViewportHeight : 0.01; + double dColorX = m_dPositionX; + double dColorY = m_dPositionY; double dColorWidth = bHasViewport ? SCALAR_BAR_VISIBLE_COLOR_WIDTH_PIXELS * dPixelX : m_dWidth * 0.33; + double dColorHeight = m_dHeight; if(dColorWidth > m_dWidth) { dColorWidth = m_dWidth; } - // 常量值与普通刻度共用色带右侧的固定像素起点。 - const double dLabelX = m_dPositionX + dColorWidth + - SCALAR_BAR_LABEL_GAP_PIXELS * dPixelX; - const double dLabelY = m_dPositionY + m_dHeight * 0.5; - m_pConstantValueActor->GetPositionCoordinate()->SetValue( - dLabelX, dLabelY, 0.0); + vtkRenderer* pRenderer = getAttachedRenderer(); + vtkNmScalarBarActor* pLayoutActor = + vtkNmScalarBarActor::SafeDownCast(m_pActor); + if(bHasViewport && pRenderer != nullptr && + m_pLookupTable != nullptr && pLayoutActor != nullptr && + pLayoutActor->refreshLayout(pRenderer)) { + // vtkScalarBarActor 的 Position/Size 是包含刻度留白的外框;端点和 + // 其他装饰必须先刷新布局,再读取最终计算出的实际彩色色带矩形。 + int nColorBarRect[4] = { 0, 0, 0, 0 }; + m_pActor->GetScalarBarRect(nColorBarRect, pRenderer); + double dLeft = nColorBarRect[0]; + double dBottom = nColorBarRect[1]; + double dRight = nColorBarRect[0] + nColorBarRect[2]; + double dTop = nColorBarRect[1] + nColorBarRect[3]; + pRenderer->ViewportToNormalizedViewport(dLeft, dBottom); + pRenderer->ViewportToNormalizedViewport(dRight, dTop); + if(vtkMath::IsFinite(dLeft) && vtkMath::IsFinite(dBottom) && + vtkMath::IsFinite(dRight) && vtkMath::IsFinite(dTop) && + dRight > dLeft && dTop > dBottom) { + dColorX = dLeft; + dColorY = dBottom; + dColorWidth = dRight - dLeft; + dColorHeight = dTop - dBottom; + } + } - // 标题通常位于色带左上方;极小视口下向下钳制,避免越过视口顶边。 - const double dDesiredTitleY = - m_dPositionY + m_dHeight + - SCALAR_BAR_TITLE_OFFSET_Y_PIXELS * dPixelY; - const double dMaximumTitleY = bHasViewport - ? qMax(0.0, 1.0 - - SCALAR_BAR_TITLE_SAFE_HEIGHT_PIXELS * dPixelY) - : dDesiredTitleY; - const double dTitleY = qMin(dDesiredTitleY, dMaximumTitleY); - m_pTitleActor->GetPositionCoordinate()->SetValue( - m_dPositionX, - dTitleY, - 0.0); + // 单值属性在色带上下端重复显示真实值,位置与普通端点刻度一致。 + const double dLabelX = dColorX + dColorWidth + + SCALAR_BAR_LABEL_GAP_PIXELS * dPixelX; + m_pConstantMinimumValueActor->GetPositionCoordinate()->SetValue( + dLabelX, dColorY, 0.0); + m_pConstantMaximumValueActor->GetPositionCoordinate()->SetValue( + dLabelX, dColorY + dColorHeight, 0.0); // 越界色块和文字也按实际像素布局,窗口缩放时不会横向漂移。 const double dSwatchWidth = bHasViewport @@ -498,21 +594,101 @@ void nmVTKScalarBarLayer::updateOverlayLayout() ? SCALAR_BAR_SWATCH_GAP_PIXELS * dPixelY : 0.012; const double dTextGap = bHasViewport ? SCALAR_BAR_TEXT_GAP_PIXELS * dPixelX : 0.008; + const bool bConstantLabelsVisible = + m_pConstantMinimumValueActor->GetVisibility() != 0 && + !m_sConstantValueText.isEmpty(); + const double dConstantTextHeight = getOverlayTextHeightPixels( + m_pConstantMinimumValueActor, pRenderer, + SCALAR_BAR_LABEL_FONT_SIZE); + const bool bBelowRangeVisible = + m_pBelowRangeSwatchActor->GetVisibility() != 0; + const bool bAboveRangeVisible = + m_pAboveRangeSwatchActor->GetVisibility() != 0; + const double dBelowTextHeight = getOverlayTextHeightPixels( + m_pBelowRangeTextActor, pRenderer, + SCALAR_BAR_LABEL_FONT_SIZE); + const double dAboveTextHeight = getOverlayTextHeightPixels( + m_pAboveRangeTextActor, pRenderer, + SCALAR_BAR_LABEL_FONT_SIZE); + const double dBelowClearance = + bConstantLabelsVisible && bBelowRangeVisible + ? qMax(0.0, + (dConstantTextHeight + dBelowTextHeight - + SCALAR_BAR_SWATCH_HEIGHT_PIXELS) * 0.5 * dPixelY) + : 0.0; + const double dAboveClearance = + bConstantLabelsVisible && bAboveRangeVisible + ? qMax(0.0, + (dConstantTextHeight + dAboveTextHeight - + SCALAR_BAR_SWATCH_HEIGHT_PIXELS) * 0.5 * dPixelY) + : 0.0; + // 单值端点与越界说明可以同时启用。将越界注记整体向外错开, + // 保证其文字区域不会覆盖位于实际色带端点的单值标签。 const double dBelowY = - m_dPositionY - dSwatchGap - dSwatchHeight; - const double dAboveY = - m_dPositionY + m_dHeight + dSwatchGap; + dColorY - dSwatchGap - dSwatchHeight - + dBelowClearance; + double dAboveX = dColorX; + double dAboveY = + dColorY + dColorHeight + dSwatchGap + + dAboveClearance; + + // 标题默认保持原有偏移;Above max 向外避让后若更高,则标题继续上移。 + const double dAboveTextHalfHeight = + m_pAboveRangeTextActor->GetVisibility() != 0 + ? dAboveTextHeight * 0.5 * dPixelY : 0.0; + const double dAboveContentHalfHeight = qMax( + dSwatchHeight * 0.5, dAboveTextHalfHeight); + const double dAboveContentTop = + dAboveY + dSwatchHeight * 0.5 + + dAboveContentHalfHeight; + const double dBaseTitleY = + dColorY + dColorHeight + + SCALAR_BAR_TITLE_OFFSET_Y_PIXELS * dPixelY; + const double dDesiredTitleY = bAboveRangeVisible + ? qMax(dBaseTitleY, + dAboveContentTop + + SCALAR_BAR_TITLE_CONTENT_GAP_PIXELS * dPixelY) + : dBaseTitleY; + const double dMaximumTitleY = bHasViewport + ? qMax(0.0, 1.0 - + SCALAR_BAR_TITLE_SAFE_HEIGHT_PIXELS * dPixelY) + : dDesiredTitleY; + const double dTitleY = qMin(dDesiredTitleY, dMaximumTitleY); + + if(bAboveRangeVisible && + dTitleY < dAboveContentTop + + SCALAR_BAR_TITLE_CONTENT_GAP_PIXELS * dPixelY) { + // 极小视口无法同时向上容纳标题和 Above max。此时保持标题不越界, + // 并把越界注记放到标题、单值标签两者的右侧,消除水平交叠。 + const double dConstantTextWidth = bConstantLabelsVisible + ? getOverlayTextWidthPixels( + m_pConstantMaximumValueActor, pRenderer, + m_sConstantValueText.length() * + SCALAR_BAR_LABEL_FONT_SIZE * 0.6) + : 0.0; + const double dTitleWidth = getOverlayTextWidthPixels( + m_pTitleActor, pRenderer, + m_sTitle.length() * + SCALAR_BAR_TITLE_FONT_SIZE * 0.6); + dAboveX = qMax( + dLabelX + dConstantTextWidth * dPixelX + dTextGap, + dColorX + dTitleWidth * dPixelX + dTextGap); + dAboveY = dColorY + dColorHeight - dSwatchHeight * 0.5; + } + + m_pTitleActor->GetPositionCoordinate()->SetValue( + dColorX, dTitleY, 0.0); updateRectangle(m_pBelowRangeSwatchPoints, - m_dPositionX, dBelowY, + dColorX, dBelowY, dSwatchWidth, dSwatchHeight); updateRectangle(m_pAboveRangeSwatchPoints, - m_dPositionX, dAboveY, + dAboveX, dAboveY, dSwatchWidth, dSwatchHeight); m_pBelowRangeTextActor->GetPositionCoordinate()->SetValue( - m_dPositionX + dSwatchWidth + dTextGap, + dColorX + dSwatchWidth + dTextGap, dBelowY + dSwatchHeight * 0.5, 0.0); m_pAboveRangeTextActor->GetPositionCoordinate()->SetValue( - m_dPositionX + dSwatchWidth + dTextGap, + dAboveX + dSwatchWidth + dTextGap, dAboveY + dSwatchHeight * 0.5, 0.0); } diff --git a/Src/nmNum/nmSubWxs/nmPebiResultSceneController.cpp b/Src/nmNum/nmSubWxs/nmPebiResultSceneController.cpp index 15f79e83..c7b0b562 100644 --- a/Src/nmNum/nmSubWxs/nmPebiResultSceneController.cpp +++ b/Src/nmNum/nmSubWxs/nmPebiResultSceneController.cpp @@ -119,6 +119,7 @@ bool getFiniteBounds(vtkDataSet* pDataSet, double dBounds[6]) bool getInternalDisplayRange(const double dVisibleRange[2], bool bLogarithmic, + bool bSingleValueMode, double dInternalRange[2]) { if(dVisibleRange == nullptr || dInternalRange == nullptr || @@ -129,58 +130,65 @@ bool getInternalDisplayRange(const double dVisibleRange[2], return false; } - dInternalRange[0] = dVisibleRange[0]; - dInternalRange[1] = dVisibleRange[1]; if(dVisibleRange[0] < dVisibleRange[1]) { + dInternalRange[0] = dVisibleRange[0]; + dInternalRange[1] = dVisibleRange[1]; return true; } + if(!bSingleValueMode || dVisibleRange[0] != dVisibleRange[1]) { + return false; + } - // VTK 不接受零宽范围。常量值只扩展内部 LUT,业务和可见范围仍为 [c,c]。 + // VTK 不接受零宽范围。单值模式从真实值单向扩展,使 c 位于 LUT 最小端。 const double dValue = dVisibleRange[0]; - double dMinimum = 0.0; double dMaximum = 0.0; if(bLogarithmic) { - dMinimum = dValue * (1.0 - 1.0e-6); dMaximum = dValue * (1.0 + 1.0e-6); - if(!vtkMath::IsFinite(dMinimum) || dMinimum <= 0.0 || - dMinimum >= dValue) { - const double dHalfValue = dValue / 2.0; - dMinimum = dHalfValue > 0.0 && dHalfValue < dValue - ? dHalfValue : dValue; - } if(!vtkMath::IsFinite(dMaximum) || dMaximum <= dValue) { - const double dTwiceValue = dValue * 2.0; - dMaximum = vtkMath::IsFinite(dTwiceValue) && - dTwiceValue > dValue ? dTwiceValue : dValue; + dMaximum = dValue * 2.0; } - } else if(dValue == 0.0) { - dMinimum = -1.0e-6; - dMaximum = 1.0e-6; } else { const double dEpsilon = qMax(1.0, fabs(dValue)) * 1.0e-6; - dMinimum = dValue - dEpsilon; dMaximum = dValue + dEpsilon; - if(!vtkMath::IsFinite(dMinimum) || dMinimum >= dValue) { - dMinimum = dValue > 0.0 ? dValue / 2.0 : dValue; - } - if(!vtkMath::IsFinite(dMaximum) || dMaximum <= dValue) { - dMaximum = dValue < 0.0 ? dValue / 2.0 : dValue; - } } - if(!vtkMath::IsFinite(dMinimum) || !vtkMath::IsFinite(dMaximum) || - dMinimum >= dMaximum || (bLogarithmic && dMinimum <= 0.0)) { + if(!vtkMath::IsFinite(dMaximum) || dValue >= dMaximum || + (bLogarithmic && dValue <= 0.0)) { return false; } - dInternalRange[0] = dMinimum; + dInternalRange[0] = dValue; dInternalRange[1] = dMaximum; return true; } +bool fillLookupTableWithFirstColor(vtkLookupTable* pLookupTable) +{ + if(pLookupTable == nullptr || + pLookupTable->GetNumberOfTableValues() <= 0) { + return false; + } + + // 保留用户选择的色带语义,但让单值网格和色标统一使用第 0 个普通表项。 + double dFirstColor[4] = { 0.0, 0.0, 0.0, 1.0 }; + pLookupTable->GetTableValue(0, dFirstColor); + for(int nComponent = 0; nComponent < 4; ++nComponent) { + if(!vtkMath::IsFinite(dFirstColor[nComponent])) { + return false; + } + } + + const int nTableValueCount = static_cast( + pLookupTable->GetNumberOfTableValues()); + for(int nIndex = 0; nIndex < nTableValueCount; ++nIndex) { + pLookupTable->SetTableValue(nIndex, dFirstColor); + } + return true; +} + struct ResultDisplayCandidate { ResultDisplayCandidate() : m_pLookupTable(nullptr), - m_bConstantValueLabel(false) + m_bSingleValueMode(false) { m_dBusinessRange[0] = m_dBusinessRange[1] = 0.0; m_dVisibleRange[0] = m_dVisibleRange[1] = 0.0; @@ -194,7 +202,7 @@ struct ResultDisplayCandidate double m_dInternalRange[2]; vtkSmartPointer m_pLookupTable; QString m_sScalarBarTitle; - bool m_bConstantValueLabel; + bool m_bSingleValueMode; }; bool prepareDisplayCandidate( @@ -247,6 +255,10 @@ bool prepareDisplayCandidate( dVisibleRange[1] = oSettings.m_dManualMaximum; } + const bool bSingleValueMode = + !oSettings.m_bHasManualRange && + dBusinessRange[0] == dBusinessRange[1]; + const bool bLogarithmic = oSettings.m_eScaleMode == NM_PebiColorScale_Logarithmic; if(bLogarithmic && @@ -264,7 +276,8 @@ bool prepareDisplayCandidate( oSettings.m_sColorMapId); if(pLookupTable == nullptr || !getInternalDisplayRange(dVisibleRange, bLogarithmic, - dInternalRange)) { + bSingleValueMode, + dInternalRange)) { if(pError != nullptr) { *pError = controllerText("Unable to construct the color mapping."); } @@ -277,6 +290,13 @@ bool prepareDisplayCandidate( } else { pLookupTable->SetScaleToLinear(); } + if(bSingleValueMode && + !fillLookupTableWithFirstColor(pLookupTable)) { + if(pError != nullptr) { + *pError = controllerText("Unable to construct the color mapping."); + } + return false; + } const QColor oBelowColor = oSettings.m_oBelowRangeColor; const QColor oAboveColor = oSettings.m_oAboveRangeColor; pLookupTable->SetBelowRangeColor( @@ -295,9 +315,6 @@ bool prepareDisplayCandidate( pLookupTable->IndexedLookupOff(); pLookupTable->ResetAnnotations(); - const bool bConstantValueLabel = - !oSettings.m_bHasManualRange && - dBusinessRange[0] == dBusinessRange[1]; const QString sTitle = nmPebiResultPropertyDisplayText::getScalarBarTitle(sPropertyId); @@ -310,7 +327,7 @@ bool prepareDisplayCandidate( oCandidate.m_dInternalRange[1] = dInternalRange[1]; oCandidate.m_pLookupTable = pLookupTable; oCandidate.m_sScalarBarTitle = sTitle; - oCandidate.m_bConstantValueLabel = bConstantValueLabel; + oCandidate.m_bSingleValueMode = bSingleValueMode; return true; } @@ -397,7 +414,7 @@ public: m_nViewportHeight(0), m_bHasPropertyRange(false), m_bHasBounds(false), - m_bCurrentConstantValueLabel(false), + m_bCurrentSingleValueMode(false), m_bScalarBarVisible(true), m_bScaleGridVisible(false), m_bWellNamesVisible(true) @@ -689,7 +706,7 @@ public: m_pScalarBarLayer->setViewportSize( m_nViewportWidth, m_nViewportHeight); } - const int nLabelCount = m_bCurrentConstantValueLabel + const int nLabelCount = m_bCurrentSingleValueMode ? 0 : m_pScalarBarLayer->getResponsiveLabelCount(); m_pScalarBarLayer->setNumberOfLabels(nLabelCount); } @@ -702,10 +719,10 @@ public: m_pScalarBarLayer->setLookupTable(oDisplay.m_pLookupTable); m_pScalarBarLayer->setTitle(oDisplay.m_sScalarBarTitle); m_pScalarBarLayer->setLabelFormat(QString::fromLatin1("%.4g")); - m_bCurrentConstantValueLabel = oDisplay.m_bConstantValueLabel; + m_bCurrentSingleValueMode = oDisplay.m_bSingleValueMode; m_pScalarBarLayer->setConstantValueLabel( - oDisplay.m_bConstantValueLabel, - oDisplay.m_bConstantValueLabel + oDisplay.m_bSingleValueMode, + oDisplay.m_bSingleValueMode ? QString::number(oDisplay.m_dBusinessRange[0], 'g', 6) : QString()); m_pScalarBarLayer->setBelowRangeSwatch( @@ -763,7 +780,7 @@ public: m_pScalarBarLayer->setBelowRangeSwatch(false, QString()); m_pScalarBarLayer->setAboveRangeSwatch(false, QString()); m_pScalarBarLayer->setConstantValueLabel(false, QString()); - m_bCurrentConstantValueLabel = false; + m_bCurrentSingleValueMode = false; applyScalarBarLayout(); } applyLayerVisibility(false); @@ -809,7 +826,7 @@ public: m_pScalarBarLayer->setAboveRangeSwatch(false, QString()); m_pScalarBarLayer->setConstantValueLabel(false, QString()); } - m_bCurrentConstantValueLabel = false; + m_bCurrentSingleValueMode = false; applyScalarBarLayout(); if(m_pScaleGridLayer != nullptr) { m_pScaleGridLayer->setVisible(false); @@ -899,7 +916,7 @@ public: double m_dBounds[6]; bool m_bHasPropertyRange; bool m_bHasBounds; - bool m_bCurrentConstantValueLabel; ///< 当前色标是否只显示唯一业务值。 + bool m_bCurrentSingleValueMode; ///< 当前是否为自动范围下的单值纯色模式。 bool m_bScalarBarVisible; bool m_bScaleGridVisible; bool m_bWellNamesVisible;