feat: 优化结果色标的响应式布局与视觉样式

- 根据 QVTK 视口尺寸动态调整色标位置、高度和刻度数量
- 增加视口尺寸通知链路,布局更新不触发额外渲染
- 为结果属性提供稳定的 ASCII 色标短标题
- 使用独立标题 Actor,统一标题、刻度和常量值的字体及阴影
- 固定色标可见宽度并优化标签间距与上下越界标注布局
- 将色标数值格式调整为四位有效数字
- 使用 Saphir 取样控制点重构 Rainbow 色带
- 保持颜色预览与场景 LUT 使用同一套插值结果
feature/nmNum-postprocessing-view-20260910
lh 2 weeks ago
parent 9d1f758965
commit d59006ee9b

@ -43,6 +43,13 @@ public:
bool setNumberOfLabels(int nLabelCount); bool setNumberOfLabels(int nLabelCount);
/** @brief 返回当前普通刻度数量。 */ /** @brief 返回当前普通刻度数量。 */
int getNumberOfLabels() const; int getNumberOfLabels() const;
/**
* @brief 按 QVTK 视口像素尺寸更新固定宽度的响应式二维布局。
* @note 只修改 Actor 布局,不主动 Render,也不修改 LookupTable。
*/
bool setViewportSize(int nWidth, int nHeight);
/** @brief 返回当前视口和色带高度建议使用的普通刻度数量。 */
int getResponsiveLabelCount() const;
/** @brief 配置低于显示范围的独立二维色块和说明文字。 */ /** @brief 配置低于显示范围的独立二维色块和说明文字。 */
void setBelowRangeSwatch(bool bVisible, const QString& sText); void setBelowRangeSwatch(bool bVisible, const QString& sText);
@ -76,6 +83,7 @@ private:
vtkSmartPointer<vtkLookupTable> m_pLookupTable; ///< 共享外部颜色表引用。 vtkSmartPointer<vtkLookupTable> m_pLookupTable; ///< 共享外部颜色表引用。
vtkSmartPointer<vtkPropAssembly> m_pRootAssembly; ///< Scene 挂载的稳定根 Prop。 vtkSmartPointer<vtkPropAssembly> m_pRootAssembly; ///< Scene 挂载的稳定根 Prop。
vtkSmartPointer<vtkScalarBarActor> m_pActor; ///< 图层独占的二维色标 Actor。 vtkSmartPointer<vtkScalarBarActor> m_pActor; ///< 图层独占的二维色标 Actor。
vtkSmartPointer<vtkTextActor> m_pTitleActor; ///< 独立左对齐 ASCII 标题。
vtkSmartPointer<vtkTextActor> m_pConstantValueActor; ///< 常量属性唯一值文字。 vtkSmartPointer<vtkTextActor> m_pConstantValueActor; ///< 常量属性唯一值文字。
vtkSmartPointer<vtkPoints> m_pBelowRangeSwatchPoints; ///< 下越界色块四角坐标。 vtkSmartPointer<vtkPoints> m_pBelowRangeSwatchPoints; ///< 下越界色块四角坐标。
vtkSmartPointer<vtkActor2D> m_pBelowRangeSwatchActor; ///< 下越界独立色块。 vtkSmartPointer<vtkActor2D> m_pBelowRangeSwatchActor; ///< 下越界独立色块。
@ -92,6 +100,8 @@ private:
double m_dPositionY; ///< 色标左下角的归一化视口 Y 坐标。 double m_dPositionY; ///< 色标左下角的归一化视口 Y 坐标。
double m_dWidth; ///< 色标在归一化视口中的宽度。 double m_dWidth; ///< 色标在归一化视口中的宽度。
double m_dHeight; ///< 色标在归一化视口中的高度。 double m_dHeight; ///< 色标在归一化视口中的高度。
int m_nViewportWidth; ///< 最近一次有效 QVTK 视口像素宽度。
int m_nViewportHeight; ///< 最近一次有效 QVTK 视口像素高度。
}; };
#endif // NMVTKSCALARBARLAYER_H #endif // NMVTKSCALARBARLAYER_H

