|
|
#include "nmVolumeSceneController.h"
|
|
|
#include "nmVolumeDataRenderAdapter.h"
|
|
|
#include "nmVolumeSlicePipeline.h"
|
|
|
#include "nmWxVTKRenderContainerWidget.h"
|
|
|
#include "nmVTKMeshLayer.h"
|
|
|
#include "nmVTKColorMapCatalog.h"
|
|
|
#include "nmVTKScalarBarLayer.h"
|
|
|
#include "nmVTKScene.h"
|
|
|
|
|
|
#include <QCoreApplication>
|
|
|
#include <QMap>
|
|
|
#include <QScopedPointer>
|
|
|
#include <QThread>
|
|
|
#include <QUuid>
|
|
|
#include <cmath>
|
|
|
#include <new>
|
|
|
#include <vtkActor.h>
|
|
|
#include <vtkCellData.h>
|
|
|
#include <vtkDataArray.h>
|
|
|
#include <vtkDataSetMapper.h>
|
|
|
#include <vtkLookupTable.h>
|
|
|
#include <vtkMath.h>
|
|
|
#include <vtkProperty.h>
|
|
|
#include <vtkPointData.h>
|
|
|
#include <vtkPolyData.h>
|
|
|
|
|
|
namespace {
|
|
|
// 固定文本标记供 Qt 4 lupdate 提取;运行期通过统一上下文翻译。
|
|
|
static const char* const s_pVolumeTranslationMarkers[] = {
|
|
|
QT_TRANSLATE_NOOP("nmVolumeSceneController", "Not enough memory to display the volume mesh."),
|
|
|
QT_TRANSLATE_NOOP("nmVolumeSceneController", "Unable to bind the volume scalar property."),
|
|
|
QT_TRANSLATE_NOOP("nmVolumeSceneController", "The color settings are invalid."),
|
|
|
QT_TRANSLATE_NOOP("nmVolumeSceneController", "The selected color map is unavailable."),
|
|
|
QT_TRANSLATE_NOOP("nmVolumeSceneController", "The manual range must contain two finite increasing values."),
|
|
|
QT_TRANSLATE_NOOP("nmVolumeSceneController", "Unable to construct the color mapping."),
|
|
|
QT_TRANSLATE_NOOP("nmVolumeSceneController", "The color settings no longer match the current volume property."),
|
|
|
QT_TRANSLATE_NOOP("nmVolumeSceneController", "Unable to apply the color settings.")
|
|
|
};
|
|
|
|
|
|
/** @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);
|
|
|
}
|
|
|
|
|
|
/** @brief 校验后构造独立颜色表;只读属性目录,绝不修改已有 Mapper 或已发布数据。 */
|
|
|
vtkSmartPointer<vtkLookupTable> makeVolumeLookupTable(const nmVolumePropertyDescriptor& oProperty,
|
|
|
const nmPebiResultDisplaySettings& oSettings, QString* pError = nullptr)
|
|
|
{
|
|
|
if(!oSettings.isStructurallyValid() || oSettings.m_eScaleMode != NM_PebiColorScale_Linear ||
|
|
|
oSettings.m_bUseBelowRangeColor || oSettings.m_bUseAboveRangeColor) {
|
|
|
if(pError != nullptr) *pError = sceneText("The color settings are invalid.");
|
|
|
return vtkSmartPointer<vtkLookupTable>();
|
|
|
}
|
|
|
if(!nmVTKColorMapCatalog::contains(oSettings.m_sColorMapId)) {
|
|
|
if(pError != nullptr) *pError = sceneText("The selected color map is unavailable.");
|
|
|
return vtkSmartPointer<vtkLookupTable>();
|
|
|
}
|
|
|
double dMinimum = oSettings.m_bHasManualRange ? oSettings.m_dManualMinimum : oProperty.m_dMinimum;
|
|
|
double dMaximum = oSettings.m_bHasManualRange ? oSettings.m_dManualMaximum : oProperty.m_dMaximum;
|
|
|
if(!vtkMath::IsFinite(dMinimum) || !vtkMath::IsFinite(dMaximum) || dMinimum > dMaximum ||
|
|
|
(oSettings.m_bHasManualRange && dMinimum == dMaximum)) {
|
|
|
if(pError != nullptr) *pError = sceneText("The manual range must contain two finite increasing values.");
|
|
|
return vtkSmartPointer<vtkLookupTable>();
|
|
|
}
|
|
|
const bool bSingleValue = !oSettings.m_bHasManualRange && dMinimum == dMaximum;
|
|
|
if(dMinimum == dMaximum) {
|
|
|
// 常量对外仍显示真实数值;只扩展内部范围,并兼容接近 double 上限的值。
|
|
|
const double dPadding = qMax(1.0, std::fabs(dMinimum)) * 1e-6;
|
|
|
dMaximum += dPadding;
|
|
|
if(!vtkMath::IsFinite(dMaximum)) { dMaximum = dMinimum; dMinimum -= dPadding; }
|
|
|
}
|
|
|
vtkSmartPointer<vtkLookupTable> pTable = nmVTKColorMapCatalog::createLookupTable(oSettings.m_sColorMapId);
|
|
|
if(pTable == nullptr || !vtkMath::IsFinite(dMinimum) || !vtkMath::IsFinite(dMaximum) || dMinimum >= dMaximum) {
|
|
|
if(pError != nullptr) *pError = sceneText("Unable to construct the color mapping.");
|
|
|
return vtkSmartPointer<vtkLookupTable>();
|
|
|
}
|
|
|
pTable->SetTableRange(dMinimum, dMaximum);
|
|
|
pTable->SetScaleToLinear();
|
|
|
pTable->SetUseBelowRangeColor(0);
|
|
|
pTable->SetUseAboveRangeColor(0);
|
|
|
pTable->IndexedLookupOff();
|
|
|
pTable->SetNanColor(0.65, 0.65, 0.65, 1.0);
|
|
|
if(bSingleValue) {
|
|
|
// 与现有结果窗口一致,自动常量色标整体使用色带首色,避免暗示不存在的梯度。
|
|
|
double dColor[4];
|
|
|
pTable->GetTableValue(0, dColor);
|
|
|
for(vtkIdType nIndex = 0; nIndex < pTable->GetNumberOfTableValues(); ++nIndex) {
|
|
|
pTable->SetTableValue(nIndex, dColor);
|
|
|
}
|
|
|
}
|
|
|
pTable->BuildSpecialColors();
|
|
|
return pTable;
|
|
|
}
|
|
|
|
|
|
/** @brief 候选挂入 Scene 后的回退保护;异常或提前返回时仅撤销该候选图层。 */
|
|
|
class VolumeSliceLayerGuard
|
|
|
{
|
|
|
public:
|
|
|
/** @brief 在挂载之前准备 ID,避免挂载后字符串分配失败造成图层遗留。 */
|
|
|
VolumeSliceLayerGuard(nmVTKScene* pScene, const QString& sId)
|
|
|
: m_pScene(pScene), m_sId(sId), m_pLayer(nullptr) {}
|
|
|
/** @brief 候选未提交时解除 Scene 所有权并删除,已提交时不操作。 */
|
|
|
~VolumeSliceLayerGuard()
|
|
|
{
|
|
|
if(m_pLayer != nullptr && m_pScene->getLayer(m_sId) == m_pLayer) m_pScene->removeLayer(m_sId);
|
|
|
}
|
|
|
/** @brief 记录成功挂载的图层,由 Scene 实际拥有对象。 */
|
|
|
void hold(nmVTKMeshLayer* pLayer) { m_pLayer = pLayer; }
|
|
|
/** @brief 事务提交后解除回退保护。 */
|
|
|
void release() { m_pLayer = nullptr; }
|
|
|
private:
|
|
|
Q_DISABLE_COPY(VolumeSliceLayerGuard)
|
|
|
nmVTKScene* m_pScene; ///< 非所有权 Scene,存活期覆盖本次同步事务。
|
|
|
QString m_sId; ///< 待提交候选的唯一身份。
|
|
|
nmVTKMeshLayer* m_pLayer; ///< Scene 所有的候选指针,仅用于身份检查。
|
|
|
};
|
|
|
|
|
|
/** @brief 两个网格图层的颜色绑定保护;任一绑定失败时恢复已经更换的颜色表。 */
|
|
|
class VolumeColorBindingGuard
|
|
|
{
|
|
|
public:
|
|
|
/** @brief 保存完整模型及可着色截面的旧颜色表,输入图层必须在作用域内存活。 */
|
|
|
VolumeColorBindingGuard(nmVTKMeshLayer* pMesh, nmVTKMeshLayer* pSlice)
|
|
|
: m_pMesh(pMesh), m_pSlice(pSlice), m_bMeshChanged(false), m_bSliceChanged(false),
|
|
|
m_pOldMeshTable(pMesh->getLookupTable()),
|
|
|
m_pOldSliceTable(pSlice == nullptr ? nullptr : pSlice->getLookupTable()) {}
|
|
|
/** @brief 事务未提交时恢复已修改图层;恢复过程不主动渲染。 */
|
|
|
~VolumeColorBindingGuard()
|
|
|
{
|
|
|
if(m_bMeshChanged) m_pMesh->setLookupTable(m_pOldMeshTable);
|
|
|
if(m_bSliceChanged) m_pSlice->setLookupTable(m_pOldSliceTable);
|
|
|
}
|
|
|
/** @brief 先绑定截面、后绑定模型,返回失败时由析构保护统一撤销。 */
|
|
|
bool bind(vtkLookupTable* pTable)
|
|
|
{
|
|
|
if(m_pSlice != nullptr) {
|
|
|
if(!m_pSlice->setLookupTable(pTable)) return false;
|
|
|
m_bSliceChanged = true;
|
|
|
}
|
|
|
if(!m_pMesh->setLookupTable(pTable)) return false;
|
|
|
m_bMeshChanged = true;
|
|
|
return true;
|
|
|
}
|
|
|
/** @brief 设置、颜色表及色标全部提交后解除回退保护。 */
|
|
|
void release() { m_bMeshChanged = m_bSliceChanged = false; }
|
|
|
private:
|
|
|
Q_DISABLE_COPY(VolumeColorBindingGuard)
|
|
|
nmVTKMeshLayer* m_pMesh; ///< 非所有权完整模型图层。
|
|
|
nmVTKMeshLayer* m_pSlice; ///< 非所有权截面图层;无交集、全缺失时为空。
|
|
|
bool m_bMeshChanged; ///< 完整模型已经切换颜色表,尚未提交。
|
|
|
bool m_bSliceChanged; ///< 截面已经切换颜色表,尚未提交。
|
|
|
vtkSmartPointer<vtkLookupTable> m_pOldMeshTable; ///< 回退所需的旧模型颜色表强引用。
|
|
|
vtkSmartPointer<vtkLookupTable> m_pOldSliceTable; ///< 回退所需的旧截面颜色表强引用。
|
|
|
};
|
|
|
}
|
|
|
|
|
|
class nmVolumeSceneControllerPrivate
|
|
|
{
|
|
|
public:
|
|
|
/** @brief 初始化未挂载图层的空场景,构造本身不改动画面。 */
|
|
|
explicit nmVolumeSceneControllerPrivate(nmWxVTKRenderContainerWidget* pContainer)
|
|
|
: m_pContainer(pContainer), m_pAdapter(nullptr), m_pMesh(nullptr),
|
|
|
m_pScalarBar(nullptr), m_pSliceMesh(nullptr),
|
|
|
m_eMeshDisplayMode(nmVTKMeshLayer::DisplayMode_SurfaceWithEdges),
|
|
|
m_bSliceEnabled(false), m_nSliceRevision(0), m_bVisible(false),
|
|
|
m_bScalarBarVisible(true) {}
|
|
|
/** @brief 仅移除仍具有相同对象身份的图层,防止误删外部同 ID 图层。 */
|
|
|
~nmVolumeSceneControllerPrivate()
|
|
|
{
|
|
|
// Scene 拥有图层,先解除 Mapper 对适配器的引用,再释放数据。
|
|
|
nmVTKScene* pScene = m_pContainer == nullptr ? nullptr : m_pContainer->getScene();
|
|
|
if(pScene != nullptr) {
|
|
|
if(m_pSliceMesh != nullptr && pScene->getLayer(m_sSliceId) == m_pSliceMesh) pScene->removeLayer(m_sSliceId);
|
|
|
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);
|
|
|
}
|
|
|
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 &&
|
|
|
pScene->getLayer(m_sMeshId) == m_pMesh &&
|
|
|
pScene->getLayer(m_sScalarBarId) == m_pScalarBar &&
|
|
|
(m_pSliceMesh == nullptr || pScene->getLayer(m_sSliceId) == m_pSliceMesh);
|
|
|
}
|
|
|
/** @brief 将模型可见状态与各覆盖层偏好合成,不触发 Render。 */
|
|
|
void updateVisibility()
|
|
|
{
|
|
|
if(!isIntact()) return;
|
|
|
const bool bHasSlice = m_pSliceData != nullptr && m_pSliceData->GetNumberOfCells() > 0;
|
|
|
// 参照模型固定为灰色半透明表面;完整模型和截面沿用底栏选中的显示模式。
|
|
|
m_pMesh->setOpacity(m_bSliceEnabled ? 0.15 : 1.0);
|
|
|
m_pMesh->setScalarVisibility(!m_bSliceEnabled && !m_sPropertyId.isEmpty());
|
|
|
m_pMesh->setDisplayMode(m_bSliceEnabled
|
|
|
? nmVTKMeshLayer::DisplayMode_Surface : m_eMeshDisplayMode);
|
|
|
m_pMesh->setVisible(m_bVisible && (!m_bSliceEnabled || m_oSliceSettings.m_bShowSource));
|
|
|
if(m_pSliceMesh != nullptr) {
|
|
|
m_pSliceMesh->setDisplayMode(m_eMeshDisplayMode);
|
|
|
m_pSliceMesh->setVisible(m_bVisible && m_bSliceEnabled && bHasSlice);
|
|
|
}
|
|
|
m_pScalarBar->setVisible(m_bVisible && m_bScalarBarVisible && !m_sPropertyId.isEmpty() && (!m_bSliceEnabled || bHasSlice));
|
|
|
}
|
|
|
/** @brief 仅在当前场景激活时更新拾取目标,隐藏候选不会抢占其他业务场景。 */
|
|
|
void updatePickTarget()
|
|
|
{
|
|
|
if(!m_bVisible || !isIntact()) return;
|
|
|
vtkActor* pTarget = m_pMesh->getActor();
|
|
|
if(m_bSliceEnabled) {
|
|
|
pTarget = m_pSliceMesh != nullptr && m_pSliceData != nullptr && m_pSliceData->GetNumberOfCells() > 0
|
|
|
? m_pSliceMesh->getActor() : nullptr;
|
|
|
}
|
|
|
m_pContainer->setRotationCenterPickTarget(pTarget);
|
|
|
}
|
|
|
/** @brief 同步色标及常量标签;只更新显示对象,不执行 Render。 */
|
|
|
void applyScalarBarState(const nmVolumePropertyDescriptor& oProperty,
|
|
|
const nmPebiResultDisplaySettings& oSettings, vtkLookupTable* pTable)
|
|
|
{
|
|
|
const bool bConstant = !oSettings.m_bHasManualRange && oProperty.m_dMinimum == oProperty.m_dMaximum;
|
|
|
m_pScalarBar->setTitle(oProperty.m_sArrayName);
|
|
|
m_pScalarBar->setLookupTable(pTable);
|
|
|
m_pScalarBar->setConstantValueLabel(bConstant, QString::number(oProperty.m_dMinimum, 'g', 8));
|
|
|
m_pScalarBar->setNumberOfLabels(bConstant ? 0 : m_pScalarBar->getResponsiveLabelCount());
|
|
|
}
|
|
|
/** @brief 完整验证候选 LUT 和属性后提交 Mapper,失败时保持旧绑定。 */
|
|
|
bool bindProperty(const QString& sPropertyId, vtkLookupTable* pPreparedTable = nullptr)
|
|
|
{
|
|
|
const nmVolumePropertyDescriptor* pProperty = m_pData->findProperty(sPropertyId);
|
|
|
if(pProperty == nullptr) return false;
|
|
|
const nmPebiResultDisplaySettings oSettings = m_mapDisplaySettings.value(sPropertyId);
|
|
|
vtkSmartPointer<vtkLookupTable> pTable = pPreparedTable;
|
|
|
if(pTable == nullptr) pTable = makeVolumeLookupTable(*pProperty, oSettings);
|
|
|
if(pTable == nullptr) return false;
|
|
|
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);
|
|
|
applyScalarBarState(*pProperty, oSettings, pTable);
|
|
|
m_sPropertyId = sPropertyId;
|
|
|
return true;
|
|
|
}
|
|
|
nmWxVTKRenderContainerWidget* m_pContainer; ///< 非所有权,生命周期长于控制器。
|
|
|
QSharedPointer<const nmVolumeData> m_pData; ///< 当前完整只读三维数据强引用。
|
|
|
nmVolumeDataRenderAdapter* m_pAdapter; ///< 本状态独占的渲染管线适配器。
|
|
|
nmVTKMeshLayer* m_pMesh; ///< Scene 所有的主模型图层,仅作非所有权观察。
|
|
|
nmVTKScalarBarLayer* m_pScalarBar; ///< Scene 所有的色标图层。
|
|
|
nmVTKMeshLayer* m_pSliceMesh; ///< Scene 所有的截面图层;每次成功计算后替换。
|
|
|
vtkSmartPointer<vtkPolyData> m_pSliceData; ///< 与 Cutter 断开的独立截面,关闭显示时保留。
|
|
|
nmVolumeSliceSettings m_oSliceSettings; ///< 当前数据最后一次成功应用的切片参数。
|
|
|
QMap<QString, nmPebiResultDisplaySettings> m_mapDisplaySettings; ///< 按单元/节点属性 ID 独立记忆的颜色设置,仅当前模型有效。
|
|
|
nmVTKMeshLayer::DisplayMode m_eMeshDisplayMode; ///< 完整模型及截面的共用显示偏好,不参与切片计算。
|
|
|
QString m_sSliceId; ///< 当前截面图层唯一 ID,不与完整模型图层复用。
|
|
|
bool m_bSliceEnabled; ///< 是否正在显示截面,而非完整模型。
|
|
|
unsigned int m_nSliceRevision; ///< 本次数据成功应用截面的次数,预览不增加。
|
|
|
QString m_sMeshId; ///< 主模型图层稳定 ID,带本次候选唯一前缀。
|
|
|
QString m_sScalarBarId; ///< 色标图层稳定 ID。
|
|
|
QString m_sPropertyId; ///< 最后成功绑定的属性 ID,纯几何时为空。
|
|
|
bool m_bVisible; ///< 是否已由窗口事务激活,而非尚在准备的隐藏候选。
|
|
|
bool m_bScalarBarVisible; ///< 当前三维模型的独立色标偏好。
|
|
|
};
|
|
|
|
|
|
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_oSliceSettings = nmVolumeSliceSettings::fromData(*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");
|
|
|
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();
|
|
|
pCandidate->m_pMesh->setDisplayMode(pCandidate->m_eMeshDisplayMode);
|
|
|
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->updatePickTarget();
|
|
|
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->m_pSliceData != nullptr) {
|
|
|
nmVolumeSliceSettings oSettings = m_pPrivate->m_oSliceSettings;
|
|
|
oSettings.m_sPropertyId = sPropertyId;
|
|
|
const bool bWasEnabled = m_pPrivate->m_bSliceEnabled;
|
|
|
if(!applySliceSettings(oSettings, pError)) return false;
|
|
|
if(!bWasEnabled) setSliceEnabled(false);
|
|
|
return true;
|
|
|
}
|
|
|
if(!m_pPrivate->bindProperty(sPropertyId)) {
|
|
|
if(pError != nullptr) *pError = sceneText("Unable to bind the volume scalar property.");
|
|
|
return false;
|
|
|
}
|
|
|
m_pPrivate->updateVisibility();
|
|
|
m_pPrivate->m_oSliceSettings.m_sPropertyId = sPropertyId;
|
|
|
if(m_pPrivate->m_bVisible) m_pPrivate->m_pContainer->renderView();
|
|
|
return true;
|
|
|
}
|
|
|
QString nmVolumeSceneController::getCurrentPropertyId() const
|
|
|
{
|
|
|
return hasData() ? m_pPrivate->m_sPropertyId : QString();
|
|
|
}
|
|
|
|
|
|
bool nmVolumeSceneController::getCurrentPropertyRange(double dRange[2]) const
|
|
|
{
|
|
|
if(!hasScalarProperty() || dRange == nullptr) return false;
|
|
|
const nmVolumePropertyDescriptor* pProperty = m_pPrivate->m_pData->findProperty(m_pPrivate->m_sPropertyId);
|
|
|
if(pProperty == nullptr) return false;
|
|
|
dRange[0] = pProperty->m_dMinimum;
|
|
|
dRange[1] = pProperty->m_dMaximum;
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
bool nmVolumeSceneController::getCurrentDisplaySettings(nmPebiResultDisplaySettings& oSettings) const
|
|
|
{
|
|
|
if(!hasScalarProperty()) return false;
|
|
|
oSettings = m_pPrivate->m_mapDisplaySettings.value(m_pPrivate->m_sPropertyId);
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
bool nmVolumeSceneController::getCurrentDisplayRange(double dRange[2]) const
|
|
|
{
|
|
|
if(!getCurrentPropertyRange(dRange)) return false;
|
|
|
const nmPebiResultDisplaySettings oSettings = m_pPrivate->m_mapDisplaySettings.value(m_pPrivate->m_sPropertyId);
|
|
|
if(oSettings.m_bHasManualRange) {
|
|
|
dRange[0] = oSettings.m_dManualMinimum;
|
|
|
dRange[1] = oSettings.m_dManualMaximum;
|
|
|
}
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
bool nmVolumeSceneController::applyCurrentDisplaySettings(const QString& sExpectedDataId,
|
|
|
const QString& sExpectedPropertyId, const nmPebiResultDisplaySettings& oSettings, QString* pError)
|
|
|
{
|
|
|
if(pError != nullptr) pError->clear();
|
|
|
if(!hasScalarProperty() || sExpectedDataId != m_pPrivate->m_pData->getDataId() ||
|
|
|
sExpectedPropertyId != m_pPrivate->m_sPropertyId) {
|
|
|
if(pError != nullptr) *pError = sceneText("The color settings no longer match the current volume property.");
|
|
|
return false;
|
|
|
}
|
|
|
try {
|
|
|
const nmVolumePropertyDescriptor* pProperty = m_pPrivate->m_pData->findProperty(sExpectedPropertyId);
|
|
|
if(pProperty == nullptr) return false;
|
|
|
// 先完整校验原始输入,再规范化;非法数值不能被自动范围的归零处理掩盖。
|
|
|
vtkSmartPointer<vtkLookupTable> pTable = makeVolumeLookupTable(*pProperty, oSettings, pError);
|
|
|
if(pTable == nullptr) return false;
|
|
|
const nmPebiResultDisplaySettings oNormalized = oSettings.normalized();
|
|
|
if(oNormalized == m_pPrivate->m_mapDisplaySettings.value(sExpectedPropertyId)) return true;
|
|
|
QMap<QString, nmPebiResultDisplaySettings> mapCandidateSettings = m_pPrivate->m_mapDisplaySettings;
|
|
|
mapCandidateSettings.insert(sExpectedPropertyId, oNormalized);
|
|
|
// 缓存截面即使隐藏也同步;全缺失和空截面不绑定标量,只更新模型与全局色标。
|
|
|
nmVTKMeshLayer* pColoredSlice = m_pPrivate->m_pSliceMesh;
|
|
|
if(pColoredSlice != nullptr && !pColoredSlice->isScalarVisible()) pColoredSlice = nullptr;
|
|
|
VolumeColorBindingGuard oGuard(m_pPrivate->m_pMesh, pColoredSlice);
|
|
|
if(!oGuard.bind(pTable)) {
|
|
|
if(pError != nullptr) *pError = sceneText("Unable to apply the color settings.");
|
|
|
return false;
|
|
|
}
|
|
|
m_pPrivate->applyScalarBarState(*pProperty, oNormalized, pTable);
|
|
|
m_pPrivate->m_mapDisplaySettings.swap(mapCandidateSettings);
|
|
|
oGuard.release();
|
|
|
if(m_pPrivate->m_bVisible) m_pPrivate->m_pContainer->renderView();
|
|
|
return true;
|
|
|
} catch(const std::bad_alloc&) {
|
|
|
if(pError != nullptr) *pError = sceneText("Not enough memory to display the volume mesh.");
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
void nmVolumeSceneController::setMeshDisplayMode(nmVTKMeshLayer::DisplayMode eMode)
|
|
|
{
|
|
|
if(!hasData() || !nmVTKMeshLayer::isValidDisplayMode(eMode) ||
|
|
|
m_pPrivate->m_eMeshDisplayMode == eMode) return;
|
|
|
m_pPrivate->m_eMeshDisplayMode = eMode;
|
|
|
m_pPrivate->updateVisibility();
|
|
|
if(m_pPrivate->m_bVisible) m_pPrivate->m_pContainer->renderView();
|
|
|
}
|
|
|
|
|
|
nmVTKMeshLayer::DisplayMode nmVolumeSceneController::getMeshDisplayMode() const
|
|
|
{
|
|
|
return m_pPrivate->m_eMeshDisplayMode;
|
|
|
}
|
|
|
|
|
|
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();
|
|
|
}
|
|
|
bool nmVolumeSceneController::isScalarBarVisible() const { return m_pPrivate->m_bScalarBarVisible; }
|
|
|
bool nmVolumeSceneController::hasScalarProperty() const { return hasData() && !m_pPrivate->m_sPropertyId.isEmpty(); }
|
|
|
|
|
|
QSharedPointer<const nmVolumeData> nmVolumeSceneController::getData() const
|
|
|
{
|
|
|
return hasData() ? m_pPrivate->m_pData : QSharedPointer<const nmVolumeData>();
|
|
|
}
|
|
|
|
|
|
nmVolumeSliceSettings nmVolumeSceneController::getSliceSettings() const { return m_pPrivate->m_oSliceSettings; }
|
|
|
bool nmVolumeSceneController::isSliceEnabled() const { return hasData() && m_pPrivate->m_bSliceEnabled; }
|
|
|
bool nmVolumeSceneController::isSliceEmpty() const
|
|
|
{
|
|
|
return isSliceEnabled() && m_pPrivate->m_pSliceData != nullptr && m_pPrivate->m_pSliceData->GetNumberOfCells() == 0;
|
|
|
}
|
|
|
unsigned int nmVolumeSceneController::getSliceRevision() const { return m_pPrivate->m_nSliceRevision; }
|
|
|
|
|
|
bool nmVolumeSceneController::setSliceEnabled(bool bEnabled, QString* pError)
|
|
|
{
|
|
|
if(pError != nullptr) pError->clear();
|
|
|
if(!hasData()) return false;
|
|
|
if(bEnabled && m_pPrivate->m_pSliceData == nullptr) return applySliceSettings(m_pPrivate->m_oSliceSettings, pError);
|
|
|
if(m_pPrivate->m_bSliceEnabled == bEnabled) return true;
|
|
|
m_pPrivate->m_bSliceEnabled = bEnabled;
|
|
|
m_pPrivate->updateVisibility();
|
|
|
m_pPrivate->updatePickTarget();
|
|
|
if(m_pPrivate->m_bVisible) m_pPrivate->m_pContainer->renderView();
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
void nmVolumeSceneController::setSliceDisplayOptions(bool bShowPlane, bool bShowSource)
|
|
|
{
|
|
|
if(!hasData()) return;
|
|
|
m_pPrivate->m_oSliceSettings.m_bShowPlane = bShowPlane;
|
|
|
m_pPrivate->m_oSliceSettings.m_bShowSource = bShowSource;
|
|
|
m_pPrivate->updateVisibility();
|
|
|
if(m_pPrivate->m_bVisible) m_pPrivate->m_pContainer->renderView();
|
|
|
}
|
|
|
|
|
|
bool nmVolumeSceneController::applySliceSettings(const nmVolumeSliceSettings& oSettings, QString* pError)
|
|
|
{
|
|
|
if(pError != nullptr) pError->clear();
|
|
|
if(!hasData()) return false;
|
|
|
nmVolumeSliceSettings oCandidateSettings = oSettings;
|
|
|
if(!oCandidateSettings.normalize(pError)) return false;
|
|
|
const nmVolumePropertyDescriptor* pProperty = m_pPrivate->m_pData->findProperty(oCandidateSettings.m_sPropertyId);
|
|
|
if(pProperty == nullptr && (!oCandidateSettings.m_sPropertyId.isEmpty() || !m_pPrivate->m_pData->getProperties().isEmpty())) {
|
|
|
if(pError != nullptr) *pError = sceneText("Unable to bind the volume scalar property.");
|
|
|
return false;
|
|
|
}
|
|
|
vtkSmartPointer<vtkPolyData> pCandidateData;
|
|
|
if(!nmVolumeSlicePipeline::build(m_pPrivate->m_pData, oCandidateSettings, pCandidateData, pError)) return false;
|
|
|
try {
|
|
|
const QString sCandidateId = QString::fromLatin1("nm.volume.slice.") + QUuid::createUuid().toString();
|
|
|
QScopedPointer<nmVTKMeshLayer> pCandidateMesh(new nmVTKMeshLayer(sCandidateId));
|
|
|
pCandidateMesh->setVisible(false);
|
|
|
pCandidateMesh->setDisplayMode(m_pPrivate->m_eMeshDisplayMode);
|
|
|
pCandidateMesh->getActor()->GetProperty()->SetColor(0.65, 0.65, 0.65);
|
|
|
pCandidateMesh->getActor()->GetProperty()->SetEdgeColor(0.0, 0.0, 0.0);
|
|
|
// 关闭光照明暗,截面颜色直接对应色标,方便定量核对。
|
|
|
pCandidateMesh->getActor()->GetProperty()->LightingOff();
|
|
|
if(!pCandidateMesh->setDataSet(pCandidateData)) return false;
|
|
|
bool bHasFiniteValues = false;
|
|
|
if(pProperty != nullptr && pCandidateData->GetNumberOfCells() > 0) {
|
|
|
vtkDataArray* pValues = pProperty->m_eAssociation == NM_VolumeProperty_CellData
|
|
|
? pCandidateData->GetCellData()->GetArray(pProperty->m_sArrayName.toUtf8().constData())
|
|
|
: pCandidateData->GetPointData()->GetArray(pProperty->m_sArrayName.toUtf8().constData());
|
|
|
for(vtkIdType nValue = 0; pValues != nullptr && nValue < pValues->GetNumberOfTuples(); ++nValue) {
|
|
|
if(vtkMath::IsFinite(pValues->GetComponent(nValue, 0))) { bHasFiniteValues = true; break; }
|
|
|
}
|
|
|
}
|
|
|
// 每次生成截面均读取当前属性的颜色偏好,自动范围始终取完整模型范围。
|
|
|
vtkSmartPointer<vtkLookupTable> pTable;
|
|
|
if(pProperty != nullptr) {
|
|
|
pTable = makeVolumeLookupTable(*pProperty,
|
|
|
m_pPrivate->m_mapDisplaySettings.value(oCandidateSettings.m_sPropertyId), pError);
|
|
|
if(pTable == nullptr) return false;
|
|
|
const nmVTKMeshLayer::ScalarAssociation eAssociation = pProperty->m_eAssociation == NM_VolumeProperty_CellData
|
|
|
? nmVTKMeshLayer::ScalarAssociation_CellData : nmVTKMeshLayer::ScalarAssociation_PointData;
|
|
|
if(bHasFiniteValues) {
|
|
|
if(!pCandidateMesh->setScalarArray(pProperty->m_sArrayName, eAssociation, pTable)) return false;
|
|
|
pCandidateMesh->setScalarVisibility(true);
|
|
|
}
|
|
|
}
|
|
|
nmVTKScene* pScene = m_pPrivate->m_pContainer->getScene();
|
|
|
VolumeSliceLayerGuard oGuard(pScene, sCandidateId);
|
|
|
if(!pScene->addLayer(pCandidateMesh.data())) return false;
|
|
|
nmVTKMeshLayer* pNewMesh = pCandidateMesh.take();
|
|
|
oGuard.hold(pNewMesh);
|
|
|
// 全部候选已验证。完整模型的属性绑定也按原接口原子提交,失败时撤销新图层。
|
|
|
if(pProperty != nullptr && !m_pPrivate->bindProperty(oCandidateSettings.m_sPropertyId, pTable)) {
|
|
|
if(pError != nullptr) *pError = sceneText("Unable to bind the volume scalar property.");
|
|
|
return false;
|
|
|
}
|
|
|
if(m_pPrivate->m_bVisible) m_pPrivate->m_pContainer->setRotationCenterPickTarget(nullptr);
|
|
|
if(m_pPrivate->m_pSliceMesh != nullptr) pScene->removeLayer(m_pPrivate->m_sSliceId);
|
|
|
m_pPrivate->m_sSliceId = sCandidateId;
|
|
|
m_pPrivate->m_pSliceMesh = pNewMesh;
|
|
|
m_pPrivate->m_pSliceData = pCandidateData;
|
|
|
m_pPrivate->m_oSliceSettings = oCandidateSettings;
|
|
|
m_pPrivate->m_bSliceEnabled = true;
|
|
|
oGuard.release();
|
|
|
++m_pPrivate->m_nSliceRevision;
|
|
|
m_pPrivate->updateVisibility();
|
|
|
m_pPrivate->updatePickTarget();
|
|
|
if(m_pPrivate->m_bVisible) m_pPrivate->m_pContainer->renderView();
|
|
|
return true;
|
|
|
} catch(const std::bad_alloc&) {
|
|
|
if(pError != nullptr) *pError = sceneText("Not enough memory to display the volume mesh.");
|
|
|
return false;
|
|
|
}
|
|
|
}
|