You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
nmWTAI-Platform/Src/nmNum/nmRender/nmVTKScalarBarLayer.cpp

540 lines
20 KiB
C++

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#include "nmVTKScalarBarLayer.h"
#include <QByteArray>
#include <QCoreApplication>
#include <QThread>
#include <QtGlobal>
#include <vtkActor2D.h>
#include <vtkCellArray.h>
#include <vtkCoordinate.h>
#include <vtkLookupTable.h>
#include <vtkMath.h>
#include <vtkPoints.h>
#include <vtkPolyData.h>
#include <vtkPolyDataMapper2D.h>
#include <vtkPropAssembly.h>
#include <vtkProperty2D.h>
#include <vtkScalarBarActor.h>
#include <vtkTextActor.h>
#include <vtkTextProperty.h>
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()
{
QCoreApplication* pApplication = QCoreApplication::instance();
return pApplication == nullptr ||
pApplication->thread() == QThread::currentThread();
}
// 独立色块使用归一化视口坐标,避免依赖仅支持 IndexedLookup 的 ScalarBar Annotation。
vtkSmartPointer<vtkActor2D> createRangeSwatchActor(vtkPoints* pPoints)
{
vtkSmartPointer<vtkCellArray> pPolygons =
vtkSmartPointer<vtkCellArray>::New();
pPolygons->InsertNextCell(4);
pPolygons->InsertCellPoint(0);
pPolygons->InsertCellPoint(1);
pPolygons->InsertCellPoint(2);
pPolygons->InsertCellPoint(3);
vtkSmartPointer<vtkPolyData> pData =
vtkSmartPointer<vtkPolyData>::New();
pData->SetPoints(pPoints);
pData->SetPolys(pPolygons);
vtkSmartPointer<vtkCoordinate> pViewportCoordinate =
vtkSmartPointer<vtkCoordinate>::New();
pViewportCoordinate->SetCoordinateSystemToNormalizedViewport();
vtkSmartPointer<vtkPolyDataMapper2D> pMapper =
vtkSmartPointer<vtkPolyDataMapper2D>::New();
pMapper->SetInputData(pData);
pMapper->SetTransformCoordinate(pViewportCoordinate);
pMapper->ScalarVisibilityOff();
vtkSmartPointer<vtkActor2D> pActor =
vtkSmartPointer<vtkActor2D>::New();
pActor->SetMapper(pMapper);
pActor->GetProperty()->SetDisplayLocationToForeground();
pActor->PickableOff();
pActor->SetVisibility(0);
return 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()->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->SetVisibility(0);
}
// 更新矩形四角并显式标记 Points 已修改,Mapper 会在下一次 Render 使用新位置。
void updateRectangle(vtkPoints* pPoints,
double dX, double dY,
double dWidth, double dHeight)
{
pPoints->SetPoint(0, dX, dY, 0.0);
pPoints->SetPoint(1, dX + dWidth, dY, 0.0);
pPoints->SetPoint(2, dX + dWidth, dY + dHeight, 0.0);
pPoints->SetPoint(3, dX, dY + dHeight, 0.0);
pPoints->Modified();
}
}
nmVTKScalarBarLayer::nmVTKScalarBarLayer(const QString& sLayerId)
: nmVTKRenderLayer(sLayerId),
m_pLookupTable(nullptr),
m_pRootAssembly(vtkSmartPointer<vtkPropAssembly>::New()),
m_pActor(vtkSmartPointer<vtkScalarBarActor>::New()),
m_pTitleActor(vtkSmartPointer<vtkTextActor>::New()),
m_pConstantValueActor(vtkSmartPointer<vtkTextActor>::New()),
m_pBelowRangeSwatchPoints(vtkSmartPointer<vtkPoints>::New()),
m_pBelowRangeSwatchActor(nullptr),
m_pBelowRangeTextActor(vtkSmartPointer<vtkTextActor>::New()),
m_pAboveRangeSwatchPoints(vtkSmartPointer<vtkPoints>::New()),
m_pAboveRangeSwatchActor(nullptr),
m_pAboveRangeTextActor(vtkSmartPointer<vtkTextActor>::New()),
m_sLabelFormat(QString::fromLatin1("%.6g")),
m_dPositionX(0.0),
m_dPositionY(0.0),
m_dWidth(0.1),
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()->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<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->DrawBelowRangeSwatchOff();
m_pActor->DrawAboveRangeSwatchOff();
// 这些二维装饰必须独立显示,不能为网格共享 LUT 开启 IndexedLookup。
m_pBelowRangeSwatchPoints->SetNumberOfPoints(4);
m_pAboveRangeSwatchPoints->SetNumberOfPoints(4);
m_pBelowRangeSwatchActor =
createRangeSwatchActor(m_pBelowRangeSwatchPoints);
m_pAboveRangeSwatchActor =
createRangeSwatchActor(m_pAboveRangeSwatchPoints);
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);
m_pRootAssembly->AddPart(m_pAboveRangeSwatchActor);
m_pRootAssembly->AddPart(m_pAboveRangeTextActor);
setRenderProp(m_pRootAssembly);
}
nmVTKScalarBarLayer::~nmVTKScalarBarLayer()
{
Q_ASSERT(isScalarBarGuiThread());
// 先移除二维 Prop,再释放对外部 LookupTable 的共享引用。
detach();
m_pLookupTable = nullptr;
m_pAboveRangeTextActor = nullptr;
m_pAboveRangeSwatchActor = nullptr;
m_pAboveRangeSwatchPoints = nullptr;
m_pBelowRangeTextActor = nullptr;
m_pBelowRangeSwatchActor = nullptr;
m_pBelowRangeSwatchPoints = nullptr;
m_pConstantValueActor = nullptr;
m_pTitleActor = nullptr;
m_pActor = nullptr;
m_pRootAssembly = nullptr;
}
void nmVTKScalarBarLayer::setLookupTable(vtkLookupTable* pLookupTable)
{
Q_ASSERT(isScalarBarGuiThread());
if(!isScalarBarGuiThread()) {
return;
}
// 智能指针共享 LookupTable,保证外部释放原指针后色标仍可安全显示。
m_pLookupTable = pLookupTable;
m_pActor->SetLookupTable(pLookupTable);
updateRangeSwatchColors();
}
vtkLookupTable* nmVTKScalarBarLayer::getLookupTable() const
{
return m_pLookupTable;
}
void nmVTKScalarBarLayer::setTitle(const QString& sTitle)
{
Q_ASSERT(isScalarBarGuiThread());
if(!isScalarBarGuiThread()) {
return;
}
m_sTitle = sTitle;
// 内置标题永久留空,避免其按整个 ScalarBarActor 宽度自动居中。
m_pActor->SetTitle("");
const QByteArray aTitle = sTitle.toUtf8();
m_pTitleActor->SetInput(aTitle.constData());
m_pTitleActor->SetVisibility(sTitle.isEmpty() ? 0 : 1);
}
QString nmVTKScalarBarLayer::getTitle() const
{
return m_sTitle;
}
bool nmVTKScalarBarLayer::setLabelFormat(const QString& sFormat)
{
Q_ASSERT(isScalarBarGuiThread());
if(!isScalarBarGuiThread() || sFormat.isEmpty()) {
return false;
}
m_sLabelFormat = sFormat;
const QByteArray aFormat = sFormat.toLatin1();
m_pActor->SetLabelFormat(aFormat.constData());
return true;
}
QString nmVTKScalarBarLayer::getLabelFormat() const
{
return m_sLabelFormat;
}
bool nmVTKScalarBarLayer::setNumberOfLabels(int nLabelCount)
{
Q_ASSERT(isScalarBarGuiThread());
if(!isScalarBarGuiThread() || nLabelCount < 0 || nLabelCount > 64) {
return false;
}
m_pActor->SetNumberOfLabels(nLabelCount);
return true;
}
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<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(
bool bVisible,
const QString& sText)
{
Q_ASSERT(isScalarBarGuiThread());
if(!isScalarBarGuiThread()) {
return;
}
m_sBelowRangeText = bVisible ? sText : QString();
const QByteArray aText = m_sBelowRangeText.toUtf8();
m_pBelowRangeTextActor->SetInput(aText.constData());
m_pBelowRangeSwatchActor->SetVisibility(bVisible ? 1 : 0);
m_pBelowRangeTextActor->SetVisibility(
bVisible && !m_sBelowRangeText.isEmpty() ? 1 : 0);
}
void nmVTKScalarBarLayer::setAboveRangeSwatch(
bool bVisible,
const QString& sText)
{
Q_ASSERT(isScalarBarGuiThread());
if(!isScalarBarGuiThread()) {
return;
}
m_sAboveRangeText = bVisible ? sText : QString();
const QByteArray aText = m_sAboveRangeText.toUtf8();
m_pAboveRangeTextActor->SetInput(aText.constData());
m_pAboveRangeSwatchActor->SetVisibility(bVisible ? 1 : 0);
m_pAboveRangeTextActor->SetVisibility(
bVisible && !m_sAboveRangeText.isEmpty() ? 1 : 0);
}
void nmVTKScalarBarLayer::setConstantValueLabel(
bool bVisible,
const QString& sText)
{
Q_ASSERT(isScalarBarGuiThread());
if(!isScalarBarGuiThread()) {
return;
}
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);
}
bool nmVTKScalarBarLayer::setPosition(double dX, double dY)
{
Q_ASSERT(isScalarBarGuiThread());
if(!isScalarBarGuiThread() || !vtkMath::IsFinite(dX) ||
!vtkMath::IsFinite(dY)) {
return false;
}
// vtkScalarBarActor 默认使用归一化视口坐标,位置值由上层布局策略决定。
m_dPositionX = dX;
m_dPositionY = dY;
m_pActor->SetPosition(dX, dY);
updateOverlayLayout();
return true;
}
bool nmVTKScalarBarLayer::setSize(double dWidth, double dHeight)
{
Q_ASSERT(isScalarBarGuiThread());
if(!isScalarBarGuiThread() || !vtkMath::IsFinite(dWidth) ||
!vtkMath::IsFinite(dHeight) || dWidth <= 0.0 || dHeight <= 0.0) {
return false;
}
// 这里只拒绝非正尺寸,不替业务层限制色标是否超出视口。
m_dWidth = dWidth;
m_dHeight = dHeight;
m_pActor->SetWidth(dWidth);
m_pActor->SetHeight(dHeight);
updateOverlayLayout();
return true;
}
void nmVTKScalarBarLayer::updateOverlayLayout()
{
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);
// 标题通常位于色带左上方;极小视口下向下钳制,避免越过视口顶边。
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 =
m_dPositionY + m_dHeight + dSwatchGap;
updateRectangle(m_pBelowRangeSwatchPoints,
m_dPositionX, dBelowY,
dSwatchWidth, dSwatchHeight);
updateRectangle(m_pAboveRangeSwatchPoints,
m_dPositionX, dAboveY,
dSwatchWidth, dSwatchHeight);
m_pBelowRangeTextActor->GetPositionCoordinate()->SetValue(
m_dPositionX + dSwatchWidth + dTextGap,
dBelowY + dSwatchHeight * 0.5, 0.0);
m_pAboveRangeTextActor->GetPositionCoordinate()->SetValue(
m_dPositionX + dSwatchWidth + dTextGap,
dAboveY + dSwatchHeight * 0.5, 0.0);
}
void nmVTKScalarBarLayer::updateRangeSwatchColors()
{
if(m_pLookupTable == nullptr) {
return;
}
// 读取 LookupTable 的业务颜色属性,不直接访问内部特殊表项索引。
const double* pBelowColor = m_pLookupTable->GetBelowRangeColor();
const double* pAboveColor = m_pLookupTable->GetAboveRangeColor();
m_pBelowRangeSwatchActor->GetProperty()->SetColor(
pBelowColor[0], pBelowColor[1], pBelowColor[2]);
m_pBelowRangeSwatchActor->GetProperty()->SetOpacity(pBelowColor[3]);
m_pAboveRangeSwatchActor->GetProperty()->SetColor(
pAboveColor[0], pAboveColor[1], pAboveColor[2]);
m_pAboveRangeSwatchActor->GetProperty()->SetOpacity(pAboveColor[3]);
}
vtkScalarBarActor* nmVTKScalarBarLayer::getActor() const
{
return m_pActor;
}