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

716 lines
27 KiB
C++

#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 <vtkObjectFactory.h>
#include <vtkPoints.h>
#include <vtkPolyData.h>
#include <vtkPolyDataMapper2D.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_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 int SCALAR_BAR_TITLE_CONTENT_GAP_PIXELS = 4;
const double SCALAR_BAR_NORMAL_BOTTOM_RATIO = 0.22;
const double SCALAR_BAR_NORMAL_HEIGHT_RATIO = 0.67;
const double SCALAR_BAR_SMALL_BOTTOM_RATIO = 0.22;
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);
}
// 优先读取 VTK 按当前 DPI 计算的真实文字高度,尚未挂载时使用稳定回退值。
double getOverlayTextHeightPixels(vtkTextActor* pActor,
vtkRenderer* pRenderer,
double dFallbackHeight)
{
if(pActor == nullptr || pRenderer == nullptr) {
return dFallbackHeight;
}
double dSize[2] = { 0.0, 0.0 };
pActor->GetSize(pRenderer, dSize);
return vtkMath::IsFinite(dSize[1]) && dSize[1] > 0.0
? dSize[1] : dFallbackHeight;
}
// 横向避让仅在极小视口空间不足时使用,正常布局仍保持色块位于色带上方。
double getOverlayTextWidthPixels(vtkTextActor* pActor,
vtkRenderer* pRenderer,
double dFallbackWidth)
{
if(pActor == nullptr || pRenderer == nullptr) {
return dFallbackWidth;
}
double dSize[2] = { 0.0, 0.0 };
pActor->GetSize(pRenderer, dSize);
return vtkMath::IsFinite(dSize[0]) && dSize[0] > 0.0
? dSize[0] : dFallbackWidth;
}
// 更新矩形四角并显式标记 Points 已修改,Mapper 会在下一次 Render 使用新位置。
void updateRectangle(vtkPoints* pPoints,
double dX, double dY,
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<vtkNmScalarBarActor>::New()),
m_pTitleActor(vtkSmartPointer<vtkTextActor>::New()),
m_pConstantMinimumValueActor(vtkSmartPointer<vtkTextActor>::New()),
m_pConstantMaximumValueActor(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_pConstantMinimumValueActor, true);
configureOverlayTextActor(m_pConstantMaximumValueActor, 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_pConstantMinimumValueActor);
m_pRootAssembly->AddPart(m_pConstantMaximumValueActor);
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_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);
updateRangeSwatchColors();
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);
// 标题宽度参与极小视口下 Above max 的横向避让。
updateOverlayLayout();
}
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::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);
updateOverlayLayout();
}
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);
updateOverlayLayout();
}
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 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 bool bConstantLabelsVisible =
m_pConstantMinimumValueActor->GetVisibility() != 0 &&
!m_sConstantValueText.isEmpty();
const double dConstantTextHeight = getOverlayTextHeightPixels(
m_pConstantMinimumValueActor, pRenderer,
SCALAR_BAR_LABEL_FONT_SIZE);
const bool bBelowRangeVisible =
m_pBelowRangeSwatchActor->GetVisibility() != 0;
const bool bAboveRangeVisible =
m_pAboveRangeSwatchActor->GetVisibility() != 0;
const double dBelowTextHeight = getOverlayTextHeightPixels(
m_pBelowRangeTextActor, pRenderer,
SCALAR_BAR_LABEL_FONT_SIZE);
const double dAboveTextHeight = getOverlayTextHeightPixels(
m_pAboveRangeTextActor, pRenderer,
SCALAR_BAR_LABEL_FONT_SIZE);
const double dBelowClearance =
bConstantLabelsVisible && bBelowRangeVisible
? qMax(0.0,
(dConstantTextHeight + dBelowTextHeight -
SCALAR_BAR_SWATCH_HEIGHT_PIXELS) * 0.5 * dPixelY)
: 0.0;
const double dAboveClearance =
bConstantLabelsVisible && bAboveRangeVisible
? qMax(0.0,
(dConstantTextHeight + dAboveTextHeight -
SCALAR_BAR_SWATCH_HEIGHT_PIXELS) * 0.5 * dPixelY)
: 0.0;
// 单值端点与越界说明可以同时启用。将越界注记整体向外错开,
// 保证其文字区域不会覆盖位于实际色带端点的单值标签。
const double dBelowY =
dColorY - dSwatchGap - dSwatchHeight -
dBelowClearance;
double dAboveX = dColorX;
double dAboveY =
dColorY + dColorHeight + dSwatchGap +
dAboveClearance;
// 标题默认保持原有偏移;Above max 向外避让后若更高,则标题继续上移。
const double dAboveTextHalfHeight =
m_pAboveRangeTextActor->GetVisibility() != 0
? dAboveTextHeight * 0.5 * dPixelY : 0.0;
const double dAboveContentHalfHeight = qMax(
dSwatchHeight * 0.5, dAboveTextHalfHeight);
const double dAboveContentTop =
dAboveY + dSwatchHeight * 0.5 +
dAboveContentHalfHeight;
const double dBaseTitleY =
dColorY + dColorHeight +
SCALAR_BAR_TITLE_OFFSET_Y_PIXELS * dPixelY;
const double dDesiredTitleY = bAboveRangeVisible
? qMax(dBaseTitleY,
dAboveContentTop +
SCALAR_BAR_TITLE_CONTENT_GAP_PIXELS * dPixelY)
: dBaseTitleY;
const double dMaximumTitleY = bHasViewport
? qMax(0.0, 1.0 -
SCALAR_BAR_TITLE_SAFE_HEIGHT_PIXELS * dPixelY)
: dDesiredTitleY;
const double dTitleY = qMin(dDesiredTitleY, dMaximumTitleY);
if(bAboveRangeVisible &&
dTitleY < dAboveContentTop +
SCALAR_BAR_TITLE_CONTENT_GAP_PIXELS * dPixelY) {
// 极小视口无法同时向上容纳标题和 Above max。此时保持标题不越界,
// 并把越界注记放到标题、单值标签两者的右侧,消除水平交叠。
const double dConstantTextWidth = bConstantLabelsVisible
? getOverlayTextWidthPixels(
m_pConstantMaximumValueActor, pRenderer,
m_sConstantValueText.length() *
SCALAR_BAR_LABEL_FONT_SIZE * 0.6)
: 0.0;
const double dTitleWidth = getOverlayTextWidthPixels(
m_pTitleActor, pRenderer,
m_sTitle.length() *
SCALAR_BAR_TITLE_FONT_SIZE * 0.6);
dAboveX = qMax(
dLabelX + dConstantTextWidth * dPixelX + dTextGap,
dColorX + dTitleWidth * dPixelX + dTextGap);
dAboveY = dColorY + dColorHeight - dSwatchHeight * 0.5;
}
m_pTitleActor->GetPositionCoordinate()->SetValue(
dColorX, dTitleY, 0.0);
updateRectangle(m_pBelowRangeSwatchPoints,
dColorX, dBelowY,
dSwatchWidth, dSwatchHeight);
updateRectangle(m_pAboveRangeSwatchPoints,
dAboveX, dAboveY,
dSwatchWidth, dSwatchHeight);
m_pBelowRangeTextActor->GetPositionCoordinate()->SetValue(
dColorX + dSwatchWidth + dTextGap,
dBelowY + dSwatchHeight * 0.5, 0.0);
m_pAboveRangeTextActor->GetPositionCoordinate()->SetValue(
dAboveX + 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;
}