@ -4,6 +4,7 @@
#include "nmRender_global.h" #include "nmRender_global.h"
#include "nmVTKViewCommandController.h" #include "nmVTKViewCommandController.h"
#include <QSize>
#include <QWidget> #include <QWidget>
#include <vtkSmartPointer.h> #include <vtkSmartPointer.h>
@ -47,6 +48,8 @@ public:
/** @brief 返回 Qt 承载控件;调用方不得删除或重新设置父对象。 */ /** @brief 返回 Qt 承载控件;调用方不得删除或重新设置父对象。 */
QVTKWidget* getVtkWidget() const; QVTKWidget* getVtkWidget() const;
/** @brief 返回当前 QVTK 视口的 Qt 像素尺寸;控件不可用时返回空尺寸。 */
QSize getViewportSize() const;
/** @brief 返回当前容器独占的 Renderer 非所有权指针。 */ /** @brief 返回当前容器独占的 Renderer 非所有权指针。 */
vtkRenderer* getRenderer() const; vtkRenderer* getRenderer() const;
/** @brief 返回 QVTKWidget 持有的 RenderWindow 非所有权指针。 */ /** @brief 返回 QVTKWidget 持有的 RenderWindow 非所有权指针。 */
@ -114,6 +117,11 @@ signals:
void contextMenuAboutToShow(QMenu* pMenu); void contextMenuAboutToShow(QMenu* pMenu);
/** @brief 将容器确认后的拾取状态同步给公共 QAction。 */ /** @brief 将容器确认后的拾取状态同步给公共 QAction。 */
void rotationCenterPickingChanged(bool bPicking); void rotationCenterPickingChanged(bool bPicking);
/**
* @brief QVTK 视口尺寸变化时同步发出实际 Qt 像素尺寸。
* @note 信号在 QVTKWidget 处理 Resize 之前发出,接收方不得主动 Render。
*/
void viewportSizeChanged(int nWidth, int nHeight);
public slots: public slots:
/** @brief 按固定倍率 1.2 放大,成功后统一渲染一次。 */ /** @brief 按固定倍率 1.2 放大,成功后统一渲染一次。 */

