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/nmSubWxs/nmVolumeSceneController.cpp

433 lines
22 KiB
C++

#include "nmVolumeSceneController.h"
#include "nmVolumeDataRenderAdapter.h"
#include "nmVolumeSlicePipeline.h"
#include "nmWxVTKRenderContainerWidget.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 <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.")
};
/** @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 完整模型与截面共用的全局线性颜色表;常量范围只为渲染作最小扩展。 */
vtkSmartPointer<vtkLookupTable> makeVolumeLookupTable(const nmVolumePropertyDescriptor& oProperty)
{
double dMinimum = oProperty.m_dMinimum;
double dMaximum = oProperty.m_dMaximum;
if(dMinimum == dMaximum) {
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();
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 所有的候选指针,仅用于身份检查。
};
}
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 完整验证候选 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;
vtkSmartPointer<vtkLookupTable> pTable = pPreparedTable;
if(pTable == nullptr) pTable = makeVolumeLookupTable(*pProperty);
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());
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; ///< 当前数据最后一次成功应用的切片参数。
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;
}
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; }
}
}
// LUT 只使用数据发布时的全局范围,不用截面的局部范围覆盖它。
vtkSmartPointer<vtkLookupTable> pTable;
if(pProperty != nullptr) {
pTable = makeVolumeLookupTable(*pProperty);
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;
}
}