|
|
#include "nmVolumeSceneController.h"
|
|
|
#include "nmVolumeDataRenderAdapter.h"
|
|
|
#include "nmWxVTKRenderContainerWidget.h"
|
|
|
#include "nmVTKBoundsGridLayer.h"
|
|
|
#include "nmVTKMeshLayer.h"
|
|
|
#include "nmVTKScalarBarLayer.h"
|
|
|
#include "nmVTKScene.h"
|
|
|
|
|
|
#include <QCoreApplication>
|
|
|
#include <QScopedPointer>
|
|
|
#include <QThread>
|
|
|
#include <QUuid>
|
|
|
#include <cmath>
|
|
|
#include <new>
|
|
|
#include <vtkActor.h>
|
|
|
#include <vtkCamera.h>
|
|
|
#include <vtkDataSetMapper.h>
|
|
|
#include <vtkLookupTable.h>
|
|
|
#include <vtkMath.h>
|
|
|
#include <vtkProperty.h>
|
|
|
#include <vtkRenderer.h>
|
|
|
|
|
|
namespace {
|
|
|
// 固定文本标记供 Qt 4 lupdate 提取;运行期通过统一上下文翻译。
|
|
|
static const char* const s_pVolumeTranslationMarkers[] = {
|
|
|
QT_TRANSLATE_NOOP("nmVolumeSceneController", " (Cell)"),
|
|
|
QT_TRANSLATE_NOOP("nmVolumeSceneController", " (Point)"),
|
|
|
QT_TRANSLATE_NOOP("nmVolumeSceneController", "Not enough memory to display the volume mesh."),
|
|
|
QT_TRANSLATE_NOOP("nmVolumeSceneController", "Unable to bind the volume scalar property.")
|
|
|
};
|
|
|
|
|
|
/** @brief Renderer 和 Mapper 只允许所属 GUI 线程操作。 */
|
|
|
bool isVolumeGuiThread()
|
|
|
{
|
|
|
return QCoreApplication::instance() == nullptr ||
|
|
|
QCoreApplication::instance()->thread() == QThread::currentThread();
|
|
|
}
|
|
|
/** @brief 返回与业务窗口解耦的三维场景翻译文本。 */
|
|
|
QString sceneText(const char* pText)
|
|
|
{
|
|
|
Q_UNUSED(s_pVolumeTranslationMarkers);
|
|
|
return QCoreApplication::translate("nmVolumeSceneController", pText);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
class nmVolumeSceneControllerPrivate
|
|
|
{
|
|
|
public:
|
|
|
/** @brief 初始化未挂载图层的空场景,构造本身不改动画面。 */
|
|
|
explicit nmVolumeSceneControllerPrivate(nmWxVTKRenderContainerWidget* pContainer)
|
|
|
: m_pContainer(pContainer), m_pAdapter(nullptr), m_pMesh(nullptr),
|
|
|
m_pScalarBar(nullptr), m_pBounds(nullptr), m_bVisible(false),
|
|
|
m_bScalarBarVisible(true), m_bScaleGridVisible(false) {}
|
|
|
/** @brief 仅移除仍具有相同对象身份的图层,防止误删外部同 ID 图层。 */
|
|
|
~nmVolumeSceneControllerPrivate()
|
|
|
{
|
|
|
// Scene 拥有图层,先解除 Mapper 对适配器的引用,再释放数据。
|
|
|
nmVTKScene* pScene = m_pContainer == nullptr ? nullptr : m_pContainer->getScene();
|
|
|
if(pScene != nullptr) {
|
|
|
if(m_pMesh != nullptr && pScene->getLayer(m_sMeshId) == m_pMesh) pScene->removeLayer(m_sMeshId);
|
|
|
if(m_pScalarBar != nullptr && pScene->getLayer(m_sScalarBarId) == m_pScalarBar) pScene->removeLayer(m_sScalarBarId);
|
|
|
if(m_pBounds != nullptr && pScene->getLayer(m_sBoundsId) == m_pBounds) pScene->removeLayer(m_sBoundsId);
|
|
|
}
|
|
|
delete m_pAdapter;
|
|
|
}
|
|
|
/** @brief 通过 Scene 登记验证观察指针,绝不解引用被外部清理的图层。 */
|
|
|
bool isIntact() const
|
|
|
{
|
|
|
nmVTKScene* pScene = m_pContainer == nullptr ? nullptr : m_pContainer->getScene();
|
|
|
return !m_pData.isNull() && pScene != nullptr && m_pMesh != nullptr &&
|
|
|
m_pScalarBar != nullptr && m_pBounds != nullptr &&
|
|
|
pScene->getLayer(m_sMeshId) == m_pMesh &&
|
|
|
pScene->getLayer(m_sScalarBarId) == m_pScalarBar &&
|
|
|
pScene->getLayer(m_sBoundsId) == m_pBounds;
|
|
|
}
|
|
|
/** @brief 将模型可见状态与各覆盖层偏好合成,不触发 Render。 */
|
|
|
void updateVisibility()
|
|
|
{
|
|
|
if(!isIntact()) return;
|
|
|
m_pMesh->setVisible(m_bVisible);
|
|
|
m_pScalarBar->setVisible(m_bVisible && m_bScalarBarVisible && !m_sPropertyId.isEmpty());
|
|
|
m_pBounds->setVisible(m_bVisible && m_bScaleGridVisible);
|
|
|
}
|
|
|
/** @brief 完整验证候选 LUT 和属性后提交 Mapper,失败时保持旧绑定。 */
|
|
|
bool bindProperty(const QString& sPropertyId)
|
|
|
{
|
|
|
const nmVolumePropertyDescriptor* pProperty = m_pData->findProperty(sPropertyId);
|
|
|
if(pProperty == nullptr) return false;
|
|
|
double dMinimum = pProperty->m_dMinimum;
|
|
|
double dMaximum = pProperty->m_dMaximum;
|
|
|
const bool bConstant = dMinimum == dMaximum;
|
|
|
if(bConstant) {
|
|
|
// LUT 需要严格递增区间;色标仍显示真实常量值,不能伪造业务范围。
|
|
|
const double dPadding = qMax(1.0, std::fabs(dMinimum)) * 1e-6;
|
|
|
dMaximum += dPadding;
|
|
|
if(!vtkMath::IsFinite(dMaximum)) {
|
|
|
dMaximum = dMinimum;
|
|
|
dMinimum -= dPadding;
|
|
|
}
|
|
|
}
|
|
|
vtkSmartPointer<vtkLookupTable> pTable = vtkSmartPointer<vtkLookupTable>::New();
|
|
|
pTable->SetNumberOfTableValues(256);
|
|
|
pTable->SetHueRange(0.67, 0.0);
|
|
|
pTable->SetTableRange(dMinimum, dMaximum);
|
|
|
pTable->SetNanColor(0.65, 0.65, 0.65, 1.0);
|
|
|
pTable->Build();
|
|
|
const nmVTKMeshLayer::ScalarAssociation eAssociation =
|
|
|
pProperty->m_eAssociation == NM_VolumeProperty_CellData
|
|
|
? nmVTKMeshLayer::ScalarAssociation_CellData : nmVTKMeshLayer::ScalarAssociation_PointData;
|
|
|
if(!m_pMesh->setInputConnection(m_pAdapter->getOutputPort(), pProperty->m_sArrayName, eAssociation, pTable)) return false;
|
|
|
m_pMesh->setScalarVisibility(true);
|
|
|
QString sTitle = pProperty->m_sArrayName;
|
|
|
const QString sOtherId = (pProperty->m_eAssociation == NM_VolumeProperty_CellData
|
|
|
? QString::fromLatin1("point:") : QString::fromLatin1("cell:")) + sTitle;
|
|
|
if(m_pData->findProperty(sOtherId) != nullptr) {
|
|
|
sTitle += pProperty->m_eAssociation == NM_VolumeProperty_CellData
|
|
|
? sceneText(" (Cell)") : sceneText(" (Point)");
|
|
|
}
|
|
|
m_pScalarBar->setTitle(sTitle);
|
|
|
m_pScalarBar->setLookupTable(pTable);
|
|
|
m_pScalarBar->setConstantValueLabel(bConstant, QString::number(pProperty->m_dMinimum, 'g', 8));
|
|
|
m_pScalarBar->setNumberOfLabels(bConstant ? 0 : m_pScalarBar->getResponsiveLabelCount());
|
|
|
m_sPropertyId = sPropertyId;
|
|
|
return true;
|
|
|
}
|
|
|
nmWxVTKRenderContainerWidget* m_pContainer; ///< 非所有权,生命周期长于控制器。
|
|
|
QSharedPointer<const nmVolumeData> m_pData; ///< 当前完整只读三维数据强引用。
|
|
|
nmVolumeDataRenderAdapter* m_pAdapter; ///< 本状态独占的渲染管线适配器。
|
|
|
nmVTKMeshLayer* m_pMesh; ///< Scene 所有的主模型图层,仅作非所有权观察。
|
|
|
nmVTKScalarBarLayer* m_pScalarBar; ///< Scene 所有的色标图层。
|
|
|
nmVTKBoundsGridLayer* m_pBounds; ///< Scene 所有的三维坐标框图层。
|
|
|
QString m_sMeshId; ///< 主模型图层稳定 ID,带本次候选唯一前缀。
|
|
|
QString m_sScalarBarId; ///< 色标图层稳定 ID。
|
|
|
QString m_sBoundsId; ///< 三维坐标框图层稳定 ID。
|
|
|
QString m_sPropertyId; ///< 最后成功绑定的属性 ID,纯几何时为空。
|
|
|
bool m_bVisible; ///< 是否已由窗口事务激活,而非尚在准备的隐藏候选。
|
|
|
bool m_bScalarBarVisible; ///< 当前三维模型的独立色标偏好。
|
|
|
bool m_bScaleGridVisible; ///< 当前三维模型的独立坐标框偏好。
|
|
|
};
|
|
|
|
|
|
nmVolumeSceneController::nmVolumeSceneController(nmWxVTKRenderContainerWidget* pContainer)
|
|
|
: m_pPrivate(new nmVolumeSceneControllerPrivate(pContainer)) {}
|
|
|
nmVolumeSceneController::~nmVolumeSceneController()
|
|
|
{
|
|
|
if(m_pPrivate->m_bVisible && m_pPrivate->m_pContainer != nullptr) {
|
|
|
m_pPrivate->m_pContainer->setRotationCenterPickTarget(nullptr);
|
|
|
}
|
|
|
delete m_pPrivate;
|
|
|
}
|
|
|
|
|
|
bool nmVolumeSceneController::setData(const QSharedPointer<const nmVolumeData>& pData, QString* pError)
|
|
|
{
|
|
|
Q_ASSERT(isVolumeGuiThread());
|
|
|
if(pError != nullptr) pError->clear();
|
|
|
if(!isVolumeGuiThread() || pData.isNull() || m_pPrivate->m_pContainer == nullptr) return false;
|
|
|
try {
|
|
|
QScopedPointer<nmVolumeSceneControllerPrivate> pCandidate(new nmVolumeSceneControllerPrivate(m_pPrivate->m_pContainer));
|
|
|
pCandidate->m_pData = pData;
|
|
|
pCandidate->m_pAdapter = new nmVolumeDataRenderAdapter(pData);
|
|
|
// 每次候选使用独立 ID,旧场景直到候选全部准备成功才释放。
|
|
|
const QString sPrefix = QString::fromLatin1("nm.volume.") + QUuid::createUuid().toString();
|
|
|
pCandidate->m_sMeshId = sPrefix + QLatin1String(".mesh");
|
|
|
pCandidate->m_sScalarBarId = sPrefix + QLatin1String(".scalar");
|
|
|
pCandidate->m_sBoundsId = sPrefix + QLatin1String(".bounds");
|
|
|
nmVTKScene* pScene = pCandidate->m_pContainer->getScene();
|
|
|
if(pScene == nullptr) return false;
|
|
|
QScopedPointer<nmVTKMeshLayer> pMesh(new nmVTKMeshLayer(pCandidate->m_sMeshId));
|
|
|
pMesh->setVisible(false);
|
|
|
if(!pScene->addLayer(pMesh.data())) return false;
|
|
|
pCandidate->m_pMesh = pMesh.take();
|
|
|
QScopedPointer<nmVTKScalarBarLayer> pScalarBar(new nmVTKScalarBarLayer(pCandidate->m_sScalarBarId));
|
|
|
pScalarBar->setVisible(false);
|
|
|
if(!pScene->addLayer(pScalarBar.data())) return false;
|
|
|
pCandidate->m_pScalarBar = pScalarBar.take();
|
|
|
QScopedPointer<nmVTKBoundsGridLayer> pBounds(new nmVTKBoundsGridLayer(pCandidate->m_sBoundsId));
|
|
|
pBounds->setVisible(false);
|
|
|
if(!pScene->addLayer(pBounds.data())) return false;
|
|
|
pCandidate->m_pBounds = pBounds.take();
|
|
|
double dBounds[6];
|
|
|
pData->getBounds(dBounds);
|
|
|
pCandidate->m_pBounds->setCamera(pCandidate->m_pContainer->getRenderer()->GetActiveCamera());
|
|
|
pCandidate->m_pBounds->setMode(nmVTKBoundsGridLayer::Mode_Box3D);
|
|
|
if(!pCandidate->m_pBounds->setBounds(dBounds[0], dBounds[1], dBounds[2], dBounds[3], dBounds[4], dBounds[5])) return false;
|
|
|
pCandidate->m_pMesh->setDisplayMode(nmVTKMeshLayer::DisplayMode_SurfaceWithEdges);
|
|
|
pCandidate->m_pMesh->getActor()->GetProperty()->SetEdgeColor(0.0, 0.0, 0.0);
|
|
|
pCandidate->m_pMesh->getActor()->GetProperty()->SetColor(0.65, 0.65, 0.65);
|
|
|
if(pData->getInitialPropertyId().isEmpty()) {
|
|
|
if(!pCandidate->m_pMesh->setInputConnection(pCandidate->m_pAdapter->getOutputPort())) return false;
|
|
|
pCandidate->m_pMesh->setScalarVisibility(false);
|
|
|
} else if(!pCandidate->bindProperty(pData->getInitialPropertyId())) {
|
|
|
if(pError != nullptr) *pError = sceneText("Unable to bind the volume scalar property.");
|
|
|
return false;
|
|
|
}
|
|
|
const QSize oSize = pCandidate->m_pContainer->getViewportSize();
|
|
|
pCandidate->m_pScalarBar->setViewportSize(oSize.width(), oSize.height());
|
|
|
// 从此不再存在失败分支。先解除旧拾取引用,再销毁旧 Actor。
|
|
|
if(m_pPrivate->m_bVisible) m_pPrivate->m_pContainer->setRotationCenterPickTarget(nullptr);
|
|
|
delete m_pPrivate;
|
|
|
m_pPrivate = pCandidate.take();
|
|
|
return true;
|
|
|
} catch(const std::bad_alloc&) {
|
|
|
if(pError != nullptr) *pError = sceneText("Not enough memory to display the volume mesh.");
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
void nmVolumeSceneController::activate(bool bFitCamera)
|
|
|
{
|
|
|
if(!hasData()) return;
|
|
|
double dBounds[6];
|
|
|
m_pPrivate->m_pData->getBounds(dBounds);
|
|
|
m_pPrivate->m_bVisible = true;
|
|
|
m_pPrivate->updateVisibility();
|
|
|
m_pPrivate->m_pContainer->setPrimaryViewBounds(dBounds);
|
|
|
m_pPrivate->m_pContainer->setRotationCenterPickTarget(m_pPrivate->m_pMesh->getActor());
|
|
|
if(bFitCamera) {
|
|
|
m_pPrivate->m_pContainer->fitIsometricViewWithoutRender();
|
|
|
m_pPrivate->m_pContainer->resetRotationCenterWithoutRender();
|
|
|
}
|
|
|
}
|
|
|
void nmVolumeSceneController::hide()
|
|
|
{
|
|
|
if(!isVolumeGuiThread()) return;
|
|
|
m_pPrivate->m_bVisible = false;
|
|
|
m_pPrivate->updateVisibility();
|
|
|
}
|
|
|
void nmVolumeSceneController::clear()
|
|
|
{
|
|
|
if(!isVolumeGuiThread()) return;
|
|
|
nmWxVTKRenderContainerWidget* pContainer = m_pPrivate->m_pContainer;
|
|
|
QScopedPointer<nmVolumeSceneControllerPrivate> pEmpty(new nmVolumeSceneControllerPrivate(pContainer));
|
|
|
if(m_pPrivate->m_bVisible) {
|
|
|
pContainer->setRotationCenterPickTarget(nullptr);
|
|
|
pContainer->clearPrimaryViewBounds();
|
|
|
}
|
|
|
delete m_pPrivate;
|
|
|
m_pPrivate = pEmpty.take();
|
|
|
}
|
|
|
bool nmVolumeSceneController::hasData() const { return isVolumeGuiThread() && m_pPrivate->isIntact(); }
|
|
|
bool nmVolumeSceneController::applyProperty(const QString& sPropertyId, QString* pError)
|
|
|
{
|
|
|
if(pError != nullptr) pError->clear();
|
|
|
if(!hasData()) return false;
|
|
|
if(sPropertyId == m_pPrivate->m_sPropertyId) return true;
|
|
|
if(!m_pPrivate->bindProperty(sPropertyId)) {
|
|
|
if(pError != nullptr) *pError = sceneText("Unable to bind the volume scalar property.");
|
|
|
return false;
|
|
|
}
|
|
|
m_pPrivate->updateVisibility();
|
|
|
if(m_pPrivate->m_bVisible) m_pPrivate->m_pContainer->renderView();
|
|
|
return true;
|
|
|
}
|
|
|
void nmVolumeSceneController::setViewportSize(int nWidth, int nHeight)
|
|
|
{
|
|
|
if(hasData()) m_pPrivate->m_pScalarBar->setViewportSize(nWidth, nHeight);
|
|
|
}
|
|
|
void nmVolumeSceneController::setScalarBarVisible(bool bVisible)
|
|
|
{
|
|
|
if(!isVolumeGuiThread()) return;
|
|
|
if(m_pPrivate->m_bScalarBarVisible == bVisible) return;
|
|
|
m_pPrivate->m_bScalarBarVisible = bVisible;
|
|
|
m_pPrivate->updateVisibility();
|
|
|
if(hasData() && m_pPrivate->m_bVisible) m_pPrivate->m_pContainer->renderView();
|
|
|
}
|
|
|
void nmVolumeSceneController::setScaleGridVisible(bool bVisible)
|
|
|
{
|
|
|
if(!isVolumeGuiThread()) return;
|
|
|
if(m_pPrivate->m_bScaleGridVisible == bVisible) return;
|
|
|
m_pPrivate->m_bScaleGridVisible = bVisible;
|
|
|
m_pPrivate->updateVisibility();
|
|
|
if(hasData() && m_pPrivate->m_bVisible) m_pPrivate->m_pContainer->renderView();
|
|
|
}
|
|
|
bool nmVolumeSceneController::isScalarBarVisible() const { return m_pPrivate->m_bScalarBarVisible; }
|
|
|
bool nmVolumeSceneController::isScaleGridVisible() const { return m_pPrivate->m_bScaleGridVisible; }
|
|
|
bool nmVolumeSceneController::hasScalarProperty() const { return hasData() && !m_pPrivate->m_sPropertyId.isEmpty(); }
|