@ -8,8 +8,8 @@
/** /**
* @brief 将结果属性目录中的稳定源文字转换为当前语言下的界面文字。 * @brief 将结果属性目录中的稳定源文字转换为当前语言下的界面文字。
* *
* 属性 ID、英文源文字和单位仍由 nmPebiResultPropertyCatalog 唯一维护; * 属性 ID、英文源文字和单位仍由 nmPebiResultPropertyCatalog 维护;
* 本类只负责固定翻译上下文,避免各业务控件重复硬编码属性名称。 * 本类统一提供 Qt 翻译文字及 VTK 可稳定绘制的固定 ASCII 短标题。
*/ */
class NM_SUB_WXS_EXPORT nmPebiResultPropertyDisplayText class NM_SUB_WXS_EXPORT nmPebiResultPropertyDisplayText
{ {
@ -20,6 +20,11 @@ public:
* @return 翻译后的名称;未知属性返回空字符串。 * @return 翻译后的名称;未知属性返回空字符串。
*/ */
static QString getDisplayName(const QString& sPropertyId); static QString getDisplayName(const QString& sPropertyId);
/**
* @brief 返回 VTK 色标使用的固定 ASCII 短标题。
* @note 标题不参与翻译;未知属性返回空字符串。
*/
static QString getScalarBarTitle(const QString& sPropertyId);
/** /**
* @brief 返回 Catalog 中的默认显示单位。 * @brief 返回 Catalog 中的默认显示单位。
* @return 单位原文;未知属性或无量纲属性返回空字符串。 * @return 单位原文;未知属性或无量纲属性返回空字符串。

@ -43,6 +43,12 @@ public:
/** @brief 解除管线和快照引用并隐藏结果图层,不销毁 Scene。 */ /** @brief 解除管线和快照引用并隐藏结果图层,不销毁 Scene。 */
void clear(); void clear();
/**
* @brief 缓存 QVTK 视口尺寸并更新色标二维布局,不主动 Render。
* @note 尺寸在无快照时仍会保存,后续创建色标图层时立即应用。
*/
void setViewportSize(int nWidth, int nHeight);
/** /**
* @brief 原子切换同一快照内的属性和帧,成功时仅渲染一次。 * @brief 原子切换同一快照内的属性和帧,成功时仅渲染一次。
* @note 静态属性只接受帧 0;失败时恢复 Adapter、Mapper 和范围。 * @note 静态属性只接受帧 0;失败时恢复 Adapter、Mapper 和范围。

@ -62,6 +62,8 @@ private slots:
void onControlOperationFailed(const QString& sError); void onControlOperationFailed(const QString& sError);
/** @brief 明确操作成功后清除旧普通错误,不影响绑定失败或过期状态。 */ /** @brief 明确操作成功后清除旧普通错误,不影响绑定失败或过期状态。 */
void onControlOperationSucceeded(); void onControlOperationSucceeded();
/** @brief 将 QVTK 视口尺寸同步给结果场景,不主动触发额外渲染。 */
void onViewportSizeChanged(int nWidth, int nHeight);
/** @brief 切换色标显隐。 */ /** @brief 切换色标显隐。 */
void onScalarBarToggled(bool bVisible); void onScalarBarToggled(bool bVisible);

@ -14,6 +14,33 @@ const char* const COLOR_MAP_GAS_SATURATION = "gas-saturation";
const char* const COLOR_MAP_WATER_SATURATION = "water-saturation"; const char* const COLOR_MAP_WATER_SATURATION = "water-saturation";
const int COLOR_TABLE_SIZE = 256; 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<int>(sizeof(RAINBOW_CONTROL_POINTS) /
sizeof(RAINBOW_CONTROL_POINTS[0]));
QString stableId(const char* pId) QString stableId(const char* pId)
{ {
return QString::fromLatin1(pId); return QString::fromLatin1(pId);
@ -33,6 +60,40 @@ QColor interpolateRgb(const QColor& oStart,
255); 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() QStringList nmVTKColorMapCatalog::getColorMapIds()
@ -61,10 +122,7 @@ bool nmVTKColorMapCatalog::getColor(const QString& sColorMapId,
QColor oCandidate; QColor oCandidate;
if(sColorMapId == stableId(COLOR_MAP_RAINBOW)) { if(sColorMapId == stableId(COLOR_MAP_RAINBOW)) {
// Saphir 风格 Rainbow:色相从蓝色 240 度连续过渡到红色 0 度。 oCandidate = getRainbowColor(dPosition);
oCandidate = QColor::fromHsvF(
(1.0 - dPosition) * (240.0 / 360.0),
1.0, 1.0, 1.0);
} else if(sColorMapId == stableId(COLOR_MAP_OIL_SATURATION)) { } else if(sColorMapId == stableId(COLOR_MAP_OIL_SATURATION)) {
oCandidate = interpolateRgb( oCandidate = interpolateRgb(
QColor(255, 255, 255), QColor(0, 255, 0), dPosition); QColor(255, 255, 255), QColor(0, 255, 0), dPosition);

@ -3,6 +3,7 @@
#include <QByteArray> #include <QByteArray>
#include <QCoreApplication> #include <QCoreApplication>
#include <QThread> #include <QThread>
#include <QtGlobal>
#include <vtkActor2D.h> #include <vtkActor2D.h>
#include <vtkCellArray.h> #include <vtkCellArray.h>
@ -20,6 +21,30 @@
namespace { 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 线程中修改。 // ScalarBarActor 和 LookupTable 均只能在 Qt GUI 线程中修改。
bool isScalarBarGuiThread() bool isScalarBarGuiThread()
{ {
@ -62,18 +87,49 @@ vtkSmartPointer<vtkActor2D> createRangeSwatchActor(vtkPoints* pPoints)
return pActor; return pActor;
} }
// 统一独立文本样式,使常量值和越界说明与普通色标刻度保持一致。 // 独立文本共用字号和颜色;常量值可选择与普通刻度一致的强调样式。
void configureOverlayTextActor(vtkTextActor* pActor) void configureOverlayTextActor(vtkTextActor* pActor,
bool bUseTickStyle)
{ {
pActor->SetInput(""); pActor->SetInput("");
pActor->SetTextScaleModeToNone(); pActor->SetTextScaleModeToNone();
pActor->GetTextProperty()->SetFontSize(11); 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()->BoldOff();
pActor->GetTextProperty()->ItalicOff(); pActor->GetTextProperty()->ItalicOff();
pActor->GetTextProperty()->ShadowOff(); pActor->GetTextProperty()->ShadowOff();
}
pActor->GetTextProperty()->SetJustificationToLeft(); pActor->GetTextProperty()->SetJustificationToLeft();
pActor->GetTextProperty()->SetVerticalJustificationToCentered(); pActor->GetTextProperty()->SetVerticalJustificationToCentered();
pActor->GetPositionCoordinate()->SetCoordinateSystemToNormalizedViewport(); pActor->GetPositionCoordinate()->SetCoordinateSystemToNormalizedViewport();
pActor->GetProperty()->SetDisplayLocationToForeground();
pActor->PickableOff();
pActor->SetVisibility(0);
}
// 独立标题不受 vtkScalarBarActor 内部居中布局影响,始终保持左对齐。
void configureTitleActor(vtkTextActor* pActor)
{
pActor->SetInput("");
pActor->SetTextScaleModeToNone();
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()->ShadowOn();
pActor->GetTextProperty()->SetShadowOffset(1, -1);
pActor->GetTextProperty()->SetJustificationToLeft();
pActor->GetTextProperty()->SetVerticalJustificationToBottom();
pActor->GetPositionCoordinate()->SetCoordinateSystemToNormalizedViewport();
pActor->GetProperty()->SetDisplayLocationToForeground();
pActor->PickableOff(); pActor->PickableOff();
pActor->SetVisibility(0); pActor->SetVisibility(0);
} }
@ -97,6 +153,7 @@ nmVTKScalarBarLayer::nmVTKScalarBarLayer(const QString& sLayerId)
m_pLookupTable(nullptr), m_pLookupTable(nullptr),
m_pRootAssembly(vtkSmartPointer<vtkPropAssembly>::New()), m_pRootAssembly(vtkSmartPointer<vtkPropAssembly>::New()),
m_pActor(vtkSmartPointer<vtkScalarBarActor>::New()), m_pActor(vtkSmartPointer<vtkScalarBarActor>::New()),
m_pTitleActor(vtkSmartPointer<vtkTextActor>::New()),
m_pConstantValueActor(vtkSmartPointer<vtkTextActor>::New()), m_pConstantValueActor(vtkSmartPointer<vtkTextActor>::New()),
m_pBelowRangeSwatchPoints(vtkSmartPointer<vtkPoints>::New()), m_pBelowRangeSwatchPoints(vtkSmartPointer<vtkPoints>::New()),
m_pBelowRangeSwatchActor(nullptr), m_pBelowRangeSwatchActor(nullptr),
@ -108,21 +165,33 @@ nmVTKScalarBarLayer::nmVTKScalarBarLayer(const QString& sLayerId)
m_dPositionX(0.0), m_dPositionX(0.0),
m_dPositionY(0.0), m_dPositionY(0.0),
m_dWidth(0.1), m_dWidth(0.1),
m_dHeight(0.8) m_dHeight(0.8),
m_nViewportWidth(0),
m_nViewportHeight(0)
{ {
Q_ASSERT(isScalarBarGuiThread()); Q_ASSERT(isScalarBarGuiThread());
// vtkScalarBarActor 是二维 Prop,仍通过图层基类统一完成挂载和显隐。 // vtkScalarBarActor 是二维 Prop,仍通过图层基类统一完成挂载和显隐。
m_pActor->SetNumberOfLabels(5); m_pActor->SetNumberOfLabels(5);
m_pActor->SetLabelFormat("%.6g"); m_pActor->SetLabelFormat("%.6g");
m_pActor->SetTitle("");
m_pActor->SetOrientationToVertical();
m_pActor->SetTextPositionToSucceedScalarBar();
m_pActor->SetTextPad(3);
m_pActor->UnconstrainedFontSizeOn(); m_pActor->UnconstrainedFontSizeOn();
m_pActor->GetLabelTextProperty()->SetFontSize(11); m_pActor->GetLabelTextProperty()->SetFontFamilyToArial();
m_pActor->GetLabelTextProperty()->BoldOff(); m_pActor->GetLabelTextProperty()->SetFontSize(
m_pActor->GetLabelTextProperty()->ItalicOff(); SCALAR_BAR_LABEL_FONT_SIZE);
m_pActor->GetLabelTextProperty()->ShadowOff(); m_pActor->GetLabelTextProperty()->SetColor(1.0, 1.0, 1.0);
m_pActor->GetTitleTextProperty()->SetFontSize(12); m_pActor->GetLabelTextProperty()->BoldOn();
m_pActor->GetTitleTextProperty()->BoldOff(); m_pActor->GetLabelTextProperty()->ItalicOn();
m_pActor->GetTitleTextProperty()->ItalicOff(); m_pActor->GetLabelTextProperty()->ShadowOn();
m_pActor->GetTitleTextProperty()->ShadowOff(); m_pActor->GetLabelTextProperty()->SetShadowOffset(1, -1);
m_pActor->SetMaximumWidthInPixels(SCALAR_BAR_TOTAL_WIDTH_PIXELS);
m_pActor->SetBarRatio(
static_cast<double>(SCALAR_BAR_COLOR_WIDTH_PIXELS) /
static_cast<double>(SCALAR_BAR_TOTAL_WIDTH_PIXELS));
m_pActor->GetProperty()->SetDisplayLocationToForeground();
m_pActor->PickableOff();
m_pActor->DrawAnnotationsOff(); m_pActor->DrawAnnotationsOff();
m_pActor->DrawBelowRangeSwatchOff(); m_pActor->DrawBelowRangeSwatchOff();
m_pActor->DrawAboveRangeSwatchOff(); m_pActor->DrawAboveRangeSwatchOff();
@ -134,13 +203,15 @@ nmVTKScalarBarLayer::nmVTKScalarBarLayer(const QString& sLayerId)
createRangeSwatchActor(m_pBelowRangeSwatchPoints); createRangeSwatchActor(m_pBelowRangeSwatchPoints);
m_pAboveRangeSwatchActor = m_pAboveRangeSwatchActor =
createRangeSwatchActor(m_pAboveRangeSwatchPoints); createRangeSwatchActor(m_pAboveRangeSwatchPoints);
configureOverlayTextActor(m_pConstantValueActor); configureTitleActor(m_pTitleActor);
configureOverlayTextActor(m_pBelowRangeTextActor); configureOverlayTextActor(m_pConstantValueActor, true);
configureOverlayTextActor(m_pAboveRangeTextActor); configureOverlayTextActor(m_pBelowRangeTextActor, false);
configureOverlayTextActor(m_pAboveRangeTextActor, false);
updateOverlayLayout(); updateOverlayLayout();
// 根 Assembly 只在构造时注册,运行期仅更新子 Actor 状态。 // 根 Assembly 只在构造时注册,运行期仅更新子 Actor 状态。
m_pRootAssembly->AddPart(m_pActor); m_pRootAssembly->AddPart(m_pActor);
m_pRootAssembly->AddPart(m_pTitleActor);
m_pRootAssembly->AddPart(m_pConstantValueActor); m_pRootAssembly->AddPart(m_pConstantValueActor);
m_pRootAssembly->AddPart(m_pBelowRangeSwatchActor); m_pRootAssembly->AddPart(m_pBelowRangeSwatchActor);
m_pRootAssembly->AddPart(m_pBelowRangeTextActor); m_pRootAssembly->AddPart(m_pBelowRangeTextActor);
@ -162,6 +233,7 @@ nmVTKScalarBarLayer::~nmVTKScalarBarLayer()
m_pBelowRangeSwatchActor = nullptr; m_pBelowRangeSwatchActor = nullptr;
m_pBelowRangeSwatchPoints = nullptr; m_pBelowRangeSwatchPoints = nullptr;
m_pConstantValueActor = nullptr; m_pConstantValueActor = nullptr;
m_pTitleActor = nullptr;
m_pActor = nullptr; m_pActor = nullptr;
m_pRootAssembly = nullptr; m_pRootAssembly = nullptr;
} }
@ -190,9 +262,11 @@ void nmVTKScalarBarLayer::setTitle(const QString& sTitle)
return; return;
} }
m_sTitle = sTitle; m_sTitle = sTitle;
// VTK 7.1 接口接收窄字符串,统一以 UTF-8 临时缓冲传入并由 Actor 复制。 // 内置标题永久留空,避免其按整个 ScalarBarActor 宽度自动居中。
m_pActor->SetTitle("");
const QByteArray aTitle = sTitle.toUtf8(); 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 QString nmVTKScalarBarLayer::getTitle() const
@ -232,6 +306,73 @@ int nmVTKScalarBarLayer::getNumberOfLabels() const
return m_pActor != nullptr ? m_pActor->GetNumberOfLabels() : 0; 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<double>(nLeftMargin) / nWidth;
m_dPositionY = dBottomRatio;
m_dWidth = static_cast<double>(nLayoutWidth) / nWidth;
m_dHeight = static_cast<double>(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<double>(SCALAR_BAR_COLOR_WIDTH_PIXELS) /
static_cast<double>(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<double>(nBarHeight) /
SCALAR_BAR_LABEL_SPACING_PIXELS),
SCALAR_BAR_MAX_LABEL_COUNT);
}
void nmVTKScalarBarLayer::setBelowRangeSwatch( void nmVTKScalarBarLayer::setBelowRangeSwatch(
bool bVisible, bool bVisible,
const QString& sText) const QString& sText)
@ -313,22 +454,50 @@ bool nmVTKScalarBarLayer::setSize(double dWidth, double dHeight)
void nmVTKScalarBarLayer::updateOverlayLayout() void nmVTKScalarBarLayer::updateOverlayLayout()
{ {
// 常量值放在色带右侧的垂直中心,视觉位置与普通刻度标签一致。 const bool bHasViewport =
const double dLabelX = m_dPositionX + m_dWidth + 0.01; 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; const double dLabelY = m_dPositionY + m_dHeight * 0.5;
m_pConstantValueActor->GetPositionCoordinate()->SetValue( m_pConstantValueActor->GetPositionCoordinate()->SetValue(
dLabelX, dLabelY, 0.0); dLabelX, dLabelY, 0.0);
// 越界色块位于色带上下两端外侧,避免覆盖连续色带和普通刻度。 // 标题通常位于色带左上方;极小视口下向下钳制,避免越过视口顶边。
double dSwatchWidth = m_dWidth * 0.25; const double dDesiredTitleY =
if(dSwatchWidth < 0.012) { m_dPositionY + m_dHeight +
dSwatchWidth = 0.012; SCALAR_BAR_TITLE_OFFSET_Y_PIXELS * dPixelY;
} else if(dSwatchWidth > 0.025) { const double dMaximumTitleY = bHasViewport
dSwatchWidth = 0.025; ? qMax(0.0, 1.0 -
} SCALAR_BAR_TITLE_SAFE_HEIGHT_PIXELS * dPixelY)
const double dSwatchHeight = 0.018; : dDesiredTitleY;
const double dSwatchGap = 0.012; const double dTitleY = qMin(dDesiredTitleY, dMaximumTitleY);
const double dTextGap = 0.008; 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 = const double dBelowY =
m_dPositionY - dSwatchGap - dSwatchHeight; m_dPositionY - dSwatchGap - dSwatchHeight;
const double dAboveY = const double dAboveY =

@ -16,6 +16,7 @@
#include <QMenu> #include <QMenu>
#include <QMessageBox> #include <QMessageBox>
#include <QMouseEvent> #include <QMouseEvent>
#include <QResizeEvent>
#include <QStackedWidget> #include <QStackedWidget>
#include <QThread> #include <QThread>
#include <QTimer> #include <QTimer>
@ -240,6 +241,11 @@ QVTKWidget* nmWxVTKRenderContainerWidget::getVtkWidget() const
return m_pVtkWidget; return m_pVtkWidget;
} }
QSize nmWxVTKRenderContainerWidget::getViewportSize() const
{
return m_pVtkWidget != nullptr ? m_pVtkWidget->size() : QSize();
}
vtkRenderer* nmWxVTKRenderContainerWidget::getRenderer() const vtkRenderer* nmWxVTKRenderContainerWidget::getRenderer() const
{ {
return m_pRenderer; return m_pRenderer;
@ -715,6 +721,17 @@ bool nmWxVTKRenderContainerWidget::eventFilter(
return QWidget::eventFilter(pObject, pEvent); return QWidget::eventFilter(pObject, pEvent);
} }
if(pEvent->type() == QEvent::Resize) {
const QResizeEvent* pResizeEvent =
static_cast<const QResizeEvent*>(pEvent);
const QSize oViewportSize = pResizeEvent->size();
if(oViewportSize.width() > 0 && oViewportSize.height() > 0) {
// 只同步布局输入;QVTKWidget 随后处理 Resize 并完成本次绘制。
emit viewportSizeChanged(
oViewportSize.width(), oViewportSize.height());
}
}
// 一次性拾取优先于 VTK 相机交互和右键菜单,并继续吞掉对应 Release。 // 一次性拾取优先于 VTK 相机交互和右键菜单,并继续吞掉对应 Release。
if(m_pRotationCenterPicker != nullptr && if(m_pRotationCenterPicker != nullptr &&
(m_pRotationCenterPicker->isActive() || (m_pRotationCenterPicker->isActive() ||

@ -34,6 +34,40 @@ QString nmPebiResultPropertyDisplayText::getDisplayName(
"nmPebiResultProperty", baSourceText.constData()); "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( QString nmPebiResultPropertyDisplayText::getDefaultDisplayUnit(
const QString& sPropertyId) const QString& sPropertyId)
{ {

@ -298,14 +298,8 @@ bool prepareDisplayCandidate(
const bool bConstantValueLabel = const bool bConstantValueLabel =
!oSettings.m_bHasManualRange && !oSettings.m_bHasManualRange &&
dBusinessRange[0] == dBusinessRange[1]; dBusinessRange[0] == dBusinessRange[1];
QString sTitle = const QString sTitle =
nmPebiResultPropertyDisplayText::getDisplayName(sPropertyId); nmPebiResultPropertyDisplayText::getScalarBarTitle(sPropertyId);
const QString sUnit =
nmPebiResultPropertyDisplayText::getDefaultDisplayUnit(sPropertyId);
if(!sUnit.isEmpty()) {
sTitle += QString::fromLatin1(" (") + sUnit +
QString::fromLatin1(")");
}
oCandidate.m_oSettings = oSettings; oCandidate.m_oSettings = oSettings;
oCandidate.m_dBusinessRange[0] = dBusinessRange[0]; oCandidate.m_dBusinessRange[0] = dBusinessRange[0];
@ -399,8 +393,11 @@ public:
m_pAdapter(nullptr), m_pAdapter(nullptr),
m_nCurrentFrameIndex(-1), m_nCurrentFrameIndex(-1),
m_nSnapshotBindingRevision(0), m_nSnapshotBindingRevision(0),
m_nViewportWidth(0),
m_nViewportHeight(0),
m_bHasPropertyRange(false), m_bHasPropertyRange(false),
m_bHasBounds(false), m_bHasBounds(false),
m_bCurrentConstantValueLabel(false),
m_bScalarBarVisible(true), m_bScalarBarVisible(true),
m_bScaleGridVisible(false), m_bScaleGridVisible(false),
m_bWellNamesVisible(true) m_bWellNamesVisible(true)
@ -552,8 +549,14 @@ public:
nmVTKScalarBarLayer* pLayer = new nmVTKScalarBarLayer( nmVTKScalarBarLayer* pLayer = new nmVTKScalarBarLayer(
QString::fromLatin1(RESULT_SCALAR_BAR_LAYER_ID)); QString::fromLatin1(RESULT_SCALAR_BAR_LAYER_ID));
pLayer->setVisible(false); pLayer->setVisible(false);
pLayer->setPosition(0.05, 0.1); if(m_nViewportWidth > 0 && m_nViewportHeight > 0 &&
pLayer->setSize(0.1, 0.8); !pLayer->setViewportSize(
m_nViewportWidth, m_nViewportHeight)) {
delete pLayer;
rollbackCreatedLayers(
bCreatedMesh, false, false, false);
return false;
}
if(!pScene->addLayer(pLayer)) { if(!pScene->addLayer(pLayer)) {
delete pLayer; delete pLayer;
rollbackCreatedLayers( rollbackCreatedLayers(
@ -677,6 +680,20 @@ public:
pLookupTable); 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) void applyScalarBarState(const ResultDisplayCandidate& oDisplay)
{ {
if(m_pScalarBarLayer == nullptr) { if(m_pScalarBarLayer == nullptr) {
@ -684,9 +701,8 @@ public:
} }
m_pScalarBarLayer->setLookupTable(oDisplay.m_pLookupTable); m_pScalarBarLayer->setLookupTable(oDisplay.m_pLookupTable);
m_pScalarBarLayer->setTitle(oDisplay.m_sScalarBarTitle); m_pScalarBarLayer->setTitle(oDisplay.m_sScalarBarTitle);
m_pScalarBarLayer->setLabelFormat(QString::fromLatin1("%.6g")); m_pScalarBarLayer->setLabelFormat(QString::fromLatin1("%.4g"));
m_pScalarBarLayer->setNumberOfLabels( m_bCurrentConstantValueLabel = oDisplay.m_bConstantValueLabel;
oDisplay.m_bConstantValueLabel ? 0 : 5);
m_pScalarBarLayer->setConstantValueLabel( m_pScalarBarLayer->setConstantValueLabel(
oDisplay.m_bConstantValueLabel, oDisplay.m_bConstantValueLabel,
oDisplay.m_bConstantValueLabel oDisplay.m_bConstantValueLabel
@ -698,6 +714,7 @@ public:
m_pScalarBarLayer->setAboveRangeSwatch( m_pScalarBarLayer->setAboveRangeSwatch(
oDisplay.m_oSettings.m_bUseAboveRangeColor, oDisplay.m_oSettings.m_bUseAboveRangeColor,
controllerText("Above max")); controllerText("Above max"));
applyScalarBarLayout();
} }
void applyLayerVisibility(bool bHasData) void applyLayerVisibility(bool bHasData)
@ -743,10 +760,11 @@ public:
if(m_pScalarBarLayer != nullptr) { if(m_pScalarBarLayer != nullptr) {
m_pScalarBarLayer->setLookupTable(nullptr); m_pScalarBarLayer->setLookupTable(nullptr);
m_pScalarBarLayer->setTitle(QString()); m_pScalarBarLayer->setTitle(QString());
m_pScalarBarLayer->setNumberOfLabels(5);
m_pScalarBarLayer->setBelowRangeSwatch(false, QString()); m_pScalarBarLayer->setBelowRangeSwatch(false, QString());
m_pScalarBarLayer->setAboveRangeSwatch(false, QString()); m_pScalarBarLayer->setAboveRangeSwatch(false, QString());
m_pScalarBarLayer->setConstantValueLabel(false, QString()); m_pScalarBarLayer->setConstantValueLabel(false, QString());
m_bCurrentConstantValueLabel = false;
applyScalarBarLayout();
} }
applyLayerVisibility(false); applyLayerVisibility(false);
return true; return true;
@ -787,11 +805,12 @@ public:
m_pScalarBarLayer->setVisible(false); m_pScalarBarLayer->setVisible(false);
m_pScalarBarLayer->setLookupTable(nullptr); m_pScalarBarLayer->setLookupTable(nullptr);
m_pScalarBarLayer->setTitle(QString()); m_pScalarBarLayer->setTitle(QString());
m_pScalarBarLayer->setNumberOfLabels(5);
m_pScalarBarLayer->setBelowRangeSwatch(false, QString()); m_pScalarBarLayer->setBelowRangeSwatch(false, QString());
m_pScalarBarLayer->setAboveRangeSwatch(false, QString()); m_pScalarBarLayer->setAboveRangeSwatch(false, QString());
m_pScalarBarLayer->setConstantValueLabel(false, QString()); m_pScalarBarLayer->setConstantValueLabel(false, QString());
} }
m_bCurrentConstantValueLabel = false;
applyScalarBarLayout();
if(m_pScaleGridLayer != nullptr) { if(m_pScaleGridLayer != nullptr) {
m_pScaleGridLayer->setVisible(false); m_pScaleGridLayer->setVisible(false);
} }
@ -873,11 +892,14 @@ public:
int m_nCurrentFrameIndex; int m_nCurrentFrameIndex;
/** @brief 不同快照对象完整提交成功后的单调序号,clear 和失败均不回退。 */ /** @brief 不同快照对象完整提交成功后的单调序号,clear 和失败均不回退。 */
quint64 m_nSnapshotBindingRevision; quint64 m_nSnapshotBindingRevision;
int m_nViewportWidth; ///< 最近一次有效 QVTK 视口像素宽度。
int m_nViewportHeight; ///< 最近一次有效 QVTK 视口像素高度。
double m_dBusinessRange[2]; double m_dBusinessRange[2];
double m_dDisplayRange[2]; double m_dDisplayRange[2];
double m_dBounds[6]; double m_dBounds[6];
bool m_bHasPropertyRange; bool m_bHasPropertyRange;
bool m_bHasBounds; bool m_bHasBounds;
bool m_bCurrentConstantValueLabel; ///< 当前色标是否只显示唯一业务值。
bool m_bScalarBarVisible; bool m_bScalarBarVisible;
bool m_bScaleGridVisible; bool m_bScaleGridVisible;
bool m_bWellNamesVisible; 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( bool nmPebiResultSceneController::applyPropertyFrame(
const QString& sPropertyId, const QString& sPropertyId,
int nFrameIndex, int nFrameIndex,

@ -197,6 +197,16 @@ void nmWxPebiResultViewWidget::onControlOperationSucceeded()
refreshStatusBar(); refreshStatusBar();
} }
void nmWxPebiResultViewWidget::onViewportSizeChanged(
int nWidth,
int nHeight)
{
if(m_pSceneController != nullptr) {
// SceneController 缓存尺寸,即使当前尚未创建结果色标也不会丢失。
m_pSceneController->setViewportSize(nWidth, nHeight);
}
}
void nmWxPebiResultViewWidget::onScalarBarToggled(bool bVisible) void nmWxPebiResultViewWidget::onScalarBarToggled(bool bVisible)
{ {
if(m_pSceneController != nullptr && m_pSceneController->hasSnapshot()) { if(m_pSceneController != nullptr && m_pSceneController->hasSnapshot()) {
@ -245,6 +255,12 @@ void nmWxPebiResultViewWidget::createUi()
nmVTKViewCommandController::ViewCommand_Print | nmVTKViewCommandController::ViewCommand_Print |
nmVTKViewCommandController::ViewCommand_PrintPreview); nmVTKViewCommandController::ViewCommand_PrintPreview);
m_pSceneController = new nmPebiResultSceneController(m_pRenderContainer); 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 = new QToolBar(pRenderArea);
m_pToolBar->setOrientation(Qt::Vertical); m_pToolBar->setOrientation(Qt::Vertical);

Loading…
Cancel
Save