|
|
|
|
@ -3,10 +3,12 @@
|
|
|
|
|
#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>
|
|
|
|
|
@ -26,7 +28,13 @@ 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", "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 线程操作。 */
|
|
|
|
|
@ -42,22 +50,53 @@ QString sceneText(const char* pText)
|
|
|
|
|
return QCoreApplication::translate("nmVolumeSceneController", pText);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @brief 完整模型与截面共用的全局线性颜色表;常量范围只为渲染作最小扩展。 */
|
|
|
|
|
vtkSmartPointer<vtkLookupTable> makeVolumeLookupTable(const nmVolumePropertyDescriptor& oProperty)
|
|
|
|
|
/** @brief 校验后构造独立颜色表;只读属性目录,绝不修改已有 Mapper 或已发布数据。 */
|
|
|
|
|
vtkSmartPointer<vtkLookupTable> makeVolumeLookupTable(const nmVolumePropertyDescriptor& oProperty,
|
|
|
|
|
const nmPebiResultDisplaySettings& oSettings, QString* pError = nullptr)
|
|
|
|
|
{
|
|
|
|
|
double dMinimum = oProperty.m_dMinimum;
|
|
|
|
|
double dMaximum = oProperty.m_dMaximum;
|
|
|
|
|
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 = vtkSmartPointer<vtkLookupTable>::New();
|
|
|
|
|
pTable->SetNumberOfTableValues(256);
|
|
|
|
|
pTable->SetHueRange(0.67, 0.0);
|
|
|
|
|
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);
|
|
|
|
|
pTable->Build();
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -83,6 +122,44 @@ private:
|
|
|
|
|
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
|
|
|
|
|
@ -145,23 +222,31 @@ public:
|
|
|
|
|
}
|
|
|
|
|
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 bool bConstant = pProperty->m_dMinimum == pProperty->m_dMaximum;
|
|
|
|
|
const nmPebiResultDisplaySettings oSettings = m_mapDisplaySettings.value(sPropertyId);
|
|
|
|
|
vtkSmartPointer<vtkLookupTable> pTable = pPreparedTable;
|
|
|
|
|
if(pTable == nullptr) pTable = makeVolumeLookupTable(*pProperty);
|
|
|
|
|
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);
|
|
|
|
|
m_pScalarBar->setTitle(pProperty->m_sArrayName);
|
|
|
|
|
m_pScalarBar->setLookupTable(pTable);
|
|
|
|
|
m_pScalarBar->setConstantValueLabel(bConstant, QString::number(pProperty->m_dMinimum, 'g', 8));
|
|
|
|
|
m_pScalarBar->setNumberOfLabels(bConstant ? 0 : m_pScalarBar->getResponsiveLabelCount());
|
|
|
|
|
applyScalarBarState(*pProperty, oSettings, pTable);
|
|
|
|
|
m_sPropertyId = sPropertyId;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
@ -173,6 +258,7 @@ public:
|
|
|
|
|
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; ///< 是否正在显示截面,而非完整模型。
|
|
|
|
|
@ -295,6 +381,77 @@ bool nmVolumeSceneController::applyProperty(const QString& sPropertyId, QString*
|
|
|
|
|
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) ||
|
|
|
|
|
@ -391,10 +548,12 @@ bool nmVolumeSceneController::applySliceSettings(const nmVolumeSliceSettings& oS
|
|
|
|
|
if(vtkMath::IsFinite(pValues->GetComponent(nValue, 0))) { bHasFiniteValues = true; break; }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// LUT 只使用数据发布时的全局范围,不用截面的局部范围覆盖它。
|
|
|
|
|
// 每次生成截面均读取当前属性的颜色偏好,自动范围始终取完整模型范围。
|
|
|
|
|
vtkSmartPointer<vtkLookupTable> pTable;
|
|
|
|
|
if(pProperty != nullptr) {
|
|
|
|
|
pTable = makeVolumeLookupTable(*pProperty);
|
|
|
|
|
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) {
|
|
|
|
|
|