diff --git a/Include/nmNum/nmRender/nmVTKScalarBarLayer.h b/Include/nmNum/nmRender/nmVTKScalarBarLayer.h index ad8c6284..3227ded3 100644 --- a/Include/nmNum/nmRender/nmVTKScalarBarLayer.h +++ b/Include/nmNum/nmRender/nmVTKScalarBarLayer.h @@ -43,6 +43,13 @@ public: bool setNumberOfLabels(int nLabelCount); /** @brief 返回当前普通刻度数量。 */ int getNumberOfLabels() const; + /** + * @brief 按 QVTK 视口像素尺寸更新固定宽度的响应式二维布局。 + * @note 只修改 Actor 布局,不主动 Render,也不修改 LookupTable。 + */ + bool setViewportSize(int nWidth, int nHeight); + /** @brief 返回当前视口和色带高度建议使用的普通刻度数量。 */ + int getResponsiveLabelCount() const; /** @brief 配置低于显示范围的独立二维色块和说明文字。 */ void setBelowRangeSwatch(bool bVisible, const QString& sText); @@ -76,6 +83,7 @@ private: vtkSmartPointer m_pLookupTable; ///< 共享外部颜色表引用。 vtkSmartPointer m_pRootAssembly; ///< Scene 挂载的稳定根 Prop。 vtkSmartPointer m_pActor; ///< 图层独占的二维色标 Actor。 + vtkSmartPointer m_pTitleActor; ///< 独立左对齐 ASCII 标题。 vtkSmartPointer m_pConstantValueActor; ///< 常量属性唯一值文字。 vtkSmartPointer m_pBelowRangeSwatchPoints; ///< 下越界色块四角坐标。 vtkSmartPointer m_pBelowRangeSwatchActor; ///< 下越界独立色块。 @@ -92,6 +100,8 @@ private: double m_dPositionY; ///< 色标左下角的归一化视口 Y 坐标。 double m_dWidth; ///< 色标在归一化视口中的宽度。 double m_dHeight; ///< 色标在归一化视口中的高度。 + int m_nViewportWidth; ///< 最近一次有效 QVTK 视口像素宽度。 + int m_nViewportHeight; ///< 最近一次有效 QVTK 视口像素高度。 }; #endif // NMVTKSCALARBARLAYER_H diff --git a/Include/nmNum/nmRender/nmWxVTKRenderContainerWidget.h b/Include/nmNum/nmRender/nmWxVTKRenderContainerWidget.h index 96af9036..49b7fb4e 100644 --- a/Include/nmNum/nmRender/nmWxVTKRenderContainerWidget.h +++ b/Include/nmNum/nmRender/nmWxVTKRenderContainerWidget.h @@ -4,6 +4,7 @@ #include "nmRender_global.h" #include "nmVTKViewCommandController.h" +#include #include #include @@ -47,6 +48,8 @@ public: /** @brief 返回 Qt 承载控件;调用方不得删除或重新设置父对象。 */ QVTKWidget* getVtkWidget() const; + /** @brief 返回当前 QVTK 视口的 Qt 像素尺寸;控件不可用时返回空尺寸。 */ + QSize getViewportSize() const; /** @brief 返回当前容器独占的 Renderer 非所有权指针。 */ vtkRenderer* getRenderer() const; /** @brief 返回 QVTKWidget 持有的 RenderWindow 非所有权指针。 */ @@ -114,6 +117,11 @@ signals: void contextMenuAboutToShow(QMenu* pMenu); /** @brief 将容器确认后的拾取状态同步给公共 QAction。 */ void rotationCenterPickingChanged(bool bPicking); + /** + * @brief QVTK 视口尺寸变化时同步发出实际 Qt 像素尺寸。 + * @note 信号在 QVTKWidget 处理 Resize 之前发出,接收方不得主动 Render。 + */ + void viewportSizeChanged(int nWidth, int nHeight); public slots: /** @brief 按固定倍率 1.2 放大,成功后统一渲染一次。 */ diff --git a/Include/nmNum/nmSubWxs/nmPebiResultPropertyDisplayText.h b/Include/nmNum/nmSubWxs/nmPebiResultPropertyDisplayText.h index 9af9d8ac..8fe9402a 100644 --- a/Include/nmNum/nmSubWxs/nmPebiResultPropertyDisplayText.h +++ b/Include/nmNum/nmSubWxs/nmPebiResultPropertyDisplayText.h @@ -8,8 +8,8 @@ /** * @brief 将结果属性目录中的稳定源文字转换为当前语言下的界面文字。 * - * 属性 ID、英文源文字和单位仍由 nmPebiResultPropertyCatalog 唯一维护; - * 本类只负责固定翻译上下文,避免各业务控件重复硬编码属性名称。 + * 属性 ID、英文源文字和单位仍由 nmPebiResultPropertyCatalog 维护; + * 本类统一提供 Qt 翻译文字及 VTK 可稳定绘制的固定 ASCII 短标题。 */ class NM_SUB_WXS_EXPORT nmPebiResultPropertyDisplayText { @@ -20,6 +20,11 @@ public: * @return 翻译后的名称;未知属性返回空字符串。 */ static QString getDisplayName(const QString& sPropertyId); + /** + * @brief 返回 VTK 色标使用的固定 ASCII 短标题。 + * @note 标题不参与翻译;未知属性返回空字符串。 + */ + static QString getScalarBarTitle(const QString& sPropertyId); /** * @brief 返回 Catalog 中的默认显示单位。 * @return 单位原文;未知属性或无量纲属性返回空字符串。 diff --git a/Include/nmNum/nmSubWxs/nmPebiResultSceneController.h b/Include/nmNum/nmSubWxs/nmPebiResultSceneController.h index 44514aa0..a85c8464 100644 --- a/Include/nmNum/nmSubWxs/nmPebiResultSceneController.h +++ b/Include/nmNum/nmSubWxs/nmPebiResultSceneController.h @@ -43,6 +43,12 @@ public: /** @brief 解除管线和快照引用并隐藏结果图层,不销毁 Scene。 */ void clear(); + /** + * @brief 缓存 QVTK 视口尺寸并更新色标二维布局,不主动 Render。 + * @note 尺寸在无快照时仍会保存,后续创建色标图层时立即应用。 + */ + void setViewportSize(int nWidth, int nHeight); + /** * @brief 原子切换同一快照内的属性和帧,成功时仅渲染一次。 * @note 静态属性只接受帧 0;失败时恢复 Adapter、Mapper 和范围。 diff --git a/Include/nmNum/nmSubWxs/nmWxPebiResultViewWidget.h b/Include/nmNum/nmSubWxs/nmWxPebiResultViewWidget.h index 7a13f51a..0038bbc4 100644 --- a/Include/nmNum/nmSubWxs/nmWxPebiResultViewWidget.h +++ b/Include/nmNum/nmSubWxs/nmWxPebiResultViewWidget.h @@ -62,6 +62,8 @@ private slots: void onControlOperationFailed(const QString& sError); /** @brief 明确操作成功后清除旧普通错误,不影响绑定失败或过期状态。 */ void onControlOperationSucceeded(); + /** @brief 将 QVTK 视口尺寸同步给结果场景,不主动触发额外渲染。 */ + void onViewportSizeChanged(int nWidth, int nHeight); /** @brief 切换色标显隐。 */ void onScalarBarToggled(bool bVisible); diff --git a/Src/nmNum/nmRender/nmVTKColorMapCatalog.cpp b/Src/nmNum/nmRender/nmVTKColorMapCatalog.cpp index 25f8c311..f2459b26 100644 --- a/Src/nmNum/nmRender/nmVTKColorMapCatalog.cpp +++ b/Src/nmNum/nmRender/nmVTKColorMapCatalog.cpp @@ -14,6 +14,33 @@ const char* const COLOR_MAP_GAS_SATURATION = "gas-saturation"; const char* const COLOR_MAP_WATER_SATURATION = "water-saturation"; const int COLOR_TABLE_SIZE = 256; +struct RainbowControlPoint +{ + double m_dPosition; + int m_nRed; + int m_nGreen; + int m_nBlue; +}; + +// 固定控制点来自最新 Saphir 截图取样,位置之间统一执行线性 RGB 插值。 +const RainbowControlPoint RAINBOW_CONTROL_POINTS[] = +{ + { 0.00, 0, 0, 202 }, + { 0.10, 0, 109, 226 }, + { 0.20, 0, 223, 252 }, + { 0.30, 0, 247, 133 }, + { 0.40, 0, 254, 13 }, + { 0.50, 104, 255, 0 }, + { 0.60, 243, 255, 0 }, + { 0.70, 255, 203, 0 }, + { 0.80, 255, 136, 0 }, + { 0.90, 235, 73, 0 }, + { 1.00, 215, 12, 0 } +}; +const int RAINBOW_CONTROL_POINT_COUNT = + static_cast(sizeof(RAINBOW_CONTROL_POINTS) / + sizeof(RAINBOW_CONTROL_POINTS[0])); + QString stableId(const char* pId) { return QString::fromLatin1(pId); @@ -33,6 +60,40 @@ QColor interpolateRgb(const QColor& oStart, 255); } +QColor getRainbowColor(double dPosition) +{ + if(dPosition <= RAINBOW_CONTROL_POINTS[0].m_dPosition) { + const RainbowControlPoint& oPoint = RAINBOW_CONTROL_POINTS[0]; + return QColor(oPoint.m_nRed, oPoint.m_nGreen, oPoint.m_nBlue, 255); + } + + for(int nIndex = 1; + nIndex < RAINBOW_CONTROL_POINT_COUNT; + ++nIndex) { + const RainbowControlPoint& oEnd = + RAINBOW_CONTROL_POINTS[nIndex]; + if(dPosition <= oEnd.m_dPosition) { + const RainbowControlPoint& oStart = + RAINBOW_CONTROL_POINTS[nIndex - 1]; + const double dLocalPosition = + (dPosition - oStart.m_dPosition) / + (oEnd.m_dPosition - oStart.m_dPosition); + return interpolateRgb( + QColor(oStart.m_nRed, + oStart.m_nGreen, + oStart.m_nBlue), + QColor(oEnd.m_nRed, + oEnd.m_nGreen, + oEnd.m_nBlue), + dLocalPosition); + } + } + + const RainbowControlPoint& oLast = + RAINBOW_CONTROL_POINTS[RAINBOW_CONTROL_POINT_COUNT - 1]; + return QColor(oLast.m_nRed, oLast.m_nGreen, oLast.m_nBlue, 255); +} + } QStringList nmVTKColorMapCatalog::getColorMapIds() @@ -61,10 +122,7 @@ bool nmVTKColorMapCatalog::getColor(const QString& sColorMapId, QColor oCandidate; if(sColorMapId == stableId(COLOR_MAP_RAINBOW)) { - // Saphir 风格 Rainbow:色相从蓝色 240 度连续过渡到红色 0 度。 - oCandidate = QColor::fromHsvF( - (1.0 - dPosition) * (240.0 / 360.0), - 1.0, 1.0, 1.0); + oCandidate = getRainbowColor(dPosition); } else if(sColorMapId == stableId(COLOR_MAP_OIL_SATURATION)) { oCandidate = interpolateRgb( QColor(255, 255, 255), QColor(0, 255, 0), dPosition); diff --git a/Src/nmNum/nmRender/nmVTKScalarBarLayer.cpp b/Src/nmNum/nmRender/nmVTKScalarBarLayer.cpp index 33ad8f6a..d54d4af5 100644 --- a/Src/nmNum/nmRender/nmVTKScalarBarLayer.cpp +++ b/Src/nmNum/nmRender/nmVTKScalarBarLayer.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -20,6 +21,30 @@ namespace { +const int SCALAR_BAR_LEFT_MARGIN_PIXELS = 20; +const int SCALAR_BAR_TOTAL_WIDTH_PIXELS = 108; +// VTK 内部留白会压缩色带,目标宽度设为 34px 后可见宽度约为 30px。 +const int SCALAR_BAR_COLOR_WIDTH_PIXELS = 34; +const int SCALAR_BAR_VISIBLE_COLOR_WIDTH_PIXELS = 30; +const int SCALAR_BAR_TITLE_FONT_SIZE = 18; +const int SCALAR_BAR_TITLE_SAFE_HEIGHT_PIXELS = 24; +const int SCALAR_BAR_LABEL_FONT_SIZE = 15; +const int SCALAR_BAR_SMALL_VIEWPORT_HEIGHT = 450; +const int SCALAR_BAR_MEDIUM_VIEWPORT_HEIGHT = 650; +const int SCALAR_BAR_LABEL_SPACING_PIXELS = 80; +const int SCALAR_BAR_MIN_LABEL_COUNT = 6; +const int SCALAR_BAR_MAX_LABEL_COUNT = 9; +const int SCALAR_BAR_TITLE_OFFSET_Y_PIXELS = 28; +const int SCALAR_BAR_SWATCH_WIDTH_PIXELS = 16; +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 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; +const double SCALAR_BAR_SMALL_HEIGHT_RATIO = 0.62; + // ScalarBarActor 和 LookupTable 均只能在 Qt GUI 线程中修改。 bool isScalarBarGuiThread() { @@ -62,18 +87,49 @@ vtkSmartPointer createRangeSwatchActor(vtkPoints* pPoints) return pActor; } -// 统一独立文本样式,使常量值和越界说明与普通色标刻度保持一致。 -void configureOverlayTextActor(vtkTextActor* pActor) +// 独立文本共用字号和颜色;常量值可选择与普通刻度一致的强调样式。 +void configureOverlayTextActor(vtkTextActor* pActor, + bool bUseTickStyle) +{ + pActor->SetInput(""); + pActor->SetTextScaleModeToNone(); + pActor->GetTextProperty()->SetFontFamilyToArial(); + pActor->GetTextProperty()->SetFontSize(SCALAR_BAR_LABEL_FONT_SIZE); + pActor->GetTextProperty()->SetColor(1.0, 1.0, 1.0); + if(bUseTickStyle) { + pActor->GetTextProperty()->BoldOn(); + pActor->GetTextProperty()->ItalicOn(); + pActor->GetTextProperty()->ShadowOn(); + pActor->GetTextProperty()->SetShadowOffset(1, -1); + } else { + pActor->GetTextProperty()->BoldOff(); + pActor->GetTextProperty()->ItalicOff(); + pActor->GetTextProperty()->ShadowOff(); + } + pActor->GetTextProperty()->SetJustificationToLeft(); + pActor->GetTextProperty()->SetVerticalJustificationToCentered(); + pActor->GetPositionCoordinate()->SetCoordinateSystemToNormalizedViewport(); + pActor->GetProperty()->SetDisplayLocationToForeground(); + pActor->PickableOff(); + pActor->SetVisibility(0); +} + +// 独立标题不受 vtkScalarBarActor 内部居中布局影响,始终保持左对齐。 +void configureTitleActor(vtkTextActor* pActor) { pActor->SetInput(""); pActor->SetTextScaleModeToNone(); - pActor->GetTextProperty()->SetFontSize(11); + pActor->GetTextProperty()->SetFontFamilyToArial(); + pActor->GetTextProperty()->SetFontSize(SCALAR_BAR_TITLE_FONT_SIZE); + pActor->GetTextProperty()->SetColor(1.0, 1.0, 1.0); pActor->GetTextProperty()->BoldOff(); pActor->GetTextProperty()->ItalicOff(); - pActor->GetTextProperty()->ShadowOff(); + pActor->GetTextProperty()->ShadowOn(); + pActor->GetTextProperty()->SetShadowOffset(1, -1); pActor->GetTextProperty()->SetJustificationToLeft(); - pActor->GetTextProperty()->SetVerticalJustificationToCentered(); + pActor->GetTextProperty()->SetVerticalJustificationToBottom(); pActor->GetPositionCoordinate()->SetCoordinateSystemToNormalizedViewport(); + pActor->GetProperty()->SetDisplayLocationToForeground(); pActor->PickableOff(); pActor->SetVisibility(0); } @@ -97,6 +153,7 @@ nmVTKScalarBarLayer::nmVTKScalarBarLayer(const QString& sLayerId) m_pLookupTable(nullptr), m_pRootAssembly(vtkSmartPointer::New()), m_pActor(vtkSmartPointer::New()), + m_pTitleActor(vtkSmartPointer::New()), m_pConstantValueActor(vtkSmartPointer::New()), m_pBelowRangeSwatchPoints(vtkSmartPointer::New()), m_pBelowRangeSwatchActor(nullptr), @@ -108,21 +165,33 @@ nmVTKScalarBarLayer::nmVTKScalarBarLayer(const QString& sLayerId) m_dPositionX(0.0), m_dPositionY(0.0), m_dWidth(0.1), - m_dHeight(0.8) + m_dHeight(0.8), + m_nViewportWidth(0), + m_nViewportHeight(0) { Q_ASSERT(isScalarBarGuiThread()); // vtkScalarBarActor 是二维 Prop,仍通过图层基类统一完成挂载和显隐。 m_pActor->SetNumberOfLabels(5); m_pActor->SetLabelFormat("%.6g"); + m_pActor->SetTitle(""); + m_pActor->SetOrientationToVertical(); + m_pActor->SetTextPositionToSucceedScalarBar(); + m_pActor->SetTextPad(3); m_pActor->UnconstrainedFontSizeOn(); - m_pActor->GetLabelTextProperty()->SetFontSize(11); - m_pActor->GetLabelTextProperty()->BoldOff(); - m_pActor->GetLabelTextProperty()->ItalicOff(); - m_pActor->GetLabelTextProperty()->ShadowOff(); - m_pActor->GetTitleTextProperty()->SetFontSize(12); - m_pActor->GetTitleTextProperty()->BoldOff(); - m_pActor->GetTitleTextProperty()->ItalicOff(); - m_pActor->GetTitleTextProperty()->ShadowOff(); + m_pActor->GetLabelTextProperty()->SetFontFamilyToArial(); + m_pActor->GetLabelTextProperty()->SetFontSize( + SCALAR_BAR_LABEL_FONT_SIZE); + m_pActor->GetLabelTextProperty()->SetColor(1.0, 1.0, 1.0); + m_pActor->GetLabelTextProperty()->BoldOn(); + m_pActor->GetLabelTextProperty()->ItalicOn(); + m_pActor->GetLabelTextProperty()->ShadowOn(); + m_pActor->GetLabelTextProperty()->SetShadowOffset(1, -1); + m_pActor->SetMaximumWidthInPixels(SCALAR_BAR_TOTAL_WIDTH_PIXELS); + m_pActor->SetBarRatio( + static_cast(SCALAR_BAR_COLOR_WIDTH_PIXELS) / + static_cast(SCALAR_BAR_TOTAL_WIDTH_PIXELS)); + m_pActor->GetProperty()->SetDisplayLocationToForeground(); + m_pActor->PickableOff(); m_pActor->DrawAnnotationsOff(); m_pActor->DrawBelowRangeSwatchOff(); m_pActor->DrawAboveRangeSwatchOff(); @@ -134,13 +203,15 @@ nmVTKScalarBarLayer::nmVTKScalarBarLayer(const QString& sLayerId) createRangeSwatchActor(m_pBelowRangeSwatchPoints); m_pAboveRangeSwatchActor = createRangeSwatchActor(m_pAboveRangeSwatchPoints); - configureOverlayTextActor(m_pConstantValueActor); - configureOverlayTextActor(m_pBelowRangeTextActor); - configureOverlayTextActor(m_pAboveRangeTextActor); + configureTitleActor(m_pTitleActor); + configureOverlayTextActor(m_pConstantValueActor, true); + configureOverlayTextActor(m_pBelowRangeTextActor, false); + configureOverlayTextActor(m_pAboveRangeTextActor, false); updateOverlayLayout(); // 根 Assembly 只在构造时注册,运行期仅更新子 Actor 状态。 m_pRootAssembly->AddPart(m_pActor); + m_pRootAssembly->AddPart(m_pTitleActor); m_pRootAssembly->AddPart(m_pConstantValueActor); m_pRootAssembly->AddPart(m_pBelowRangeSwatchActor); m_pRootAssembly->AddPart(m_pBelowRangeTextActor); @@ -162,6 +233,7 @@ nmVTKScalarBarLayer::~nmVTKScalarBarLayer() m_pBelowRangeSwatchActor = nullptr; m_pBelowRangeSwatchPoints = nullptr; m_pConstantValueActor = nullptr; + m_pTitleActor = nullptr; m_pActor = nullptr; m_pRootAssembly = nullptr; } @@ -190,9 +262,11 @@ void nmVTKScalarBarLayer::setTitle(const QString& sTitle) return; } m_sTitle = sTitle; - // VTK 7.1 接口接收窄字符串,统一以 UTF-8 临时缓冲传入并由 Actor 复制。 + // 内置标题永久留空,避免其按整个 ScalarBarActor 宽度自动居中。 + m_pActor->SetTitle(""); const QByteArray aTitle = sTitle.toUtf8(); - m_pActor->SetTitle(aTitle.constData()); + m_pTitleActor->SetInput(aTitle.constData()); + m_pTitleActor->SetVisibility(sTitle.isEmpty() ? 0 : 1); } QString nmVTKScalarBarLayer::getTitle() const @@ -232,6 +306,73 @@ int nmVTKScalarBarLayer::getNumberOfLabels() const return m_pActor != nullptr ? m_pActor->GetNumberOfLabels() : 0; } +bool nmVTKScalarBarLayer::setViewportSize(int nWidth, int nHeight) +{ + Q_ASSERT(isScalarBarGuiThread()); + if(!isScalarBarGuiThread() || nWidth <= 0 || nHeight <= 0) { + return false; + } + + m_nViewportWidth = nWidth; + m_nViewportHeight = nHeight; + const bool bSmallViewport = + nHeight < SCALAR_BAR_SMALL_VIEWPORT_HEIGHT; + const double dBottomRatio = bSmallViewport + ? SCALAR_BAR_SMALL_BOTTOM_RATIO + : SCALAR_BAR_NORMAL_BOTTOM_RATIO; + const double dHeightRatio = bSmallViewport + ? SCALAR_BAR_SMALL_HEIGHT_RATIO + : SCALAR_BAR_NORMAL_HEIGHT_RATIO; + + // 小于目标宽度的极端视口会安全收窄;正常窗口保持固定像素宽度。 + const int nLeftMargin = qMin( + SCALAR_BAR_LEFT_MARGIN_PIXELS, qMax(0, nWidth - 1)); + const int nLayoutWidth = qMin( + SCALAR_BAR_TOTAL_WIDTH_PIXELS, + qMax(1, nWidth - nLeftMargin)); + const int nLayoutHeight = qMax(1, qRound(nHeight * dHeightRatio)); + m_dPositionX = static_cast(nLeftMargin) / nWidth; + m_dPositionY = dBottomRatio; + m_dWidth = static_cast(nLayoutWidth) / nWidth; + m_dHeight = static_cast(nLayoutHeight) / nHeight; + + m_pActor->SetPosition(m_dPositionX, m_dPositionY); + m_pActor->SetWidth(m_dWidth); + m_pActor->SetHeight(m_dHeight); + m_pActor->SetMaximumWidthInPixels(nLayoutWidth); + m_pActor->SetMaximumHeightInPixels(nLayoutHeight); + double dBarRatio = + static_cast(SCALAR_BAR_COLOR_WIDTH_PIXELS) / + static_cast(nLayoutWidth); + if(dBarRatio >= 1.0) { + dBarRatio = 0.95; + } + m_pActor->SetBarRatio(dBarRatio); + updateOverlayLayout(); + return true; +} + +int nmVTKScalarBarLayer::getResponsiveLabelCount() const +{ + if(m_nViewportHeight <= 0) { + return 5; + } + if(m_nViewportHeight < SCALAR_BAR_SMALL_VIEWPORT_HEIGHT) { + return 4; + } + if(m_nViewportHeight <= SCALAR_BAR_MEDIUM_VIEWPORT_HEIGHT) { + return 6; + } + + const int nBarHeight = qMax( + 1, qRound(m_dHeight * m_nViewportHeight)); + return qBound( + SCALAR_BAR_MIN_LABEL_COUNT, + qRound(static_cast(nBarHeight) / + SCALAR_BAR_LABEL_SPACING_PIXELS), + SCALAR_BAR_MAX_LABEL_COUNT); +} + void nmVTKScalarBarLayer::setBelowRangeSwatch( bool bVisible, const QString& sText) @@ -313,22 +454,50 @@ bool nmVTKScalarBarLayer::setSize(double dWidth, double dHeight) void nmVTKScalarBarLayer::updateOverlayLayout() { - // 常量值放在色带右侧的垂直中心,视觉位置与普通刻度标签一致。 - const double dLabelX = m_dPositionX + m_dWidth + 0.01; + const bool bHasViewport = + m_nViewportWidth > 0 && m_nViewportHeight > 0; + const double dPixelX = bHasViewport + ? 1.0 / m_nViewportWidth : 0.01; + const double dPixelY = bHasViewport + ? 1.0 / m_nViewportHeight : 0.01; + double dColorWidth = bHasViewport + ? SCALAR_BAR_VISIBLE_COLOR_WIDTH_PIXELS * dPixelX + : m_dWidth * 0.33; + 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); - // 越界色块位于色带上下两端外侧,避免覆盖连续色带和普通刻度。 - double dSwatchWidth = m_dWidth * 0.25; - if(dSwatchWidth < 0.012) { - dSwatchWidth = 0.012; - } else if(dSwatchWidth > 0.025) { - dSwatchWidth = 0.025; - } - const double dSwatchHeight = 0.018; - const double dSwatchGap = 0.012; - const double dTextGap = 0.008; + // 标题通常位于色带左上方;极小视口下向下钳制,避免越过视口顶边。 + 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 dSwatchWidth = bHasViewport + ? SCALAR_BAR_SWATCH_WIDTH_PIXELS * dPixelX + : qMin(m_dWidth * 0.25, 0.025); + const double dSwatchHeight = bHasViewport + ? SCALAR_BAR_SWATCH_HEIGHT_PIXELS * dPixelY : 0.018; + const double dSwatchGap = bHasViewport + ? SCALAR_BAR_SWATCH_GAP_PIXELS * dPixelY : 0.012; + const double dTextGap = bHasViewport + ? SCALAR_BAR_TEXT_GAP_PIXELS * dPixelX : 0.008; const double dBelowY = m_dPositionY - dSwatchGap - dSwatchHeight; const double dAboveY = diff --git a/Src/nmNum/nmRender/nmWxVTKRenderContainerWidget.cpp b/Src/nmNum/nmRender/nmWxVTKRenderContainerWidget.cpp index c00c673b..16172d15 100644 --- a/Src/nmNum/nmRender/nmWxVTKRenderContainerWidget.cpp +++ b/Src/nmNum/nmRender/nmWxVTKRenderContainerWidget.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -240,6 +241,11 @@ QVTKWidget* nmWxVTKRenderContainerWidget::getVtkWidget() const return m_pVtkWidget; } +QSize nmWxVTKRenderContainerWidget::getViewportSize() const +{ + return m_pVtkWidget != nullptr ? m_pVtkWidget->size() : QSize(); +} + vtkRenderer* nmWxVTKRenderContainerWidget::getRenderer() const { return m_pRenderer; @@ -715,6 +721,17 @@ bool nmWxVTKRenderContainerWidget::eventFilter( return QWidget::eventFilter(pObject, pEvent); } + if(pEvent->type() == QEvent::Resize) { + const QResizeEvent* pResizeEvent = + static_cast(pEvent); + const QSize oViewportSize = pResizeEvent->size(); + if(oViewportSize.width() > 0 && oViewportSize.height() > 0) { + // 只同步布局输入;QVTKWidget 随后处理 Resize 并完成本次绘制。 + emit viewportSizeChanged( + oViewportSize.width(), oViewportSize.height()); + } + } + // 一次性拾取优先于 VTK 相机交互和右键菜单,并继续吞掉对应 Release。 if(m_pRotationCenterPicker != nullptr && (m_pRotationCenterPicker->isActive() || diff --git a/Src/nmNum/nmSubWxs/nmPebiResultPropertyDisplayText.cpp b/Src/nmNum/nmSubWxs/nmPebiResultPropertyDisplayText.cpp index 424f4af8..89382d3f 100644 --- a/Src/nmNum/nmSubWxs/nmPebiResultPropertyDisplayText.cpp +++ b/Src/nmNum/nmSubWxs/nmPebiResultPropertyDisplayText.cpp @@ -34,6 +34,40 @@ QString nmPebiResultPropertyDisplayText::getDisplayName( "nmPebiResultProperty", baSourceText.constData()); } +QString nmPebiResultPropertyDisplayText::getScalarBarTitle( + const QString& sPropertyId) +{ + nmPebiResultPropertyDescriptor oDescriptor; + if(!nmPebiResultPropertyCatalog::findDescriptor( + sPropertyId, oDescriptor)) { + return QString(); + } + + // VTK 标题只映射稳定的 ASCII 属性符号,单位始终由 Catalog 唯一提供。 + QString sTitle; + if(sPropertyId == QString::fromLatin1("pressure")) { + sTitle = QString::fromLatin1("p"); + } + if(sPropertyId == QString::fromLatin1("permeability")) { + sTitle = QString::fromLatin1("k"); + } + if(sPropertyId == QString::fromLatin1("porosity")) { + sTitle = QString::fromLatin1("phi"); + } + if(sPropertyId == QString::fromLatin1("thickness")) { + sTitle = QString::fromLatin1("h"); + } + if(sTitle.isEmpty()) { + return QString(); + } + + if(!oDescriptor.m_sDefaultDisplayUnit.isEmpty()) { + sTitle += QString::fromLatin1(" ") + + oDescriptor.m_sDefaultDisplayUnit; + } + return sTitle; +} + QString nmPebiResultPropertyDisplayText::getDefaultDisplayUnit( const QString& sPropertyId) { diff --git a/Src/nmNum/nmSubWxs/nmPebiResultSceneController.cpp b/Src/nmNum/nmSubWxs/nmPebiResultSceneController.cpp index 7086e017..15f79e83 100644 --- a/Src/nmNum/nmSubWxs/nmPebiResultSceneController.cpp +++ b/Src/nmNum/nmSubWxs/nmPebiResultSceneController.cpp @@ -298,14 +298,8 @@ bool prepareDisplayCandidate( const bool bConstantValueLabel = !oSettings.m_bHasManualRange && dBusinessRange[0] == dBusinessRange[1]; - QString sTitle = - nmPebiResultPropertyDisplayText::getDisplayName(sPropertyId); - const QString sUnit = - nmPebiResultPropertyDisplayText::getDefaultDisplayUnit(sPropertyId); - if(!sUnit.isEmpty()) { - sTitle += QString::fromLatin1(" (") + sUnit + - QString::fromLatin1(")"); - } + const QString sTitle = + nmPebiResultPropertyDisplayText::getScalarBarTitle(sPropertyId); oCandidate.m_oSettings = oSettings; oCandidate.m_dBusinessRange[0] = dBusinessRange[0]; @@ -399,8 +393,11 @@ public: m_pAdapter(nullptr), m_nCurrentFrameIndex(-1), m_nSnapshotBindingRevision(0), + m_nViewportWidth(0), + m_nViewportHeight(0), m_bHasPropertyRange(false), m_bHasBounds(false), + m_bCurrentConstantValueLabel(false), m_bScalarBarVisible(true), m_bScaleGridVisible(false), m_bWellNamesVisible(true) @@ -552,8 +549,14 @@ public: nmVTKScalarBarLayer* pLayer = new nmVTKScalarBarLayer( QString::fromLatin1(RESULT_SCALAR_BAR_LAYER_ID)); pLayer->setVisible(false); - pLayer->setPosition(0.05, 0.1); - pLayer->setSize(0.1, 0.8); + if(m_nViewportWidth > 0 && m_nViewportHeight > 0 && + !pLayer->setViewportSize( + m_nViewportWidth, m_nViewportHeight)) { + delete pLayer; + rollbackCreatedLayers( + bCreatedMesh, false, false, false); + return false; + } if(!pScene->addLayer(pLayer)) { delete pLayer; rollbackCreatedLayers( @@ -677,6 +680,20 @@ public: pLookupTable); } + void applyScalarBarLayout() + { + if(m_pScalarBarLayer == nullptr) { + return; + } + if(m_nViewportWidth > 0 && m_nViewportHeight > 0) { + m_pScalarBarLayer->setViewportSize( + m_nViewportWidth, m_nViewportHeight); + } + const int nLabelCount = m_bCurrentConstantValueLabel + ? 0 : m_pScalarBarLayer->getResponsiveLabelCount(); + m_pScalarBarLayer->setNumberOfLabels(nLabelCount); + } + void applyScalarBarState(const ResultDisplayCandidate& oDisplay) { if(m_pScalarBarLayer == nullptr) { @@ -684,9 +701,8 @@ public: } m_pScalarBarLayer->setLookupTable(oDisplay.m_pLookupTable); m_pScalarBarLayer->setTitle(oDisplay.m_sScalarBarTitle); - m_pScalarBarLayer->setLabelFormat(QString::fromLatin1("%.6g")); - m_pScalarBarLayer->setNumberOfLabels( - oDisplay.m_bConstantValueLabel ? 0 : 5); + m_pScalarBarLayer->setLabelFormat(QString::fromLatin1("%.4g")); + m_bCurrentConstantValueLabel = oDisplay.m_bConstantValueLabel; m_pScalarBarLayer->setConstantValueLabel( oDisplay.m_bConstantValueLabel, oDisplay.m_bConstantValueLabel @@ -698,6 +714,7 @@ public: m_pScalarBarLayer->setAboveRangeSwatch( oDisplay.m_oSettings.m_bUseAboveRangeColor, controllerText("Above max")); + applyScalarBarLayout(); } void applyLayerVisibility(bool bHasData) @@ -743,10 +760,11 @@ public: if(m_pScalarBarLayer != nullptr) { m_pScalarBarLayer->setLookupTable(nullptr); m_pScalarBarLayer->setTitle(QString()); - m_pScalarBarLayer->setNumberOfLabels(5); m_pScalarBarLayer->setBelowRangeSwatch(false, QString()); m_pScalarBarLayer->setAboveRangeSwatch(false, QString()); m_pScalarBarLayer->setConstantValueLabel(false, QString()); + m_bCurrentConstantValueLabel = false; + applyScalarBarLayout(); } applyLayerVisibility(false); return true; @@ -787,11 +805,12 @@ public: m_pScalarBarLayer->setVisible(false); m_pScalarBarLayer->setLookupTable(nullptr); m_pScalarBarLayer->setTitle(QString()); - m_pScalarBarLayer->setNumberOfLabels(5); m_pScalarBarLayer->setBelowRangeSwatch(false, QString()); m_pScalarBarLayer->setAboveRangeSwatch(false, QString()); m_pScalarBarLayer->setConstantValueLabel(false, QString()); } + m_bCurrentConstantValueLabel = false; + applyScalarBarLayout(); if(m_pScaleGridLayer != nullptr) { m_pScaleGridLayer->setVisible(false); } @@ -873,11 +892,14 @@ public: int m_nCurrentFrameIndex; /** @brief 不同快照对象完整提交成功后的单调序号,clear 和失败均不回退。 */ quint64 m_nSnapshotBindingRevision; + int m_nViewportWidth; ///< 最近一次有效 QVTK 视口像素宽度。 + int m_nViewportHeight; ///< 最近一次有效 QVTK 视口像素高度。 double m_dBusinessRange[2]; double m_dDisplayRange[2]; double m_dBounds[6]; bool m_bHasPropertyRange; bool m_bHasBounds; + bool m_bCurrentConstantValueLabel; ///< 当前色标是否只显示唯一业务值。 bool m_bScalarBarVisible; bool m_bScaleGridVisible; bool m_bWellNamesVisible; @@ -1132,6 +1154,27 @@ void nmPebiResultSceneController::clear() } } +void nmPebiResultSceneController::setViewportSize( + int nWidth, + int nHeight) +{ + Q_ASSERT(isResultSceneGuiThread()); + if(!isResultSceneGuiThread() || m_pPrivate == nullptr || + nWidth <= 0 || nHeight <= 0) { + return; + } + + m_pPrivate->discardActiveDataIfLayersLost(); + if(m_pPrivate->m_nViewportWidth == nWidth && + m_pPrivate->m_nViewportHeight == nHeight) { + return; + } + m_pPrivate->m_nViewportWidth = nWidth; + m_pPrivate->m_nViewportHeight = nHeight; + // QVTKWidget 的 Resize 会负责绘制,这里只同步二维布局和刻度数量。 + m_pPrivate->applyScalarBarLayout(); +} + bool nmPebiResultSceneController::applyPropertyFrame( const QString& sPropertyId, int nFrameIndex, diff --git a/Src/nmNum/nmSubWxs/nmWxPebiResultViewWidget.cpp b/Src/nmNum/nmSubWxs/nmWxPebiResultViewWidget.cpp index 2d783461..22f1b4f9 100644 --- a/Src/nmNum/nmSubWxs/nmWxPebiResultViewWidget.cpp +++ b/Src/nmNum/nmSubWxs/nmWxPebiResultViewWidget.cpp @@ -197,6 +197,16 @@ void nmWxPebiResultViewWidget::onControlOperationSucceeded() refreshStatusBar(); } +void nmWxPebiResultViewWidget::onViewportSizeChanged( + int nWidth, + int nHeight) +{ + if(m_pSceneController != nullptr) { + // SceneController 缓存尺寸,即使当前尚未创建结果色标也不会丢失。 + m_pSceneController->setViewportSize(nWidth, nHeight); + } +} + void nmWxPebiResultViewWidget::onScalarBarToggled(bool bVisible) { if(m_pSceneController != nullptr && m_pSceneController->hasSnapshot()) { @@ -245,6 +255,12 @@ void nmWxPebiResultViewWidget::createUi() nmVTKViewCommandController::ViewCommand_Print | nmVTKViewCommandController::ViewCommand_PrintPreview); m_pSceneController = new nmPebiResultSceneController(m_pRenderContainer); + connect(m_pRenderContainer, SIGNAL(viewportSizeChanged(int,int)), + this, SLOT(onViewportSizeChanged(int,int))); + const QSize oInitialViewportSize = + m_pRenderContainer->getViewportSize(); + onViewportSizeChanged( + oInitialViewportSize.width(), oInitialViewportSize.height()); m_pToolBar = new QToolBar(pRenderArea); m_pToolBar->setOrientation(Qt::Vertical);