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

451 lines
16 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 <vtkCoordinate.h>
#include <vtkLookupTable.h>
#include <vtkMath.h>
#include <vtkObjectFactory.h>
#include <vtkPropAssembly.h>
#include <vtkProperty2D.h>
#include <vtkRenderer.h>
#include <vtkScalarBarActor.h>
#include <vtkTextActor.h>
#include <vtkTextProperty.h>
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。
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_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();
}
// 单值端点标签沿用普通刻度的字号、粗斜体和阴影样式。
void configureConstantValueActor(vtkTextActor* pActor)
{
pActor->SetInput("");
pActor->SetTextScaleModeToNone();
pActor->GetTextProperty()->SetFontFamilyToArial();
pActor->GetTextProperty()->SetFontSize(SCALAR_BAR_LABEL_FONT_SIZE);
pActor->GetTextProperty()->SetColor(1.0, 1.0, 1.0);
pActor->GetTextProperty()->BoldOn();
pActor->GetTextProperty()->ItalicOn();
pActor->GetTextProperty()->ShadowOn();
pActor->GetTextProperty()->SetShadowOffset(1, -1);
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);
}
}
nmVTKScalarBarLayer::nmVTKScalarBarLayer(const QString& sLayerId)
: nmVTKRenderLayer(sLayerId),
m_pLookupTable(nullptr),
m_pRootAssembly(vtkSmartPointer<vtkPropAssembly>::New()),
m_pActor(vtkSmartPointer<vtkNmScalarBarActor>::New()),
m_pTitleActor(vtkSmartPointer<vtkTextActor>::New()),
m_pConstantMinimumValueActor(vtkSmartPointer<vtkTextActor>::New()),
m_pConstantMaximumValueActor(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();
configureTitleActor(m_pTitleActor);
configureConstantValueActor(m_pConstantMinimumValueActor);
configureConstantValueActor(m_pConstantMaximumValueActor);
updateOverlayLayout();
// 根 Assembly 只在构造时注册,运行期仅更新子 Actor 状态。
m_pRootAssembly->AddPart(m_pActor);
m_pRootAssembly->AddPart(m_pTitleActor);
m_pRootAssembly->AddPart(m_pConstantMinimumValueActor);
m_pRootAssembly->AddPart(m_pConstantMaximumValueActor);
setRenderProp(m_pRootAssembly);
}
nmVTKScalarBarLayer::~nmVTKScalarBarLayer()
{
Q_ASSERT(isScalarBarGuiThread());
// 先移除二维 Prop,再释放对外部 LookupTable 的共享引用。
detach();
m_pLookupTable = nullptr;
m_pConstantMaximumValueActor = nullptr;
m_pConstantMinimumValueActor = 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);
updateOverlayLayout();
}
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());
// 格式变化可能改变刻度宽度及 VTK 内部实际色带矩形。
updateOverlayLayout();
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);
// 标签数量会影响 VTK 内部色带矩形,独立 Overlay 必须同步其最新位置。
updateOverlayLayout();
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::setConstantValueLabel(
bool bVisible,
const QString& sText)
{
Q_ASSERT(isScalarBarGuiThread());
if(!isScalarBarGuiThread()) {
return;
}
m_sConstantValueText = bVisible ? sText : QString();
const QByteArray aText = m_sConstantValueText.toUtf8();
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)
{
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 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;
}
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 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 dDesiredTitleY =
dColorY + dColorHeight +
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(
dColorX, dTitleY, 0.0);
}
vtkScalarBarActor* nmVTKScalarBarLayer::getActor() const
{
return m_pActor;
}